Create ChatAgent.Api.Tests and ChatAgent.Client.Tests projects with xUnit and Moq. Test HealthController (200 + valid response), ChatController (SSE streaming with mocked upstream, error handling), and ChatApiClient (delta parsing, error events, health endpoint). 6 tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
4.9 KiB
C#
159 lines
4.9 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using ChatAgent.Shared.Models;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace ChatAgent.Api.Tests;
|
|
|
|
public class ChatControllerTests : IClassFixture<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly WebApplicationFactory<Program> _factory;
|
|
|
|
public ChatControllerTests(WebApplicationFactory<Program> factory)
|
|
{
|
|
_factory = factory;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostChat_StreamsTextDeltas_AndDone()
|
|
{
|
|
// Arrange: mock the upstream Responses API with canned SSE events
|
|
var sseContent = BuildSSE(
|
|
("response.created", null),
|
|
("response.output_text.delta", "Hello"),
|
|
("response.output_text.delta", " world"),
|
|
("response.completed", null)
|
|
);
|
|
|
|
var client = CreateClientWithMockedUpstream(sseContent);
|
|
|
|
var request = new ChatRequest
|
|
{
|
|
Messages = new List<ChatMessage>
|
|
{
|
|
new() { Role = "user", Content = "Hi" }
|
|
}
|
|
};
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/api/chat", request);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.Equal("text/event-stream", response.Content.Headers.ContentType?.MediaType);
|
|
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
var events = ParseSSEData(body);
|
|
|
|
// Assert: should contain text deltas and [DONE]
|
|
Assert.Contains(events, e => e.Contains("\"text\":\"Hello\""));
|
|
Assert.Contains(events, e => e.Contains("\"text\":\" world\""));
|
|
Assert.Contains(events, e => e == "[DONE]");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostChat_HandlesUpstreamError_ReturnsErrorEvent()
|
|
{
|
|
// Arrange: upstream returns 500
|
|
var mockHandler = new MockHttpMessageHandler(
|
|
new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
|
{
|
|
Content = new StringContent("Internal Server Error")
|
|
});
|
|
|
|
var client = CreateClientWithHandler(mockHandler);
|
|
|
|
var request = new ChatRequest
|
|
{
|
|
Messages = new List<ChatMessage>
|
|
{
|
|
new() { Role = "user", Content = "Hi" }
|
|
}
|
|
};
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync("/api/chat", request);
|
|
var body = await response.Content.ReadAsStringAsync();
|
|
var events = ParseSSEData(body);
|
|
|
|
// Assert: should contain error and [DONE]
|
|
Assert.Contains(events, e => e.Contains("\"error\""));
|
|
Assert.Contains(events, e => e == "[DONE]");
|
|
}
|
|
|
|
private HttpClient CreateClientWithMockedUpstream(string sseContent)
|
|
{
|
|
var mockHandler = new MockHttpMessageHandler(
|
|
new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(sseContent, Encoding.UTF8, "text/event-stream")
|
|
});
|
|
|
|
return CreateClientWithHandler(mockHandler);
|
|
}
|
|
|
|
private HttpClient CreateClientWithHandler(MockHttpMessageHandler handler)
|
|
{
|
|
return _factory.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.ConfigureServices(services =>
|
|
{
|
|
// Remove existing IHttpClientFactory registrations for "ResponsesApi"
|
|
// and replace with our mock
|
|
services.AddHttpClient("ResponsesApi")
|
|
.ConfigurePrimaryHttpMessageHandler(() => handler);
|
|
});
|
|
}).CreateClient();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a fake SSE stream mimicking the Responses API format.
|
|
/// </summary>
|
|
private static string BuildSSE(params (string type, string? delta)[] events)
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var (type, delta) in events)
|
|
{
|
|
var data = delta != null
|
|
? $"{{\"type\":\"{type}\",\"delta\":\"{delta}\"}}"
|
|
: $"{{\"type\":\"{type}\"}}";
|
|
sb.AppendLine($"event: {type}");
|
|
sb.AppendLine($"data: {data}");
|
|
sb.AppendLine();
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the data payloads from SSE-formatted text.
|
|
/// </summary>
|
|
private static List<string> ParseSSEData(string sseText)
|
|
{
|
|
return sseText.Split('\n')
|
|
.Where(line => line.StartsWith("data: "))
|
|
.Select(line => line.Substring(6).Trim())
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple mock HttpMessageHandler that returns a canned response.
|
|
/// </summary>
|
|
public class MockHttpMessageHandler : HttpMessageHandler
|
|
{
|
|
private readonly HttpResponseMessage _response;
|
|
|
|
public MockHttpMessageHandler(HttpResponseMessage response)
|
|
{
|
|
_response = response;
|
|
}
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(_response);
|
|
}
|
|
}
|