Add comprehensive edge case tests for JSON-RPC payload shapes (#1143)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
Copilot 2026-01-14 14:26:18 +02:00 • committed by GitHub
commit dada237416
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -302,4 +302,462 @@ public static class JsonRpcMessageConverterTests
Assert.Equal(original.Error.Code, deserialized.Error.Code);
Assert.Equal(original.Error.Message, deserialized.Error.Message);
}
[Fact]
public static void Deserialize_ResponseWithExplicitNullError_TreatedAsSuccessResponse()
{
// Arrange - Some implementations may include "error": null in success responses.
// While JSON-RPC 2.0 spec says responses have either result OR error (not both),
// this tests that we handle the lenient case gracefully.
string json = """{"jsonrpc":"2.0","id":1,"result":{"data":"value"},"error":null}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert - Should be a success response since error is null
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.Equal(new RequestId(1), response.Id);
Assert.NotNull(response.Result);
Assert.Equal("value", response.Result["data"]?.GetValue<string>());
}
[Fact]
public static void Deserialize_ResponseWithNullResultAndNullError_TreatedAsSuccessWithNullResult()
{
// Arrange - Both result and error are explicitly null.
// Per JSON-RPC 2.0, result: null is a valid success response value.
string json = """{"jsonrpc":"2.0","id":1,"result":null,"error":null}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert - result: null is valid, error: null is ignored
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.Equal(new RequestId(1), response.Id);
Assert.Null(response.Result);
}
[Fact]
public static void Deserialize_ResponseWithBothErrorAndResult_ErrorTakesPrecedence()
{
// Arrange - JSON-RPC 2.0 spec says a response should have either result OR error, not both.
// However, if a non-compliant implementation sends both, we verify consistent behavior:
// error takes precedence regardless of property order.
string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid"},"result":{"data":"ignored"}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert - Error takes precedence
Assert.NotNull(message);
Assert.IsType<JsonRpcError>(message);
var error = (JsonRpcError)message;
Assert.Equal(new RequestId(1), error.Id);
Assert.Equal(-32600, error.Error.Code);
}
[Fact]
public static void Deserialize_ResponseWithBothResultAndError_ErrorTakesPrecedenceRegardlessOfOrder()
{
// Arrange - Same as above but with result appearing before error in the JSON.
// Validates that property order doesn't affect the precedence logic.
string json = """{"jsonrpc":"2.0","id":1,"result":{"data":"ignored"},"error":{"code":-32600,"message":"Invalid"}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert - Error still takes precedence
Assert.NotNull(message);
Assert.IsType<JsonRpcError>(message);
var error = (JsonRpcError)message;
Assert.Equal(new RequestId(1), error.Id);
Assert.Equal(-32600, error.Error.Code);
}
[Fact]
public static void Deserialize_RequestWithEmptyStringId_IsValidRequest()
{
// Arrange - Empty string is a valid ID per JSON-RPC 2.0
string json = """{"jsonrpc":"2.0","id":"","method":"test"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal(new RequestId(""), request.Id);
Assert.Equal("test", request.Method);
}
[Fact]
public static void Deserialize_RequestWithZeroId_IsValidRequest()
{
// Arrange - Zero is a valid numeric ID
string json = """{"jsonrpc":"2.0","id":0,"method":"test"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal(new RequestId(0), request.Id);
}
[Fact]
public static void Deserialize_RequestWithNegativeId_IsValidRequest()
{
// Arrange - Negative numbers are valid IDs
string json = """{"jsonrpc":"2.0","id":-42,"method":"test"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal(new RequestId(-42), request.Id);
}
[Fact]
public static void Deserialize_RequestWithLargeNumericId_IsValidRequest()
{
// Arrange - Large number ID
string json = """{"jsonrpc":"2.0","id":9223372036854775807,"method":"test"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal(new RequestId(long.MaxValue), request.Id);
}
[Fact]
public static void Deserialize_NotificationWithExplicitNullParams_IsValidNotification()
{
// Arrange - params: null is valid
string json = """{"jsonrpc":"2.0","method":"notify","params":null}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcNotification>(message);
var notification = (JsonRpcNotification)message;
Assert.Equal("notify", notification.Method);
Assert.Null(notification.Params);
}
[Fact]
public static void Deserialize_RequestWithEmptyObjectParams_IsValidRequest()
{
// Arrange - Empty object params
string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.NotNull(request.Params);
Assert.IsType<JsonObject>(request.Params);
}
[Fact]
public static void Deserialize_RequestWithArrayParams_IsValidRequest()
{
// Arrange - Array params (positional arguments per JSON-RPC 2.0)
string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":["arg1",42,true]}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.NotNull(request.Params);
Assert.IsType<JsonArray>(request.Params);
var array = (JsonArray)request.Params;
Assert.Equal(3, array.Count);
Assert.Equal("arg1", array[0]?.GetValue<string>());
Assert.Equal(42, array[1]?.GetValue<int>());
Assert.True(array[2]?.GetValue<bool>());
}
[Fact]
public static void Deserialize_ErrorWithNullData_IsValidError()
{
// Arrange - Error with explicit null data
string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid","data":null}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcError>(message);
var error = (JsonRpcError)message;
Assert.Equal(-32600, error.Error.Code);
Assert.Equal("Invalid", error.Error.Message);
Assert.Null(error.Error.Data);
}
[Fact]
public static void Deserialize_ErrorWithComplexData_IsValidError()
{
// Arrange - Error with complex object data
string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"Invalid","data":{"details":["error1","error2"],"field":"name"}}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcError>(message);
var error = (JsonRpcError)message;
Assert.NotNull(error.Error.Data);
}
[Fact]
public static void Deserialize_RequestWithPropertiesInUnusualOrder_IsValidRequest()
{
// Arrange - Properties in unusual order (params, method, id, jsonrpc)
string json = """{"params":{"key":"value"},"method":"test","id":123,"jsonrpc":"2.0"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal("2.0", request.JsonRpc);
Assert.Equal(new RequestId(123), request.Id);
Assert.Equal("test", request.Method);
Assert.Equal("value", request.Params?["key"]?.GetValue<string>());
}
[Fact]
public static void Deserialize_ResponseWithPropertiesInUnusualOrder_IsValidResponse()
{
// Arrange - Properties in unusual order (result, id, jsonrpc)
string json = """{"result":{"status":"ok"},"id":"abc","jsonrpc":"2.0"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.Equal("2.0", response.JsonRpc);
Assert.Equal(new RequestId("abc"), response.Id);
Assert.Equal("ok", response.Result?["status"]?.GetValue<string>());
}
[Fact]
public static void Deserialize_MessageWithUnicodeInStringValues_PreservesUnicode()
{
// Arrange - Unicode characters in method name, ID, and params
string json = """{"jsonrpc":"2.0","id":"请求-123","method":"日本語/メソッド","params":{"emoji":"🚀","text":"Ελληνικά"}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal(new RequestId("请求-123"), request.Id);
Assert.Equal("日本語/メソッド", request.Method);
Assert.Equal("🚀", request.Params?["emoji"]?.GetValue<string>());
Assert.Equal("Ελληνικά", request.Params?["text"]?.GetValue<string>());
}
[Fact]
public static void Deserialize_MessageWithEscapedCharacters_HandlesEscaping()
{
// Arrange - JSON with escaped characters
string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{"path":"C:\\Users\\test","quote":"He said \"hello\""}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal("C:\\Users\\test", request.Params?["path"]?.GetValue<string>());
Assert.Equal("He said \"hello\"", request.Params?["quote"]?.GetValue<string>());
}
[Fact]
public static void Deserialize_ResponseWithPrimitiveResult_IsValid()
{
// Arrange - Result is a primitive string, not an object
string json = """{"jsonrpc":"2.0","id":1,"result":"simple string result"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.NotNull(response.Result);
Assert.Equal("simple string result", response.Result.GetValue<string>());
}
[Fact]
public static void Deserialize_ResponseWithNumericResult_IsValid()
{
// Arrange - Result is a number
string json = """{"jsonrpc":"2.0","id":1,"result":42}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.NotNull(response.Result);
Assert.Equal(42, response.Result.GetValue<int>());
}
[Fact]
public static void Deserialize_ResponseWithBooleanResult_IsValid()
{
// Arrange - Result is a boolean
string json = """{"jsonrpc":"2.0","id":1,"result":true}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.NotNull(response.Result);
Assert.True(response.Result.GetValue<bool>());
}
[Fact]
public static void Deserialize_ResponseWithArrayResult_IsValid()
{
// Arrange - Result is an array
string json = """{"jsonrpc":"2.0","id":1,"result":[1,2,3,"four"]}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcResponse>(message);
var response = (JsonRpcResponse)message;
Assert.NotNull(response.Result);
Assert.IsType<JsonArray>(response.Result);
var array = (JsonArray)response.Result;
Assert.Equal(4, array.Count);
}
[Fact]
public static void Deserialize_MessageWithMultipleUnknownPropertiesInterspersed_IgnoresUnknown()
{
// Arrange - Unknown properties interspersed with known ones
string json = """{"unknown1":"x","jsonrpc":"2.0","unknown2":123,"id":1,"unknown3":true,"method":"test","unknown4":null}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.Equal("2.0", request.JsonRpc);
Assert.Equal(new RequestId(1), request.Id);
Assert.Equal("test", request.Method);
}
[Fact]
public static void Deserialize_NotificationWithMethodOnly_NoParams_IsValid()
{
// Arrange - Minimal notification with no params
string json = """{"jsonrpc":"2.0","method":"ping"}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcNotification>(message);
var notification = (JsonRpcNotification)message;
Assert.Equal("ping", notification.Method);
Assert.Null(notification.Params);
}
[Fact]
public static void Deserialize_RequestWithNestedComplexParams_IsValid()
{
// Arrange - Deeply nested params structure
string json = """{"jsonrpc":"2.0","id":1,"method":"test","params":{"level1":{"level2":{"level3":{"value":"deep"}}}}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcRequest>(message);
var request = (JsonRpcRequest)message;
Assert.NotNull(request.Params);
var deepValue = request.Params["level1"]?["level2"]?["level3"]?["value"]?.GetValue<string>();
Assert.Equal("deep", deepValue);
}
[Fact]
public static void Deserialize_ErrorWithNumericData_IsValid()
{
// Arrange - Error with numeric data (not object or string)
string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Error","data":42}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcError>(message);
var error = (JsonRpcError)message;
Assert.NotNull(error.Error.Data);
}
[Fact]
public static void Deserialize_ErrorWithArrayData_IsValid()
{
// Arrange - Error with array data
string json = """{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Multiple errors","data":["error1","error2","error3"]}}""";
// Act
var message = JsonSerializer.Deserialize<JsonRpcMessage>(json, McpJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(message);
Assert.IsType<JsonRpcError>(message);
var error = (JsonRpcError)message;
Assert.NotNull(error.Error.Data);
}
}