- Renamed all directories: OpenHarbor.MCP.* → Svrnty.MCP.* - Updated all namespaces in 179 C# files - Renamed 20 .csproj files and 3 .sln files - Updated 193 documentation references - Updated 33 references in main CODEX codebase - Updated Codex.sln with new paths - Build verified: 0 errors Preparing for extraction to standalone repositories. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
307 lines
8.9 KiB
C#
307 lines
8.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Unit tests for HttpTransport following TDD approach.
|
|
/// Tests JSON-RPC 2.0 protocol over HTTP REST endpoints.
|
|
/// </summary>
|
|
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<JsonDocument>(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<JsonDocument>(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<ArgumentNullException>(() =>
|
|
services!.AddMcpServer(server)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void HttpTransport_AddMcpServer_ThrowsOnNullServer()
|
|
{
|
|
// Arrange
|
|
var services = new ServiceCollection();
|
|
McpServer? server = null;
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
services.AddMcpServer(server!)
|
|
);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_client?.Dispose();
|
|
_app?.DisposeAsync().AsTask().Wait();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test tool for HTTP transport tests.
|
|
/// </summary>
|
|
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<JsonDocument> ExecuteAsync(JsonDocument? arguments)
|
|
{
|
|
await Task.CompletedTask;
|
|
return JsonDocument.Parse("""{"result": "success", "tool": "test_tool"}""");
|
|
}
|
|
}
|
|
}
|