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> { private readonly WebApplicationFactory _factory; public ChatControllerTests(WebApplicationFactory 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 { 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 { 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(); } /// /// Builds a fake SSE stream mimicking the Responses API format. /// 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(); } /// /// Extracts the data payloads from SSE-formatted text. /// private static List ParseSSEData(string sseText) { return sseText.Split('\n') .Where(line => line.StartsWith("data: ")) .Select(line => line.Substring(6).Trim()) .ToList(); } } /// /// Simple mock HttpMessageHandler that returns a canned response. /// public class MockHttpMessageHandler : HttpMessageHandler { private readonly HttpResponseMessage _response; public MockHttpMessageHandler(HttpResponseMessage response) { _response = response; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(_response); } }