11using ModelContextProtocol . Protocol ;
2+ using System . Text ;
23using System . Text . Json ;
34
45namespace ModelContextProtocol . Tests . Protocol ;
@@ -323,4 +324,242 @@ public void AudioContentBlock_Deserialization_HandlesEscapedForwardSlashInBase64
323324 Assert . Equal ( base64 , System . Text . Encoding . UTF8 . GetString ( audio . Data . ToArray ( ) ) ) ;
324325 Assert . Equal ( originalBytes , audio . DecodedData . ToArray ( ) ) ;
325326 }
327+
328+ /// <summary>
329+ /// Provides test data for base64 roundtrip tests. Each entry is a byte array that exercises
330+ /// different base64 encoding characteristics:
331+ /// - Various lengths producing 0, 1, or 2 padding characters
332+ /// - Bytes that produce all 64 base64 alphabet characters including '+' and '/'
333+ /// </summary>
334+ public static TheoryData < byte [ ] > Base64TestData ( )
335+ {
336+ var data = new TheoryData < byte [ ] >
337+ {
338+ Array . Empty < byte > ( ) , // empty: ""
339+ new byte [ ] { 0x00 } , // 1 byte, 2 padding chars: "AA=="
340+ new byte [ ] { 0x00 , 0x01 } , // 2 bytes, 1 padding char: "AAE="
341+ new byte [ ] { 0x00 , 0x01 , 0x02 } , // 3 bytes, no padding: "AAEC"
342+ new byte [ ] { 0xFF , 0xD8 , 0xFF , 0xE0 } , // produces '/' in base64: "/9j/4A=="
343+ new byte [ ] { 0xFB , 0xEF , 0xBE } , // produces '+' in base64: "+++"
344+ } ;
345+
346+ // All 256 byte values to exercise the full base64 alphabet
347+ byte [ ] allBytes = new byte [ 256 ] ;
348+ for ( int i = 0 ; i < 256 ; i ++ )
349+ {
350+ allBytes [ i ] = ( byte ) i ;
351+ }
352+ data . Add ( allBytes ) ;
353+
354+ // Larger payload (1024 bytes)
355+ byte [ ] largePayload = new byte [ 1024 ] ;
356+ new Random ( 42 ) . NextBytes ( largePayload ) ;
357+ data . Add ( largePayload ) ;
358+
359+ return data ;
360+ }
361+
362+ [ Theory ]
363+ [ MemberData ( nameof ( Base64TestData ) ) ]
364+ public void ImageContentBlock_FromBytes_RoundtripsCorrectly ( byte [ ] originalBytes )
365+ {
366+ string expectedBase64 = Convert . ToBase64String ( originalBytes ) ;
367+
368+ var image = ImageContentBlock . FromBytes ( originalBytes , "image/png" ) ;
369+
370+ Assert . Equal ( "image/png" , image . MimeType ) ;
371+ Assert . Equal ( originalBytes , image . DecodedData . ToArray ( ) ) ;
372+ Assert . Equal ( expectedBase64 , Encoding . UTF8 . GetString ( image . Data . ToArray ( ) ) ) ;
373+ }
374+
375+ [ Theory ]
376+ [ MemberData ( nameof ( Base64TestData ) ) ]
377+ public void ImageContentBlock_DataSetter_RoundtripsCorrectly ( byte [ ] originalBytes )
378+ {
379+ string base64 = Convert . ToBase64String ( originalBytes ) ;
380+ byte [ ] base64Utf8 = Encoding . UTF8 . GetBytes ( base64 ) ;
381+
382+ var image = new ImageContentBlock { Data = base64Utf8 , MimeType = "image/png" } ;
383+
384+ Assert . Equal ( base64Utf8 , image . Data . ToArray ( ) ) ;
385+ Assert . Equal ( originalBytes , image . DecodedData . ToArray ( ) ) ;
386+ }
387+
388+ [ Theory ]
389+ [ MemberData ( nameof ( Base64TestData ) ) ]
390+ public void ImageContentBlock_JsonRoundtrip_PreservesData ( byte [ ] originalBytes )
391+ {
392+ string base64 = Convert . ToBase64String ( originalBytes ) ;
393+ byte [ ] base64Utf8 = Encoding . UTF8 . GetBytes ( base64 ) ;
394+
395+ var original = new ImageContentBlock { Data = base64Utf8 , MimeType = "image/png" } ;
396+ string json = JsonSerializer . Serialize < ContentBlock > ( original , McpJsonUtilities . DefaultOptions ) ;
397+ var deserialized = Assert . IsType < ImageContentBlock > (
398+ JsonSerializer . Deserialize < ContentBlock > ( json , McpJsonUtilities . DefaultOptions ) ) ;
399+
400+ Assert . Equal ( base64Utf8 , deserialized . Data . ToArray ( ) ) ;
401+ Assert . Equal ( originalBytes , deserialized . DecodedData . ToArray ( ) ) ;
402+ }
403+
404+ [ Theory ]
405+ [ MemberData ( nameof ( Base64TestData ) ) ]
406+ public void ImageContentBlock_FromBytes_JsonRoundtrip_PreservesData ( byte [ ] originalBytes )
407+ {
408+ string expectedBase64 = Convert . ToBase64String ( originalBytes ) ;
409+
410+ var original = ImageContentBlock . FromBytes ( originalBytes , "image/jpeg" ) ;
411+ string json = JsonSerializer . Serialize < ContentBlock > ( original , McpJsonUtilities . DefaultOptions ) ;
412+ var deserialized = Assert . IsType < ImageContentBlock > (
413+ JsonSerializer . Deserialize < ContentBlock > ( json , McpJsonUtilities . DefaultOptions ) ) ;
414+
415+ Assert . Equal ( expectedBase64 , Encoding . UTF8 . GetString ( deserialized . Data . ToArray ( ) ) ) ;
416+ Assert . Equal ( originalBytes , deserialized . DecodedData . ToArray ( ) ) ;
417+ }
418+
419+ [ Theory ]
420+ [ MemberData ( nameof ( Base64TestData ) ) ]
421+ public void ImageContentBlock_EscapedJsonRoundtrip_PreservesData ( byte [ ] originalBytes )
422+ {
423+ string base64 = Convert . ToBase64String ( originalBytes ) ;
424+
425+ // Simulate JSON encoder that escapes '/' as '\/'
426+ string json = $$ """ {"type":"image","data":"{{ base64 . Replace ( "/" , "\\ /" ) }} ","mimeType":"image/png"}""" ;
427+
428+ var deserialized = Assert . IsType < ImageContentBlock > (
429+ JsonSerializer . Deserialize < ContentBlock > ( json , McpJsonUtilities . DefaultOptions ) ) ;
430+
431+ Assert . Equal ( base64 , Encoding . UTF8 . GetString ( deserialized . Data . ToArray ( ) ) ) ;
432+ Assert . Equal ( originalBytes , deserialized . DecodedData . ToArray ( ) ) ;
433+ }
434+
435+ [ Fact ]
436+ public void ImageContentBlock_DataSetterInvalidatesCachedDecodedData ( )
437+ {
438+ byte [ ] bytes1 = [ 1 , 2 , 3 ] ;
439+ var image = ImageContentBlock . FromBytes ( bytes1 , "image/png" ) ;
440+
441+ // Access DecodedData to populate cache
442+ Assert . Equal ( bytes1 , image . DecodedData . ToArray ( ) ) ;
443+
444+ // Set new Data to invalidate cache
445+ byte [ ] newBytes = [ 4 , 5 , 6 ] ;
446+ string newBase64 = Convert . ToBase64String ( newBytes ) ;
447+ image . Data = Encoding . UTF8 . GetBytes ( newBase64 ) ;
448+
449+ Assert . Equal ( newBytes , image . DecodedData . ToArray ( ) ) ;
450+ }
451+
452+ [ Theory ]
453+ [ MemberData ( nameof ( Base64TestData ) ) ]
454+ public void AudioContentBlock_FromBytes_RoundtripsCorrectly ( byte [ ] originalBytes )
455+ {
456+ string expectedBase64 = Convert . ToBase64String ( originalBytes ) ;
457+
458+ var audio = AudioContentBlock . FromBytes ( originalBytes , "audio/wav" ) ;
459+
460+ Assert . Equal ( "audio/wav" , audio . MimeType ) ;
461+ Assert . Equal ( originalBytes , audio . DecodedData . ToArray ( ) ) ;
462+ Assert . Equal ( expectedBase64 , Encoding . UTF8 . GetString ( audio . Data . ToArray ( ) ) ) ;
463+ }
464+
465+ [ Theory ]
466+ [ MemberData ( nameof ( Base64TestData ) ) ]
467+ public void AudioContentBlock_DataSetter_RoundtripsCorrectly ( byte [ ] originalBytes )
468+ {
469+ string base64 = Convert . ToBase64String ( originalBytes ) ;
470+ byte [ ] base64Utf8 = Encoding . UTF8 . GetBytes ( base64 ) ;
471+
472+ var audio = new AudioContentBlock { Data = base64Utf8 , MimeType = "audio/wav" } ;
473+
474+ Assert . Equal ( base64Utf8 , audio . Data . ToArray ( ) ) ;
475+ Assert . Equal ( originalBytes , audio . DecodedData . ToArray ( ) ) ;
476+ }
477+
478+ [ Theory ]
479+ [ MemberData ( nameof ( Base64TestData ) ) ]
480+ public void AudioContentBlock_JsonRoundtrip_PreservesData ( byte [ ] originalBytes )
481+ {
482+ string base64 = Convert . ToBase64String ( originalBytes ) ;
483+ byte [ ] base64Utf8 = Encoding . UTF8 . GetBytes ( base64 ) ;
484+
485+ var original = new AudioContentBlock { Data = base64Utf8 , MimeType = "audio/wav" } ;
486+ string json = JsonSerializer . Serialize < ContentBlock > ( original , McpJsonUtilities . DefaultOptions ) ;
487+ var deserialized = Assert . IsType < AudioContentBlock > (
488+ JsonSerializer . Deserialize < ContentBlock > ( json , McpJsonUtilities . DefaultOptions ) ) ;
489+
490+ Assert . Equal ( base64Utf8 , deserialized . Data . ToArray ( ) ) ;
491+ Assert . Equal ( originalBytes , deserialized . DecodedData . ToArray ( ) ) ;
492+ }
493+
494+ [ Theory ]
495+ [ MemberData ( nameof ( Base64TestData ) ) ]
496+ public void AudioContentBlock_FromBytes_JsonRoundtrip_PreservesData ( byte [ ] originalBytes )
497+ {
498+ string expectedBase64 = Convert . ToBase64String ( originalBytes ) ;
499+
500+ var original = AudioContentBlock . FromBytes ( originalBytes , "audio/mp3" ) ;
501+ string json = JsonSerializer . Serialize < ContentBlock > ( original , McpJsonUtilities . DefaultOptions ) ;
502+ var deserialized = Assert . IsType < AudioContentBlock > (
503+ JsonSerializer . Deserialize < ContentBlock > ( json , McpJsonUtilities . DefaultOptions ) ) ;
504+
505+ Assert . Equal ( expectedBase64 , Encoding . UTF8 . GetString ( deserialized . Data . ToArray ( ) ) ) ;
506+ Assert . Equal ( originalBytes , deserialized . DecodedData . ToArray ( ) ) ;
507+ }
508+
509+ [ Theory ]
510+ [ MemberData ( nameof ( Base64TestData ) ) ]
511+ public void AudioContentBlock_EscapedJsonRoundtrip_PreservesData ( byte [ ] originalBytes )
512+ {
513+ string base64 = Convert . ToBase64String ( originalBytes ) ;
514+
515+ string json = $$ """ {"type":"audio","data":"{{ base64 . Replace ( "/" , "\\ /" ) }} ","mimeType":"audio/wav"}""" ;
516+
517+ var deserialized = Assert . IsType < AudioContentBlock > (
518+ JsonSerializer . Deserialize < ContentBlock > ( json , McpJsonUtilities . DefaultOptions ) ) ;
519+
520+ Assert . Equal ( base64 , Encoding . UTF8 . GetString ( deserialized . Data . ToArray ( ) ) ) ;
521+ Assert . Equal ( originalBytes , deserialized . DecodedData . ToArray ( ) ) ;
522+ }
523+
524+ [ Fact ]
525+ public void AudioContentBlock_DataSetterInvalidatesCachedDecodedData ( )
526+ {
527+ byte [ ] bytes1 = [ 1 , 2 , 3 ] ;
528+ var audio = AudioContentBlock . FromBytes ( bytes1 , "audio/wav" ) ;
529+
530+ Assert . Equal ( bytes1 , audio . DecodedData . ToArray ( ) ) ;
531+
532+ byte [ ] newBytes = [ 4 , 5 , 6 ] ;
533+ string newBase64 = Convert . ToBase64String ( newBytes ) ;
534+ audio . Data = Encoding . UTF8 . GetBytes ( newBase64 ) ;
535+
536+ Assert . Equal ( newBytes , audio . DecodedData . ToArray ( ) ) ;
537+ }
538+
539+ [ Theory ]
540+ [ MemberData ( nameof ( Base64TestData ) ) ]
541+ public void ImageContentBlock_FromBytes_LazilyEncodesData ( byte [ ] originalBytes )
542+ {
543+ // FromBytes should only decode when Data is accessed
544+ var image = ImageContentBlock . FromBytes ( originalBytes , "image/png" ) ;
545+
546+ // First, access DecodedData without touching Data
547+ Assert . Equal ( originalBytes , image . DecodedData . ToArray ( ) ) ;
548+
549+ // Now access Data and verify it lazily encoded correctly
550+ string expectedBase64 = Convert . ToBase64String ( originalBytes ) ;
551+ Assert . Equal ( expectedBase64 , Encoding . UTF8 . GetString ( image . Data . ToArray ( ) ) ) ;
552+ }
553+
554+ [ Theory ]
555+ [ MemberData ( nameof ( Base64TestData ) ) ]
556+ public void AudioContentBlock_FromBytes_LazilyEncodesData ( byte [ ] originalBytes )
557+ {
558+ var audio = AudioContentBlock . FromBytes ( originalBytes , "audio/wav" ) ;
559+
560+ Assert . Equal ( originalBytes , audio . DecodedData . ToArray ( ) ) ;
561+
562+ string expectedBase64 = Convert . ToBase64String ( originalBytes ) ;
563+ Assert . Equal ( expectedBase64 , Encoding . UTF8 . GetString ( audio . Data . ToArray ( ) ) ) ;
564+ }
326565}
0 commit comments