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;
///
/// Unit tests for HttpServerConnection following TDD approach.
/// Tests HTTP transport implementation for MCP client connections.
///
public class HttpServerConnectionTests
{
private readonly McpServerConfig _httpConfig;
private readonly Mock _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();
_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(() => 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(),
ItExpr.IsAny()
);
}
[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(
() => 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(
() => 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(
() => 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(
() => 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(
() => 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(
() => 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 { ["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(
() => 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>(
"SendAsync",
ItExpr.Is(req => req.RequestUri!.PathAndQuery.Contains("/health")),
ItExpr.IsAny()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.ServiceUnavailable
});
// Act & Assert
var exception = await Assert.ThrowsAsync(
() => 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>(
"SendAsync",
ItExpr.Is(req => req.RequestUri!.PathAndQuery.Contains("/health")),
ItExpr.IsAny()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(content, Encoding.UTF8, "application/json")
});
}
private void SetupMockJsonRpcResponse(HttpStatusCode statusCode, string json)
{
_mockHttpHandler.Protected()
.Setup>(
"SendAsync",
ItExpr.Is(req => req.RequestUri!.PathAndQuery.Contains("/mcp/invoke")),
ItExpr.IsAny()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = statusCode,
Content = new StringContent(json, Encoding.UTF8, "application/json")
});
}
}