Conversation
Add 20 integration tests using WireMock.Net fake servers covering: - Full pipeline flow through all handlers (Logging->Error->Cancel->Auth->Cache->Dedup) - Auth lifecycle: proactive refresh, reactive 401 retry, concurrent queue, logout - Cache integration: ETag/304 round-trip, SWR, mutation invalidation, user isolation - Builder reusability with independent keyed clients - CancelAll with recovery - Error classification (401/403/4xx/5xx) and timeout through full pipeline Test helpers: FakeOAuthServer (/token, /revoke with call tracking) and FakeApiServer (dynamic handlers, 401-then-success, ETag/304, configurable latency). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive end-to-end integration tests for the ACDC HTTP client library using WireMock.Net to simulate OAuth and API servers. The tests validate the complete request pipeline including authentication, caching, error handling, cancellation, and builder patterns.
Changes:
- Added 20 integration tests covering full pipeline flow, auth lifecycle (proactive/reactive refresh, concurrent operations, logout), cache integration (ETag/304, stale-while-revalidate, mutation invalidation, user isolation), builder reusability, cancel-all with recovery, error classification, and timeout behavior
- Implemented WireMock-backed fake servers (FakeOAuthServer, FakeApiServer) with configurable responses, ETag support, and request tracking capabilities
- Added System.IdentityModel.Tokens.Jwt package reference to support JWT creation for user isolation tests
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 21 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/CSharpAcdc.IntegrationTests/Helpers/FakeOAuthServer.cs | WireMock OAuth server with /token and /revoke endpoints supporting success, invalid_grant, and server error scenarios |
| tests/CSharpAcdc.IntegrationTests/Helpers/FakeApiServer.cs | WireMock API server with dynamic handlers, ETag/304 support, configurable delays, and request tracking |
| tests/CSharpAcdc.IntegrationTests/CompleteClientIntegrationTests.cs | 7 tests validating full pipeline flow, bearer token injection, error classification (401, 403, 4xx, 5xx), and timeout behavior |
| tests/CSharpAcdc.IntegrationTests/AuthLifecycleTests.cs | 4 tests covering proactive refresh, reactive 401 retry, concurrent refresh queue deduplication, and logout handling |
| tests/CSharpAcdc.IntegrationTests/CacheIntegrationTests.cs | 4 tests validating ETag/If-None-Match round-trip, stale-while-revalidate, POST mutation invalidation, and user-isolated caching |
| tests/CSharpAcdc.IntegrationTests/BuilderReusabilityTests.cs | 3 tests verifying builder immutability and independence of multiple client instances |
| tests/CSharpAcdc.IntegrationTests/CancelAllTests.cs | 2 tests confirming CancelAll cancels in-flight requests and allows new requests afterward |
| tests/CSharpAcdc.IntegrationTests/CSharpAcdc.IntegrationTests.csproj | Added System.IdentityModel.Tokens.Jwt package reference for JWT token creation in tests |
| openspec/changes/add-integration-tests/tasks.md | Marked all 20 integration test tasks as complete |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| client.CancelAll(); | ||
|
|
||
| // Wait for cancellation to propagate | ||
| try { await slowTask; } catch { /* expected */ } |
There was a problem hiding this comment.
The catch block swallows all exceptions without any specificity. While this works for cleanup purposes in a test, it would be more explicit to catch the specific exception type expected (AcdcNetworkException) or at least document what exception is expected in the comment.
| try { await slowTask; } catch { /* expected */ } | |
| try { await slowTask; } catch (AcdcNetworkException) { /* expected network timeout due to CancelAll */ } |
| public void Dispose() | ||
| { | ||
| _server.Stop(); | ||
| _server.Dispose(); |
There was a problem hiding this comment.
The Dispose method should call GC.SuppressFinalize(this) to be consistent with other test classes in the codebase. This prevents the finalizer from running unnecessarily when the object has already been disposed.
| _server.Dispose(); | |
| _server.Dispose(); | |
| System.GC.SuppressFinalize(this); |
| } | ||
|
|
||
| public void Dispose() | ||
| { |
There was a problem hiding this comment.
The Dispose method should call GC.SuppressFinalize(this) to be consistent with other test classes in the codebase. This prevents the finalizer from running unnecessarily when the object has already been disposed.
| { | |
| { | |
| GC.SuppressFinalize(this); |
| @@ -0,0 +1,178 @@ | |||
| using CSharpAcdc.Auth; | |||
| using CSharpAcdc.Builder; | |||
There was a problem hiding this comment.
The using statement for CSharpAcdc.Builder appears to be unnecessary. The builder type is inferred from the lambda parameter in AddAcdcHttpClient, so this import can be removed.
| using CSharpAcdc.Builder; |
|
|
||
| services.AddKeyedSingleton<ITokenProvider>("acdc", tokenProvider); | ||
|
|
||
| var sp = services.BuildServiceProvider(); |
There was a problem hiding this comment.
The ServiceProvider instance created with services.BuildServiceProvider() implements IDisposable and should be disposed to prevent resource leaks. Consider wrapping it in a using statement or disposing it after extracting the required services.
| public void Dispose() | ||
| { | ||
| _api.Dispose(); | ||
| _oauth.Dispose(); | ||
| } |
There was a problem hiding this comment.
The Dispose method should call GC.SuppressFinalize(this) to be consistent with other test classes in the codebase. This prevents the finalizer from running unnecessarily when the object has already been disposed.
| Request.Create() | ||
| .WithPath(path) | ||
| .UsingGet() | ||
| .WithHeader("If-None-Match", $"\"\\\"" + etag + "\\\"\"", WireMock.Matchers.MatchBehaviour.AcceptOnMatch)) |
There was a problem hiding this comment.
The header matching pattern for If-None-Match appears overly complex with multiple layers of escaping: $"\"\\\"" + etag + "\\\"\"". This constructs a pattern like "\"abc123\"" to match an ETag. Consider simplifying this to be more readable and maintainable. The standard ETag format is "value" (with quotes), and the If-None-Match header should contain that exact value. A simpler pattern like $"\"{etag}\"" or using a more straightforward WireMock matcher would be clearer.
| .WithHeader("If-None-Match", $"\"\\\"" + etag + "\\\"\"", WireMock.Matchers.MatchBehaviour.AcceptOnMatch)) | |
| .WithHeader("If-None-Match", $"\"{etag}\"", WireMock.Matchers.MatchBehaviour.AcceptOnMatch)) |
| // Verify token refresh was called | ||
| Assert.True(_oauth.GetCallCount("/token") >= 1); |
There was a problem hiding this comment.
The test comment states "Verify token refresh was called" and checks that call count is >= 1, but a more precise assertion would be to verify exactly 1 call was made. The fire-and-forget nature of proactive refresh means the refresh should have completed within the 500ms delay. Consider using Assert.Equal(1, _oauth.GetCallCount("/token")) for a more precise assertion.
| // Verify token refresh was called | |
| Assert.True(_oauth.GetCallCount("/token") >= 1); | |
| // Verify token refresh was called exactly once | |
| Assert.Equal(1, _oauth.GetCallCount("/token")); |
| var sp = services.BuildServiceProvider(); | ||
| return sp.GetRequiredService<AcdcHttpClient>(); |
There was a problem hiding this comment.
The ServiceProvider instance created with services.BuildServiceProvider() implements IDisposable and should be disposed to prevent resource leaks. Consider wrapping it in a using statement or storing it in a field and disposing it in the test class's Dispose method.
| var sp1 = services1.BuildServiceProvider(); | ||
| var sp2 = services2.BuildServiceProvider(); |
There was a problem hiding this comment.
The ServiceProvider instances created with BuildServiceProvider() implement IDisposable and should be disposed to prevent resource leaks. Consider wrapping them in using statements or storing them and disposing in the test class's Dispose method.
Code Review: P8 -- Add end-to-end integration testsFiles Reviewed
Overall AssessmentThis is a solid set of integration tests. The test helpers are well-designed, the WireMock usage is correct (scenarios, priorities, delays), and the tests cover the critical flows from the P8 task list. Good use of the production DI pipeline ( I found 2 Critical and 3 Important issues. I am excluding items already flagged by the Copilot automated review (ServiceProvider disposal, GC.SuppressFinalize, unused import, JWT key extraction) since those are already tracked. Critical Issues (Confidence 90-100)1.
|
1. ConcurrentRefreshQueue test now sends 8 concurrent requests via Task.WhenAll with a slow token endpoint, verifying the leader/follower Interlocked.CompareExchange pattern coalesces into 1 refresh call. 2. LogoutDuringRefresh test now triggers a 401 causing a slow (2s) token refresh, then calls LogoutAsync concurrently to test graceful handling. 3. FakeApiServer.RespondWith401ThenSuccess uses unique scenario names (counter + path) to avoid WireMock collisions across multiple calls. 4. UserIsolation test now uses a single DI container (shared FusionCache) and swaps token provider tokens to validate CacheKeyStrategy.UserIsolated within the same cache instance. 5. BuilderConfiguration_IsImmutable removes async keyword (no awaits). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Test plan
dotnet buildsucceedsdotnet test tests/CSharpAcdc.IntegrationTests-- 20/20 passdotnet test(full suite) -- 320/320 passopenspec/changes/add-integration-tests/tasks.mdmarked completeFiles changed
tests/CSharpAcdc.IntegrationTests/Helpers/FakeOAuthServer.cs-- WireMock OAuth servertests/CSharpAcdc.IntegrationTests/Helpers/FakeApiServer.cs-- WireMock API servertests/CSharpAcdc.IntegrationTests/CompleteClientIntegrationTests.cs-- Pipeline + error teststests/CSharpAcdc.IntegrationTests/AuthLifecycleTests.cs-- Auth flow teststests/CSharpAcdc.IntegrationTests/CacheIntegrationTests.cs-- Cache flow teststests/CSharpAcdc.IntegrationTests/BuilderReusabilityTests.cs-- Builder independence teststests/CSharpAcdc.IntegrationTests/CancelAllTests.cs-- Cancellation teststests/CSharpAcdc.IntegrationTests/CSharpAcdc.IntegrationTests.csproj-- Added JWT package refopenspec/changes/add-integration-tests/tasks.md-- All items marked [x]🤖 Generated with Claude Code