svrnty-mcp-client/tests/Svrnty.MCP.Client.Core.Tests/HttpServerConnectionTests.cs
Svrnty 97880406dc refactor: rename OpenHarbor.MCP to Svrnty.MCP across all libraries
- 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>
2025-10-22 21:04:17 -04:00

448 lines
14 KiB
C#

using System.Net;
using System.Text;
using Moq;
using Moq.Protected;
using Xunit;
using Svrnty.MCP.Client.Core.Exceptions;
using Svrnty.MCP.Client.Core.Models;
using Svrnty.MCP.Client.Infrastructure;
namespace Svrnty.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")
});
}
}