Skip to content

Commit 7c5c692

Browse files
Copilotstephentoub
andcommitted
Add comprehensive AIContent roundtrip tests for all base64 protocol types
Add Theory-based tests validating conversions to/from AIContent for ImageContentBlock, AudioContentBlock, and BlobResourceContents: - ToAIContent preserves decoded data and media type - Full roundtrip through ToAIContent → ToContentBlock - Both FromBytes() and Data/Blob setter construction paths - DataContent → ContentBlock direction (image/audio/blob) - JSON deserialization (both escaped and unescaped) through AIContent roundtrips Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent a076b94 commit 7c5c692

1 file changed

Lines changed: 274 additions & 0 deletions

File tree

tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,280 @@ public void ToChatMessage_CallToolResult_WithAnonymousTypeInContent_Works()
402402

403403
Assert.Null(exception);
404404
}
405+
406+
/// <summary>
407+
/// Provides test data for base64 AIContent roundtrip tests covering various lengths,
408+
/// the full base64 alphabet (including '+' and '/'), and with/without padding.
409+
/// </summary>
410+
public static TheoryData<byte[]> Base64TestData()
411+
{
412+
var data = new TheoryData<byte[]>
413+
{
414+
new byte[] { 0x00 }, // 1 byte, 2 padding chars: "AA=="
415+
new byte[] { 0x00, 0x01 }, // 2 bytes, 1 padding char: "AAE="
416+
new byte[] { 0x00, 0x01, 0x02 }, // 3 bytes, no padding: "AAEC"
417+
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, // produces '/' in base64: "/9j/4A=="
418+
new byte[] { 0xFB, 0xEF, 0xBE }, // produces '+' in base64: "++++"
419+
};
420+
421+
// All 256 byte values to exercise the full base64 alphabet
422+
byte[] allBytes = new byte[256];
423+
for (int i = 0; i < 256; i++)
424+
{
425+
allBytes[i] = (byte)i;
426+
}
427+
data.Add(allBytes);
428+
429+
return data;
430+
}
431+
432+
[Theory]
433+
[MemberData(nameof(Base64TestData))]
434+
public void ImageContentBlock_ToAIContent_PreservesData(byte[] originalBytes)
435+
{
436+
var image = ImageContentBlock.FromBytes(originalBytes, "image/png");
437+
438+
var aiContent = image.ToAIContent();
439+
440+
var dataContent = Assert.IsType<DataContent>(aiContent);
441+
Assert.Equal("image/png", dataContent.MediaType);
442+
Assert.Equal(originalBytes, dataContent.Data.ToArray());
443+
}
444+
445+
[Theory]
446+
[MemberData(nameof(Base64TestData))]
447+
public void ImageContentBlock_ToAIContent_RoundTrips(byte[] originalBytes)
448+
{
449+
var image = ImageContentBlock.FromBytes(originalBytes, "image/png");
450+
451+
var aiContent = Assert.IsType<DataContent>(image.ToAIContent());
452+
var roundTripped = Assert.IsType<ImageContentBlock>(aiContent.ToContentBlock());
453+
454+
Assert.Equal("image/png", roundTripped.MimeType);
455+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
456+
}
457+
458+
[Theory]
459+
[MemberData(nameof(Base64TestData))]
460+
public void ImageContentBlock_DataSetter_ToAIContent_RoundTrips(byte[] originalBytes)
461+
{
462+
string base64 = Convert.ToBase64String(originalBytes);
463+
var image = new ImageContentBlock
464+
{
465+
Data = System.Text.Encoding.UTF8.GetBytes(base64),
466+
MimeType = "image/jpeg"
467+
};
468+
469+
var aiContent = Assert.IsType<DataContent>(image.ToAIContent());
470+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
471+
472+
var roundTripped = Assert.IsType<ImageContentBlock>(aiContent.ToContentBlock());
473+
Assert.Equal("image/jpeg", roundTripped.MimeType);
474+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
475+
}
476+
477+
[Theory]
478+
[MemberData(nameof(Base64TestData))]
479+
public void AudioContentBlock_ToAIContent_PreservesData(byte[] originalBytes)
480+
{
481+
var audio = AudioContentBlock.FromBytes(originalBytes, "audio/wav");
482+
483+
var aiContent = audio.ToAIContent();
484+
485+
var dataContent = Assert.IsType<DataContent>(aiContent);
486+
Assert.Equal("audio/wav", dataContent.MediaType);
487+
Assert.Equal(originalBytes, dataContent.Data.ToArray());
488+
}
489+
490+
[Theory]
491+
[MemberData(nameof(Base64TestData))]
492+
public void AudioContentBlock_ToAIContent_RoundTrips(byte[] originalBytes)
493+
{
494+
var audio = AudioContentBlock.FromBytes(originalBytes, "audio/wav");
495+
496+
var aiContent = Assert.IsType<DataContent>(audio.ToAIContent());
497+
var roundTripped = Assert.IsType<AudioContentBlock>(aiContent.ToContentBlock());
498+
499+
Assert.Equal("audio/wav", roundTripped.MimeType);
500+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
501+
}
502+
503+
[Theory]
504+
[MemberData(nameof(Base64TestData))]
505+
public void AudioContentBlock_DataSetter_ToAIContent_RoundTrips(byte[] originalBytes)
506+
{
507+
string base64 = Convert.ToBase64String(originalBytes);
508+
var audio = new AudioContentBlock
509+
{
510+
Data = System.Text.Encoding.UTF8.GetBytes(base64),
511+
MimeType = "audio/mp3"
512+
};
513+
514+
var aiContent = Assert.IsType<DataContent>(audio.ToAIContent());
515+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
516+
517+
var roundTripped = Assert.IsType<AudioContentBlock>(aiContent.ToContentBlock());
518+
Assert.Equal("audio/mp3", roundTripped.MimeType);
519+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
520+
}
521+
522+
[Theory]
523+
[MemberData(nameof(Base64TestData))]
524+
public void BlobResourceContents_ToAIContent_PreservesData(byte[] originalBytes)
525+
{
526+
var blob = BlobResourceContents.FromBytes(originalBytes, "file:///test.bin", "application/octet-stream");
527+
var embedded = new EmbeddedResourceBlock { Resource = blob };
528+
529+
var aiContent = embedded.ToAIContent();
530+
531+
var dataContent = Assert.IsType<DataContent>(aiContent);
532+
Assert.Equal("application/octet-stream", dataContent.MediaType);
533+
Assert.Equal(originalBytes, dataContent.Data.ToArray());
534+
}
535+
536+
[Theory]
537+
[MemberData(nameof(Base64TestData))]
538+
public void BlobResourceContents_ToAIContent_RoundTrips(byte[] originalBytes)
539+
{
540+
var blob = BlobResourceContents.FromBytes(originalBytes, "file:///test.bin", "application/octet-stream");
541+
var embedded = new EmbeddedResourceBlock { Resource = blob };
542+
543+
var aiContent = Assert.IsType<DataContent>(embedded.ToAIContent());
544+
var roundTripped = Assert.IsType<EmbeddedResourceBlock>(aiContent.ToContentBlock());
545+
var roundTrippedBlob = Assert.IsType<BlobResourceContents>(roundTripped.Resource);
546+
547+
Assert.Equal("application/octet-stream", roundTrippedBlob.MimeType);
548+
Assert.Equal(originalBytes, roundTrippedBlob.DecodedData.ToArray());
549+
}
550+
551+
[Theory]
552+
[MemberData(nameof(Base64TestData))]
553+
public void BlobResourceContents_BlobSetter_ToAIContent_RoundTrips(byte[] originalBytes)
554+
{
555+
string base64 = Convert.ToBase64String(originalBytes);
556+
var blob = new BlobResourceContents
557+
{
558+
Blob = System.Text.Encoding.UTF8.GetBytes(base64),
559+
Uri = "file:///test.bin",
560+
MimeType = "application/octet-stream"
561+
};
562+
var embedded = new EmbeddedResourceBlock { Resource = blob };
563+
564+
var aiContent = Assert.IsType<DataContent>(embedded.ToAIContent());
565+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
566+
567+
var roundTripped = Assert.IsType<EmbeddedResourceBlock>(aiContent.ToContentBlock());
568+
var roundTrippedBlob = Assert.IsType<BlobResourceContents>(roundTripped.Resource);
569+
Assert.Equal("application/octet-stream", roundTrippedBlob.MimeType);
570+
Assert.Equal(originalBytes, roundTrippedBlob.DecodedData.ToArray());
571+
}
572+
573+
[Theory]
574+
[MemberData(nameof(Base64TestData))]
575+
public void DataContent_ImageToContentBlock_PreservesData(byte[] originalBytes)
576+
{
577+
var dataContent = new DataContent(originalBytes, "image/png");
578+
579+
var contentBlock = dataContent.ToContentBlock();
580+
581+
var image = Assert.IsType<ImageContentBlock>(contentBlock);
582+
Assert.Equal("image/png", image.MimeType);
583+
Assert.Equal(originalBytes, image.DecodedData.ToArray());
584+
}
585+
586+
[Theory]
587+
[MemberData(nameof(Base64TestData))]
588+
public void DataContent_AudioToContentBlock_PreservesData(byte[] originalBytes)
589+
{
590+
var dataContent = new DataContent(originalBytes, "audio/wav");
591+
592+
var contentBlock = dataContent.ToContentBlock();
593+
594+
var audio = Assert.IsType<AudioContentBlock>(contentBlock);
595+
Assert.Equal("audio/wav", audio.MimeType);
596+
Assert.Equal(originalBytes, audio.DecodedData.ToArray());
597+
}
598+
599+
[Theory]
600+
[MemberData(nameof(Base64TestData))]
601+
public void DataContent_BlobToContentBlock_PreservesData(byte[] originalBytes)
602+
{
603+
var dataContent = new DataContent(originalBytes, "application/octet-stream");
604+
605+
var contentBlock = dataContent.ToContentBlock();
606+
607+
var embedded = Assert.IsType<EmbeddedResourceBlock>(contentBlock);
608+
var blob = Assert.IsType<BlobResourceContents>(embedded.Resource);
609+
Assert.Equal("application/octet-stream", blob.MimeType);
610+
Assert.Equal(originalBytes, blob.DecodedData.ToArray());
611+
}
612+
613+
[Theory]
614+
[MemberData(nameof(Base64TestData))]
615+
public void ImageContentBlock_JsonDeserialized_ToAIContent_RoundTrips(byte[] originalBytes)
616+
{
617+
string base64 = Convert.ToBase64String(originalBytes);
618+
string json = $$"""{"type":"image","data":"{{base64}}","mimeType":"image/png"}""";
619+
620+
var image = Assert.IsType<ImageContentBlock>(
621+
JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions));
622+
var aiContent = Assert.IsType<DataContent>(image.ToAIContent());
623+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
624+
625+
var roundTripped = Assert.IsType<ImageContentBlock>(aiContent.ToContentBlock());
626+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
627+
}
628+
629+
[Theory]
630+
[MemberData(nameof(Base64TestData))]
631+
public void ImageContentBlock_EscapedJsonDeserialized_ToAIContent_RoundTrips(byte[] originalBytes)
632+
{
633+
string base64 = Convert.ToBase64String(originalBytes);
634+
string json = $$"""{"type":"image","data":"{{base64.Replace("/", "\\/")}}","mimeType":"image/png"}""";
635+
636+
var image = Assert.IsType<ImageContentBlock>(
637+
JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions));
638+
var aiContent = Assert.IsType<DataContent>(image.ToAIContent());
639+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
640+
641+
var roundTripped = Assert.IsType<ImageContentBlock>(aiContent.ToContentBlock());
642+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
643+
}
644+
645+
[Theory]
646+
[MemberData(nameof(Base64TestData))]
647+
public void AudioContentBlock_EscapedJsonDeserialized_ToAIContent_RoundTrips(byte[] originalBytes)
648+
{
649+
string base64 = Convert.ToBase64String(originalBytes);
650+
string json = $$"""{"type":"audio","data":"{{base64.Replace("/", "\\/")}}","mimeType":"audio/wav"}""";
651+
652+
var audio = Assert.IsType<AudioContentBlock>(
653+
JsonSerializer.Deserialize<ContentBlock>(json, McpJsonUtilities.DefaultOptions));
654+
var aiContent = Assert.IsType<DataContent>(audio.ToAIContent());
655+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
656+
657+
var roundTripped = Assert.IsType<AudioContentBlock>(aiContent.ToContentBlock());
658+
Assert.Equal(originalBytes, roundTripped.DecodedData.ToArray());
659+
}
660+
661+
[Theory]
662+
[MemberData(nameof(Base64TestData))]
663+
public void BlobResourceContents_EscapedJsonDeserialized_ToAIContent_RoundTrips(byte[] originalBytes)
664+
{
665+
string base64 = Convert.ToBase64String(originalBytes);
666+
string json = $$"""{"uri":"file:///test.bin","blob":"{{base64.Replace("/", "\\/")}}","mimeType":"application/octet-stream"}""";
667+
668+
var blob = Assert.IsType<BlobResourceContents>(
669+
JsonSerializer.Deserialize<ResourceContents>(json, McpJsonUtilities.DefaultOptions));
670+
var embedded = new EmbeddedResourceBlock { Resource = blob };
671+
672+
var aiContent = Assert.IsType<DataContent>(embedded.ToAIContent());
673+
Assert.Equal(originalBytes, aiContent.Data.ToArray());
674+
675+
var roundTripped = Assert.IsType<EmbeddedResourceBlock>(aiContent.ToContentBlock());
676+
var roundTrippedBlob = Assert.IsType<BlobResourceContents>(roundTripped.Resource);
677+
Assert.Equal(originalBytes, roundTrippedBlob.DecodedData.ToArray());
678+
}
405679
}
406680

407681
// Test type for named user-defined type test

0 commit comments

Comments
 (0)