test: add xUnit test coverage for API controllers and client services

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>
This commit is contained in:
local
2026-04-04 02:21:10 +01:00
parent 00e7df2802
commit 17a5a58e73
13 changed files with 632 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-04

View File

@@ -0,0 +1,45 @@
## Context
No test projects exist. The solution has 3 projects (Client, Api, Shared) under `src/`. Tests need to cover the API controllers and the client's ChatApiClient service, both of which involve HTTP and SSE streaming.
## Goals / Non-Goals
**Goals:**
- Establish test infrastructure (xUnit + Moq)
- Test API controllers using WebApplicationFactory (integration-style)
- Test ChatApiClient using a mock HttpMessageHandler (unit-style)
- All tests runnable via `dotnet test` from solution root
**Non-Goals:**
- Blazor component tests (bUnit) — Chat.razor is UI-heavy, defer to a future phase
- End-to-end browser tests (Playwright/Selenium)
- Testing the upstream Responses API itself
## Decisions
### Decision 1: Test framework — xUnit + Moq
xUnit is the .NET standard. Moq for mocking HttpMessageHandler so we can control HTTP responses in tests without hitting real servers.
### Decision 2: Test project layout
```
tests/
├── ChatAgent.Api.Tests/ → references Api project
└── ChatAgent.Client.Tests/ → references Client project + Shared
```
Both added to the `ChatAgent.sln` solution file under a `tests` solution folder.
### Decision 3: API tests use WebApplicationFactory
`Microsoft.AspNetCore.Mvc.Testing` provides `WebApplicationFactory<Program>` for integration-style tests. For ChatController, we inject a mock `IHttpClientFactory` that returns a handler with canned SSE responses — no real Responses API needed.
### Decision 4: Client tests mock HttpMessageHandler
ChatApiClient takes an HttpClient. Tests create an HttpClient with a custom `DelegatingHandler` that returns canned SSE response streams. This tests the SSE parsing logic in isolation.
## Risks / Trade-offs
- [WebApplicationFactory requires Api's Program to be accessible] → Add `InternalsVisibleTo` or use the `public partial class Program {}` trick in Api's Program.cs
- [SSE stream mocking is verbose] → Create a small helper method that builds SSE response content from a list of events

View File

@@ -0,0 +1,26 @@
## Why
The project has zero test coverage. There are two controllers, a typed HttpClient service, and shared models — all untested. Adding tests now establishes the pattern before the codebase grows, and catches regressions as new phases are added.
## What Changes
- Create an xUnit test project for the API (`ChatAgent.Api.Tests`)
- Create an xUnit test project for the Client services (`ChatAgent.Client.Tests`)
- Add tests for HealthController (GET /api/health)
- Add tests for ChatController (POST /api/chat SSE streaming proxy)
- Add tests for ChatApiClient (GetHealthAsync, SendChatStreamingAsync)
- Add both test projects to the solution
## Capabilities
### New Capabilities
- `test-infrastructure`: Test project setup, test framework choices, and shared test utilities
### Modified Capabilities
<!-- None — adding tests doesn't change existing specs -->
## Impact
- `tests/ChatAgent.Api.Tests/`: New xUnit test project
- `tests/ChatAgent.Client.Tests/`: New xUnit test project
- `ChatAgent.sln`: Add test projects

View File

@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: API test project exists
An xUnit test project SHALL exist at `tests/ChatAgent.Api.Tests/` targeting the API project, with xUnit, Moq, and Microsoft.AspNetCore.Mvc.Testing as dependencies.
#### Scenario: API tests run
- **WHEN** `dotnet test` is run from the solution root
- **THEN** API tests are discovered and executed
### Requirement: Client test project exists
An xUnit test project SHALL exist at `tests/ChatAgent.Client.Tests/` targeting the Client services, with xUnit and Moq as dependencies.
#### Scenario: Client tests run
- **WHEN** `dotnet test` is run from the solution root
- **THEN** Client service tests are discovered and executed
### Requirement: HealthController test coverage
Tests SHALL verify that GET /api/health returns HTTP 200 with a valid HealthResponse containing a non-empty Status and a recent Timestamp.
#### Scenario: Health endpoint returns 200
- **WHEN** a GET request is sent to /api/health
- **THEN** the response status is 200 and the body contains `status: "healthy"` and a UTC timestamp
### Requirement: ChatController test coverage
Tests SHALL verify that POST /api/chat returns a streaming SSE response containing text deltas and a [DONE] terminator. Tests SHALL mock the upstream Responses API.
#### Scenario: Chat streams text deltas
- **WHEN** a POST is sent to /api/chat with a valid ChatRequest
- **THEN** the response is `text/event-stream` containing `data: {"text":"..."}` events followed by `data: [DONE]`
#### Scenario: Chat handles upstream error
- **WHEN** the Responses API is unreachable or returns an error
- **THEN** the response contains a `data: {"error":"..."}` event followed by `data: [DONE]`
### Requirement: ChatApiClient test coverage
Tests SHALL verify that SendChatStreamingAsync correctly parses SSE events from the backend into text deltas, handles [DONE], and throws on error events.
#### Scenario: Client parses text deltas
- **WHEN** the backend returns SSE events with text deltas
- **THEN** SendChatStreamingAsync yields each text fragment in order
#### Scenario: Client handles error event
- **WHEN** the backend returns an error SSE event
- **THEN** SendChatStreamingAsync throws HttpRequestException with the error message

View File

@@ -0,0 +1,23 @@
## 1. Test Project Setup
- [x] 1.1 Create xUnit test project at tests/ChatAgent.Api.Tests with xUnit, Moq, Microsoft.AspNetCore.Mvc.Testing
- [x] 1.2 Create xUnit test project at tests/ChatAgent.Client.Tests with xUnit, Moq
- [x] 1.3 Add both test projects to ChatAgent.sln under a tests solution folder
- [x] 1.4 Make Api's Program class accessible to tests (public partial class Program)
## 2. API Tests
- [x] 2.1 Test HealthController: GET /api/health returns 200 with valid HealthResponse
- [x] 2.2 Test ChatController: POST /api/chat streams SSE text deltas from mocked upstream
- [x] 2.3 Test ChatController: POST /api/chat handles upstream error gracefully
## 3. Client Service Tests
- [x] 3.1 Create SSE response helper for building mock SSE streams
- [x] 3.2 Test ChatApiClient.SendChatStreamingAsync: parses text deltas in order
- [x] 3.3 Test ChatApiClient.SendChatStreamingAsync: throws on error event
- [x] 3.4 Test ChatApiClient.GetHealthAsync: returns HealthResponse on success
## 4. Verify
- [x] 4.1 Run dotnet test from solution root — all tests pass

View File

@@ -0,0 +1,60 @@
## Purpose
Define test project setup, framework choices, and required test coverage for the ChatAgent solution.
## Requirements
### Requirement: API test project exists
An xUnit test project SHALL exist at `tests/ChatAgent.Api.Tests/` targeting the API project, with xUnit, Moq, and Microsoft.AspNetCore.Mvc.Testing as dependencies.
#### Scenario: API tests run
- **WHEN** `dotnet test` is run from the solution root
- **THEN** API tests are discovered and executed
### Requirement: Client test project exists
An xUnit test project SHALL exist at `tests/ChatAgent.Client.Tests/` targeting the Client services, with xUnit and Moq as dependencies.
#### Scenario: Client tests run
- **WHEN** `dotnet test` is run from the solution root
- **THEN** Client service tests are discovered and executed
### Requirement: HealthController test coverage
Tests SHALL verify that GET /api/health returns HTTP 200 with a valid HealthResponse containing a non-empty Status and a recent Timestamp.
#### Scenario: Health endpoint returns 200
- **WHEN** a GET request is sent to /api/health
- **THEN** the response status is 200 and the body contains `status: "healthy"` and a UTC timestamp
### Requirement: ChatController test coverage
Tests SHALL verify that POST /api/chat returns a streaming SSE response containing text deltas and a [DONE] terminator. Tests SHALL mock the upstream Responses API.
#### Scenario: Chat streams text deltas
- **WHEN** a POST is sent to /api/chat with a valid ChatRequest
- **THEN** the response is `text/event-stream` containing `data: {"text":"..."}` events followed by `data: [DONE]`
#### Scenario: Chat handles upstream error
- **WHEN** the Responses API is unreachable or returns an error
- **THEN** the response contains a `data: {"error":"..."}` event followed by `data: [DONE]`
### Requirement: ChatApiClient test coverage
Tests SHALL verify that SendChatStreamingAsync correctly parses SSE events from the backend into text deltas, handles [DONE], and throws on error events.
#### Scenario: Client parses text deltas
- **WHEN** the backend returns SSE events with text deltas
- **THEN** SendChatStreamingAsync yields each text fragment in order
#### Scenario: Client handles error event
- **WHEN** the backend returns an error SSE event
- **THEN** SendChatStreamingAsync throws HttpRequestException with the error message