csharp-sdk/tests/ModelContextProtocol.Tests/Protocol/ReferenceTests.cs
Jeff Handley d4ee78797b
Ignore unexpected properties when deserializing a ContentBlock (#956)
* Ignore unexpected properties when deserializing a ContentBlock

* Fix deserialization of Reference.Title and more bugs for unknown props

* Remove comments about skipping additional properties
2025-11-09 01:00:55 -08:00

412 lines
13 KiB
C#

using ModelContextProtocol.Protocol;
using System.Text.Json;
namespace ModelContextProtocol.Tests.Protocol;
public static class ReferenceTests
{
[Fact]
public static void PromptReference_UnknownArrayProperty_IsIgnored()
{
// This test verifies that the Reference.Converter properly skips unknown properties
// even when they contain complex structures like arrays or objects.
//
// In this unexpected JSON, "unknownArray" appears inside a prompt reference (where it doesn't belong).
// The converter should gracefully ignore this unknown property and successfully deserialize
// the rest of the reference.
const string jsonWithUnknownArray = """
{
"type": "ref/prompt",
"name": "test_prompt",
"title": "Test Prompt",
"unknownArray": [
{
"nested": "value"
},
{
"another": "object"
}
]
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithUnknownArray,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var promptRef = Assert.IsType<PromptReference>(result);
Assert.Equal("ref/prompt", promptRef.Type);
Assert.Equal("test_prompt", promptRef.Name);
Assert.Equal("Test Prompt", promptRef.Title);
}
[Fact]
public static void ResourceReference_UnknownObjectProperty_IsIgnored()
{
// Test that unknown properties with nested objects are properly skipped
const string jsonWithUnknownObject = """
{
"type": "ref/resource",
"uri": "file:///test/resource",
"unknownObject": {
"deeply": {
"nested": {
"value": "should be ignored"
}
}
}
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithUnknownObject,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var resourceRef = Assert.IsType<ResourceTemplateReference>(result);
Assert.Equal("ref/resource", resourceRef.Type);
Assert.Equal("file:///test/resource", resourceRef.Uri);
}
[Fact]
public static void PromptReference_UnknownMixedProperties_AreIgnored()
{
// Test multiple unknown properties with different types
const string jsonWithMixedUnknown = """
{
"type": "ref/prompt",
"name": "my_prompt",
"unknownString": "value",
"unknownNumber": 42,
"unknownArray": [1, 2, 3],
"unknownObject": {"key": "value"},
"unknownBool": true,
"unknownNull": null
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithMixedUnknown,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var promptRef = Assert.IsType<PromptReference>(result);
Assert.Equal("ref/prompt", promptRef.Type);
Assert.Equal("my_prompt", promptRef.Name);
}
[Fact]
public static void ResourceReference_UnknownNestedArrays_AreIgnored()
{
// Test complex unknown properties with arrays of objects
const string jsonWithNestedArrays = """
{
"type": "ref/resource",
"uri": "resource://test/{id}",
"unknownComplex": [
{
"nested": [
{"deep": "value1"},
{"deep": "value2"}
]
},
{
"nested": [
{"deep": "value3"}
]
}
]
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithNestedArrays,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var resourceRef = Assert.IsType<ResourceTemplateReference>(result);
Assert.Equal("ref/resource", resourceRef.Type);
Assert.Equal("resource://test/{id}", resourceRef.Uri);
}
[Fact]
public static void PromptReference_MultipleUnknownProperties_AllIgnored()
{
// Test that multiple unknown properties are all properly skipped
const string jsonWithMultipleUnknown = """
{
"type": "ref/prompt",
"unknownOne": {"a": 1},
"name": "test",
"unknownTwo": [1, 2, 3],
"title": "Test Title",
"unknownThree": {"b": {"c": "d"}}
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithMultipleUnknown,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var promptRef = Assert.IsType<PromptReference>(result);
Assert.Equal("ref/prompt", promptRef.Type);
Assert.Equal("test", promptRef.Name);
Assert.Equal("Test Title", promptRef.Title);
}
[Fact]
public static void ResourceReference_UnknownArrayOfArrays_IsIgnored()
{
// Test deeply nested array structures in unknown properties
const string jsonWithArrayOfArrays = """
{
"type": "ref/resource",
"uri": "http://example.com/resource",
"unknownNested": [
[
[1, 2, 3],
[4, 5, 6]
],
[
[7, 8, 9]
]
]
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithArrayOfArrays,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var resourceRef = Assert.IsType<ResourceTemplateReference>(result);
Assert.Equal("ref/resource", resourceRef.Type);
Assert.Equal("http://example.com/resource", resourceRef.Uri);
}
[Fact]
public static void PromptReference_EmptyUnknownArray_IsIgnored()
{
// Test empty arrays in unknown properties
const string jsonWithEmptyArray = """
{
"type": "ref/prompt",
"name": "prompt",
"unknownEmpty": []
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithEmptyArray,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var promptRef = Assert.IsType<PromptReference>(result);
Assert.Equal("ref/prompt", promptRef.Type);
Assert.Equal("prompt", promptRef.Name);
}
[Fact]
public static void ResourceReference_EmptyUnknownObject_IsIgnored()
{
// Test empty objects in unknown properties
const string jsonWithEmptyObject = """
{
"type": "ref/resource",
"uri": "test://resource",
"unknownEmpty": {}
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithEmptyObject,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var resourceRef = Assert.IsType<ResourceTemplateReference>(result);
Assert.Equal("ref/resource", resourceRef.Type);
Assert.Equal("test://resource", resourceRef.Uri);
}
[Fact]
public static void PromptReference_UnknownPropertiesBetweenRequired_AreIgnored()
{
// Test unknown properties interspersed with required ones
const string jsonWithInterspersedUnknown = """
{
"unknownFirst": {"x": 1},
"type": "ref/prompt",
"unknownSecond": [1, 2],
"name": "my_prompt",
"unknownThird": {"nested": {"value": true}},
"title": "My Prompt"
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithInterspersedUnknown,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var promptRef = Assert.IsType<PromptReference>(result);
Assert.Equal("ref/prompt", promptRef.Type);
Assert.Equal("my_prompt", promptRef.Name);
Assert.Equal("My Prompt", promptRef.Title);
}
[Fact]
public static void ResourceReference_VeryDeeplyNestedUnknown_IsIgnored()
{
// Test very deeply nested structures in unknown properties
const string jsonWithVeryDeepNesting = """
{
"type": "ref/resource",
"uri": "deep://resource",
"unknownDeep": {
"level1": {
"level2": {
"level3": {
"level4": {
"level5": {
"value": "deep"
}
}
}
}
}
}
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithVeryDeepNesting,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var resourceRef = Assert.IsType<ResourceTemplateReference>(result);
Assert.Equal("ref/resource", resourceRef.Type);
Assert.Equal("deep://resource", resourceRef.Uri);
}
[Fact]
public static void PromptReference_SerializationRoundTrip_PreservesKnownProperties()
{
// Test that serialization/deserialization preserves known properties
var original = new PromptReference
{
Name = "test_prompt",
Title = "Test Prompt Title"
};
var json = JsonSerializer.Serialize<Reference>(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<Reference>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
var promptRef = Assert.IsType<PromptReference>(deserialized);
Assert.Equal(original.Type, promptRef.Type);
Assert.Equal(original.Name, promptRef.Name);
Assert.Equal(original.Title, promptRef.Title);
}
[Fact]
public static void ResourceReference_SerializationRoundTrip_PreservesKnownProperties()
{
// Test that serialization/deserialization preserves known properties
var original = new ResourceTemplateReference
{
Uri = "file:///path/to/resource"
};
var json = JsonSerializer.Serialize<Reference>(original, McpJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<Reference>(json, McpJsonUtilities.DefaultOptions);
Assert.NotNull(deserialized);
var resourceRef = Assert.IsType<ResourceTemplateReference>(deserialized);
Assert.Equal(original.Type, resourceRef.Type);
Assert.Equal(original.Uri, resourceRef.Uri);
}
[Fact]
public static void PromptReference_WithoutTitle_Deserializes()
{
// Test that title is optional
const string jsonWithoutTitle = """
{
"type": "ref/prompt",
"name": "minimal_prompt"
}
""";
var result = JsonSerializer.Deserialize<Reference>(
jsonWithoutTitle,
McpJsonUtilities.DefaultOptions);
Assert.NotNull(result);
var promptRef = Assert.IsType<PromptReference>(result);
Assert.Equal("ref/prompt", promptRef.Type);
Assert.Equal("minimal_prompt", promptRef.Name);
Assert.Null(promptRef.Title);
}
[Fact]
public static void Reference_UnknownType_ThrowsException()
{
// Test that unknown reference types throw an exception
const string jsonWithUnknownType = """
{
"type": "ref/unknown",
"name": "test"
}
""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize<Reference>(jsonWithUnknownType, McpJsonUtilities.DefaultOptions));
}
[Fact]
public static void PromptReference_MissingName_ThrowsException()
{
// Test that missing required name property throws an exception
const string jsonMissingName = """
{
"type": "ref/prompt",
"title": "Test"
}
""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize<Reference>(jsonMissingName, McpJsonUtilities.DefaultOptions));
}
[Fact]
public static void ResourceReference_MissingUri_ThrowsException()
{
// Test that missing required uri property throws an exception
const string jsonMissingUri = """
{
"type": "ref/resource"
}
""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize<Reference>(jsonMissingUri, McpJsonUtilities.DefaultOptions));
}
}