svrnty-mcp-client/tests/Svrnty.MCP.Client.Core.Tests/HttpServerConnectionTests.cs
Svrnty d936ad7856 docs: comprehensive AI coding assistant research and MCP-first implementation plan
Research conducted on modern AI coding assistants (Cursor, GitHub Copilot, Cline,
Aider, Windsurf, Replit Agent) to understand architecture patterns, context management,
code editing workflows, and tool use protocols.

Key Decision: Pivoted from building full CLI (40-50h) to validation-driven MCP-first
approach (10-15h). Build 5 core CODEX MCP tools that work with ANY coding assistant,
validate adoption over 2-4 weeks, then decide on full CLI if demand proven.

Files:
- research/ai-systems/modern-coding-assistants-architecture.md (comprehensive research)
- research/ai-systems/codex-coding-assistant-implementation-plan.md (original CLI plan, preserved)
- research/ai-systems/codex-mcp-tools-implementation-plan.md (approved MCP-first plan)
- ideas/registry.json (updated with approved MCP tools proposal)

Architech Validation: APPROVED with pivot to MCP-first approach
Human Decision: Approved (pragmatic validation-driven development)

Next: Begin Phase 1 implementation (10-15 hours, 5 core MCP tools)

🤖 Generated with CODEX Research System

Co-Authored-By: The Archivist <archivist@codex.svrnty.io>
Co-Authored-By: The Architech <architech@codex.svrnty.io>
Co-Authored-By: Mathias Beaulieu-Duncan <mat@svrnty.io>
2025-10-22 21:00:34 -04:00

448 lines
14 KiB
C#

using System.Net;
using System.Text;
using Moq;
using Moq.Protected;
using Xunit;
using OpenHarbor.MCP.Client.Core.Exceptions;
using OpenHarbor.MCP.Client.Core.Models;
using OpenHarbor.MCP.Client.Infrastructure;
namespace OpenHarbor.MCP.Client.Core.Tests;
/// <summary>
/// Unit tests for HttpServerConnection following TDD approach.
/// Tests HTTP transport implementation for MCP client connections.
/// </summary>
public class HttpServerConnectionTests
{
private readonly McpServerConfig _httpConfig;
private readonly Mock<HttpMessageHandler> _mockHttpHandler;
private readonly HttpClient _mockHttpClient;
public HttpServerConnectionTests()
{
_httpConfig = new McpServerConfig
{
Name = "test-server",
Transport = new HttpTransportConfig
{
Type = "Http",
BaseUrl = "http://localhost:5050"
},
Timeout = TimeSpan.FromSeconds(30)
};
_mockHttpHandler = new Mock<HttpMessageHandler>();
_mockHttpClient = new HttpClient(_mockHttpHandler.Object)
{
BaseAddress = new Uri("http://localhost:5050")
};
}
[Fact]
public void Constructor_WithValidConfig_CreatesConnection()
{
// Arrange & Act
var connection = new HttpServerConnection(_httpConfig);
// Assert
Assert.Equal("test-server", connection.ServerName);
Assert.False(connection.IsConnected);
}
[Fact]
public void Constructor_WithNullConfig_ThrowsArgumentNullException()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new HttpServerConnection(null!));
}
[Fact]
public async Task ConnectAsync_WithValidServer_EstablishesConnection()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
// Act
await connection.ConnectAsync();
// Assert
Assert.True(connection.IsConnected);
}
[Fact]
public async Task ConnectAsync_WhenAlreadyConnected_DoesNothing()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
await connection.ConnectAsync(); // Second call
// Assert
Assert.True(connection.IsConnected);
_mockHttpHandler.Protected().Verify(
"SendAsync",
Times.Once(), // Only called once
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
);
}
[Fact]
public async Task ConnectAsync_WithNonHttpConfig_ThrowsMcpConnectionException()
{
// Arrange
var stdioConfig = new McpServerConfig
{
Name = "stdio-server",
Transport = new StdioTransportConfig
{
Type = "Stdio",
Command = "dotnet"
}
};
var connection = new HttpServerConnection(stdioConfig, _mockHttpClient);
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.ConnectAsync());
Assert.Contains("not configured for HTTP transport", exception.Message);
}
[Fact]
public async Task ConnectAsync_WithFailedHealthCheck_ThrowsMcpConnectionException()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.ServiceUnavailable, "");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.ConnectAsync());
Assert.Contains("health check failed", exception.Message);
}
[Fact]
public async Task ConnectAsync_WithApiKey_AddsAuthorizationHeader()
{
// Arrange
var configWithApiKey = new McpServerConfig
{
Name = "secure-server",
Transport = new HttpTransportConfig
{
Type = "Http",
BaseUrl = "http://localhost:5050",
ApiKey = "test-api-key-123"
}
};
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(configWithApiKey, _mockHttpClient);
// Act
await connection.ConnectAsync();
// Assert
Assert.NotNull(_mockHttpClient.DefaultRequestHeaders.Authorization);
Assert.Equal("Bearer", _mockHttpClient.DefaultRequestHeaders.Authorization.Scheme);
Assert.Equal("test-api-key-123", _mockHttpClient.DefaultRequestHeaders.Authorization.Parameter);
}
[Fact]
public async Task DisconnectAsync_WhenConnected_SetsIsConnectedToFalse()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
await connection.DisconnectAsync();
// Assert
Assert.False(connection.IsConnected);
}
[Fact]
public async Task DisconnectAsync_WhenNotConnected_DoesNothing()
{
// Arrange
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
// Act & Assert (should not throw)
await connection.DisconnectAsync();
Assert.False(connection.IsConnected);
}
[Fact]
public async Task ListToolsAsync_WhenNotConnected_ThrowsMcpConnectionException()
{
// Arrange
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.ListToolsAsync());
Assert.Contains("not connected", exception.Message);
}
[Fact]
public async Task ListToolsAsync_WithValidResponse_ReturnsTools()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var responseJson = @"{
""jsonrpc"": ""2.0"",
""id"": ""1"",
""result"": {
""tools"": [
{
""name"": ""search_codex"",
""description"": ""Search the CODEX knowledge base""
},
{
""name"": ""get_document"",
""description"": ""Get a document by ID""
}
]
}
}";
SetupMockJsonRpcResponse(HttpStatusCode.OK, responseJson);
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
var tools = (await connection.ListToolsAsync()).ToList();
// Assert
Assert.Equal(2, tools.Count);
Assert.Equal("search_codex", tools[0].Name);
Assert.Equal("Search the CODEX knowledge base", tools[0].Description);
Assert.Equal("get_document", tools[1].Name);
}
[Fact]
public async Task ListToolsAsync_WithErrorResponse_ThrowsMcpConnectionException()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var errorJson = @"{
""jsonrpc"": ""2.0"",
""id"": ""1"",
""error"": {
""code"": -32600,
""message"": ""Invalid Request""
}
}";
SetupMockJsonRpcResponse(HttpStatusCode.OK, errorJson);
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.ListToolsAsync());
Assert.Contains("returned error", exception.Message);
}
[Fact]
public async Task CallToolAsync_WhenNotConnected_ThrowsMcpConnectionException()
{
// Arrange
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.CallToolAsync("search_codex"));
Assert.Contains("not connected", exception.Message);
}
[Fact]
public async Task CallToolAsync_WithEmptyToolName_ThrowsArgumentException()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => connection.CallToolAsync(string.Empty));
}
[Fact]
public async Task CallToolAsync_WithValidResponse_ReturnsSuccess()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var responseJson = @"{
""jsonrpc"": ""2.0"",
""id"": ""2"",
""result"": {
""content"": ""Search results: Found 5 documents""
}
}";
SetupMockJsonRpcResponse(HttpStatusCode.OK, responseJson);
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
var result = await connection.CallToolAsync(
"search_codex",
new Dictionary<string, object> { ["query"] = "test" }
);
// Assert
Assert.True(result.IsSuccess);
Assert.Equal("Search results: Found 5 documents", result.Content);
}
[Fact]
public async Task CallToolAsync_WithErrorResponse_ReturnsFailure()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var errorJson = @"{
""jsonrpc"": ""2.0"",
""id"": ""2"",
""error"": {
""code"": -32602,
""message"": ""Invalid parameters""
}
}";
SetupMockJsonRpcResponse(HttpStatusCode.OK, errorJson);
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
var result = await connection.CallToolAsync("search_codex");
// Assert
Assert.False(result.IsSuccess);
Assert.Contains("Invalid parameters", result.Error);
}
[Fact]
public async Task CallToolAsync_WithoutArguments_SendsValidRequest()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var responseJson = @"{
""jsonrpc"": ""2.0"",
""id"": ""2"",
""result"": {
""content"": ""Tool executed successfully""
}
}";
SetupMockJsonRpcResponse(HttpStatusCode.OK, responseJson);
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
var result = await connection.CallToolAsync("list_tools");
// Assert
Assert.True(result.IsSuccess);
}
[Fact]
public async Task PingAsync_WhenNotConnected_ThrowsMcpConnectionException()
{
// Arrange
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.PingAsync());
Assert.Contains("not connected", exception.Message);
}
[Fact]
public async Task PingAsync_WithHealthyServer_Succeeds()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act & Assert (should not throw)
await connection.PingAsync();
}
[Fact]
public async Task PingAsync_WithUnhealthyServer_ThrowsMcpConnectionException()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Setup ping to fail
_mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.PathAndQuery.Contains("/health")),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.ServiceUnavailable
});
// Act & Assert
var exception = await Assert.ThrowsAsync<McpConnectionException>(
() => connection.PingAsync());
Assert.Contains("Ping", exception.Message);
}
[Fact]
public async Task DisposeAsync_WhenConnected_DisconnectsAndDisposesHttpClient()
{
// Arrange
SetupMockHealthCheck(HttpStatusCode.OK, "{\"status\":\"Healthy\"}");
var connection = new HttpServerConnection(_httpConfig, _mockHttpClient);
await connection.ConnectAsync();
// Act
await connection.DisposeAsync();
// Assert
Assert.False(connection.IsConnected);
}
// Helper methods
private void SetupMockHealthCheck(HttpStatusCode statusCode, string content)
{
_mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.PathAndQuery.Contains("/health")),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(content, Encoding.UTF8, "application/json")
});
}
private void SetupMockJsonRpcResponse(HttpStatusCode statusCode, string json)
{
_mockHttpHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.PathAndQuery.Contains("/mcp/invoke")),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(json, Encoding.UTF8, "application/json")
});
}
}