using Xunit;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Svrnty.MCP.AspNetCore.Extensions;
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Svrnty.MCP.Core.Tests;
///
/// Unit tests for HttpTransport following TDD approach.
/// Tests JSON-RPC 2.0 protocol over HTTP REST endpoints.
///
public class HttpTransportTests : IDisposable
{
private readonly WebApplication _app;
private readonly HttpClient _client;
private readonly McpServer _server;
public HttpTransportTests()
{
// Create a test MCP server with a mock tool
var registry = new ToolRegistry();
registry.AddTool(new TestTool());
_server = new McpServer(registry);
// Create test web application
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddMcpServer(_server);
_app = builder.Build();
_app.MapMcpEndpoints(_server);
_app.StartAsync().Wait();
_client = _app.GetTestClient();
}
[Fact]
public async Task HttpTransport_HealthEndpoint_ReturnsOk()
{
// Act
var response = await _client.GetAsync("/health");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Healthy", content);
Assert.Contains("MCP Server", content);
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithValidRequest_ReturnsSuccess()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "test-1"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
var responseObj = JsonSerializer.Deserialize(responseJson);
Assert.NotNull(responseObj);
Assert.Equal("2.0", responseObj.RootElement.GetProperty("jsonrpc").GetString());
Assert.Equal("test-1", responseObj.RootElement.GetProperty("id").GetString());
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithEmptyBody_ReturnsBadRequest()
{
// Arrange
var content = new StringContent("", Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("error", responseJson);
Assert.Contains("-32700", responseJson); // Parse error code
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithInvalidJson_ReturnsBadRequest()
{
// Arrange
var content = new StringContent("{invalid json}", Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("error", responseJson);
Assert.Contains("Parse error", responseJson);
}
[Fact]
public async Task HttpTransport_InvokeEndpoint_WithToolsCall_ExecutesTool()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/call",
id = "test-call-1",
@params = new
{
name = "test_tool",
arguments = new
{
query = "test query"
}
}
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
var responseObj = JsonSerializer.Deserialize(responseJson);
Assert.NotNull(responseObj);
Assert.Equal("test-call-1", responseObj.RootElement.GetProperty("id").GetString());
}
[Fact]
public async Task HttpTransport_ContentTypeHeader_IsApplicationJson()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "test-header"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
}
[Fact]
public async Task HttpTransport_MultipleRequests_AllSucceed()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "multi-1"
};
var json = JsonSerializer.Serialize(request);
// Act - Send 5 requests
for (int i = 0; i < 5; i++)
{
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
[Fact]
public async Task HttpTransport_ToolsListRequest_ReturnsToolInfo()
{
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "tools/list",
id = "list-test"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("test_tool", responseJson);
}
[Fact]
public async Task HttpTransport_ServerError_Returns500WithJsonRpcError()
{
// This test would need a tool that throws an exception
// For now, we test the error handling structure
// Arrange
var request = new
{
jsonrpc = "2.0",
method = "invalid/method",
id = "error-test"
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/mcp/invoke", content);
// Assert - Should handle gracefully
var responseJson = await response.Content.ReadAsStringAsync();
Assert.Contains("jsonrpc", responseJson);
}
[Fact]
public void HttpTransport_AddMcpServer_ThrowsOnNullServices()
{
// Arrange
IServiceCollection? services = null;
var server = new McpServer(new ToolRegistry());
// Act & Assert
Assert.Throws(() =>
services!.AddMcpServer(server)
);
}
[Fact]
public void HttpTransport_AddMcpServer_ThrowsOnNullServer()
{
// Arrange
var services = new ServiceCollection();
McpServer? server = null;
// Act & Assert
Assert.Throws(() =>
services.AddMcpServer(server!)
);
}
public void Dispose()
{
_client?.Dispose();
_app?.DisposeAsync().AsTask().Wait();
}
///
/// Test tool for HTTP transport tests.
///
private class TestTool : IMcpTool
{
public string Name => "test_tool";
public string Description => "Test tool for HTTP transport tests";
public JsonDocument Schema => JsonDocument.Parse("""
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Test query parameter"
}
}
}
""");
public async Task ExecuteAsync(JsonDocument? arguments)
{
await Task.CompletedTask;
return JsonDocument.Parse("""{"result": "success", "tool": "test_tool"}""");
}
}
}