using Xunit; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenHarbor.MCP.AspNetCore.Extensions; using System; using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace OpenHarbor.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"}"""); } } }