using Xunit; using Moq; using Moq.Protected; using System.Net; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using OpenHarbor.MCP.Core; using CodexMcpServer.Tools; namespace CodexMcpServer.Tests; /// /// Unit tests for ListDocumentsTool following TDD approach. /// Tests integration with CODEX API /api/documents endpoint with pagination. /// public class ListDocumentsToolTests { [Fact] public void ListDocumentsTool_ShouldHaveCorrectName() { // Arrange var httpClient = new HttpClient(); var tool = new ListDocumentsTool(httpClient); // Act var name = tool.Name; // Assert Assert.Equal("list_documents", name); } [Fact] public void ListDocumentsTool_ShouldHaveDescription() { // Arrange var httpClient = new HttpClient(); var tool = new ListDocumentsTool(httpClient); // Act var description = tool.Description; // Assert Assert.NotNull(description); Assert.NotEmpty(description); Assert.Contains("list", description.ToLower()); } [Fact] public void ListDocumentsTool_ShouldHaveValidSchema() { // Arrange var httpClient = new HttpClient(); var tool = new ListDocumentsTool(httpClient); // Act var schema = tool.Schema; // Assert Assert.NotNull(schema); var root = schema.RootElement; Assert.Equal(JsonValueKind.Object, root.ValueKind); // Schema should have optional pagination properties Assert.True(root.TryGetProperty("properties", out var properties)); // All properties should be optional (no required array or empty required) } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithNoArguments_ShouldCallCodexApi() { // Arrange var mockResponse = new { documents = new[] { new { id = "doc1", title = "Document 1" }, new { id = "doc2", title = "Document 2" } }, total = 2, page = 1, pageSize = 20 }; var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.Is(req => req.Method == HttpMethod.Get && req.RequestUri.ToString().Contains("/api/documents") ), ItExpr.IsAny() ) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(JsonSerializer.Serialize(mockResponse)) }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); // Act var result = await tool.ExecuteAsync(null); // Assert Assert.NotNull(result); var root = result.RootElement; Assert.True(root.TryGetProperty("documents", out _)); } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithPaginationParams_ShouldIncludeInRequest() { // Arrange string capturedUri = null; var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => { capturedUri = req.RequestUri?.ToString(); return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("""{"documents": [], "total": 0}""") }; }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); var arguments = JsonDocument.Parse("""{"page": 2, "pageSize": 10}"""); // Act await tool.ExecuteAsync(arguments); // Assert Assert.NotNull(capturedUri); Assert.Contains("/api/documents", capturedUri); Assert.Contains("page=2", capturedUri); Assert.Contains("pageSize=10", capturedUri); } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithHttpError_ShouldReturnErrorResponse() { // Arrange var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.InternalServerError, Content = new StringContent("Internal server error") }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); // Act var result = await tool.ExecuteAsync(null); // Assert Assert.NotNull(result); var root = result.RootElement; Assert.True(root.TryGetProperty("error", out _)); } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithInvalidJson_ShouldReturnError() { // Arrange var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("invalid json{{{") }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); // Act var result = await tool.ExecuteAsync(null); // Assert Assert.NotNull(result); var root = result.RootElement; Assert.True(root.TryGetProperty("error", out _)); } [Fact] public void ListDocumentsTool_ShouldImplementIMcpTool() { // Arrange var httpClient = new HttpClient(); // Act var tool = new ListDocumentsTool(httpClient); // Assert Assert.IsAssignableFrom(tool); } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithLimit_ShouldIncludeInRequest() { // Arrange string capturedUri = null; var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => { capturedUri = req.RequestUri?.ToString(); return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("""{"documents": [], "total": 0}""") }; }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); var arguments = JsonDocument.Parse("""{"limit": 5}"""); // Act await tool.ExecuteAsync(arguments); // Assert Assert.NotNull(capturedUri); Assert.Contains("limit=5", capturedUri); } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithOffset_ShouldIncludeInRequest() { // Arrange string capturedUri = null; var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => { capturedUri = req.RequestUri?.ToString(); return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("""{"documents": [], "total": 0}""") }; }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); var arguments = JsonDocument.Parse("""{"offset": 10}"""); // Act await tool.ExecuteAsync(arguments); // Assert Assert.NotNull(capturedUri); Assert.Contains("offset=10", capturedUri); } [Fact] public async Task ListDocumentsTool_ExecuteAsync_WithMultipleParams_ShouldIncludeAllInRequest() { // Arrange string capturedUri = null; var mockHttpMessageHandler = new Mock(); mockHttpMessageHandler .Protected() .Setup>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => { capturedUri = req.RequestUri?.ToString(); return new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("""{"documents": [], "total": 0}""") }; }); var httpClient = new HttpClient(mockHttpMessageHandler.Object) { BaseAddress = new System.Uri("http://localhost:5050") }; var tool = new ListDocumentsTool(httpClient); var arguments = JsonDocument.Parse("""{"page": 3, "pageSize": 15, "limit": 100, "offset": 30}"""); // Act await tool.ExecuteAsync(arguments); // Assert Assert.NotNull(capturedUri); Assert.Contains("page=3", capturedUri); Assert.Contains("pageSize=15", capturedUri); Assert.Contains("limit=100", capturedUri); Assert.Contains("offset=30", capturedUri); } }