# Step 26: End-to-End Testing **Phase 7: Testing & Documentation** **Estimated Complexity:** High **Dependencies:** All implementation steps --- ## Overview End-to-end testing validates the complete request flow from HTTP client through the gateway, transport layer, microservice, and back. Tests cover all handlers, authentication, rate limiting, streaming, and failure scenarios. --- ## Goals 1. Validate complete request/response flow 2. Test all route handlers 3. Verify authentication and authorization 4. Test rate limiting behavior 5. Validate streaming and large payloads 6. Test failure scenarios and resilience --- ## Test Infrastructure ```csharp namespace StellaOps.Router.Tests; /// /// End-to-end test fixture providing gateway and microservice hosts. /// public sealed class EndToEndTestFixture : IAsyncLifetime { private IHost? _gatewayHost; private IHost? _microserviceHost; private InMemoryTransportHub? _transportHub; public HttpClient GatewayClient { get; private set; } = null!; public string GatewayBaseUrl { get; private set; } = null!; public async Task InitializeAsync() { // Shared transport hub for InMemory testing _transportHub = new InMemoryTransportHub( NullLoggerFactory.Instance.CreateLogger()); // Start gateway _gatewayHost = await CreateGatewayHostAsync(); await _gatewayHost.StartAsync(); GatewayBaseUrl = "http://localhost:5000"; GatewayClient = new HttpClient { BaseAddress = new Uri(GatewayBaseUrl) }; // Start test microservice _microserviceHost = await CreateMicroserviceHostAsync(); await _microserviceHost.StartAsync(); // Wait for connection await Task.Delay(500); } private async Task CreateGatewayHostAsync() { return Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(web => { web.UseUrls("http://localhost:5000"); web.ConfigureServices((context, services) => { services.AddSingleton(_transportHub!); services.AddStellaGateway(context.Configuration); services.AddInMemoryTransport(); // Use in-memory rate limiter services.AddSingleton(); // Mock Authority services.AddSingleton(); }); web.Configure(app => { app.UseRouting(); app.UseStellaGateway(); app.UseEndpoints(endpoints => { endpoints.MapStellaRoutes(); }); }); }) .Build(); } private async Task CreateMicroserviceHostAsync() { var host = StellaMicroserviceBuilder .Create("test-service") .ConfigureServices(services => { services.AddSingleton(_transportHub!); services.AddScoped(); }) .ConfigureTransport(t => t.Default = "InMemory") .ConfigureEndpoints(e => { e.AutoDiscover = true; e.BasePath = "/api"; }) .Build(); return (IHost)host; } public async Task DisposeAsync() { GatewayClient.Dispose(); if (_microserviceHost != null) { await _microserviceHost.StopAsync(); _microserviceHost.Dispose(); } if (_gatewayHost != null) { await _gatewayHost.StopAsync(); _gatewayHost.Dispose(); } _transportHub?.Dispose(); } } ``` --- ## Test Endpoint Handler ```csharp namespace StellaOps.Router.Tests; [StellaEndpoint(BasePath = "/test")] public class TestEndpointHandler : EndpointHandler { [StellaGet("echo")] public ResponsePayload Echo() { return Ok(new { method = Context.Method, path = Context.Path, query = Context.Query.ToDictionary(q => q.Key, q => q.Value.ToString()), headers = Context.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), claims = Context.Claims }); } [StellaPost("echo")] public async Task EchoBody() { var body = Context.ReadBodyAsString(); return Ok(new { body }); } [StellaGet("items/{id}")] public ResponsePayload GetItem([FromPath] string id) { return Ok(new { id }); } [StellaGet("slow")] public async Task SlowEndpoint(CancellationToken cancellationToken) { await Task.Delay(5000, cancellationToken); return Ok(new { completed = true }); } [StellaGet("error")] public ResponsePayload ThrowError() { throw new InvalidOperationException("Test error"); } [StellaGet("status/{code}")] public ResponsePayload ReturnStatus([FromPath] int code) { return Response().WithStatus(code).WithJson(new { statusCode = code }).Build(); } [StellaGet("protected")] [StellaAuth(RequiredClaims = new[] { "admin" })] public ResponsePayload ProtectedEndpoint() { return Ok(new { message = "Access granted" }); } [StellaPost("upload")] public ResponsePayload HandleUpload() { var size = Context.ContentLength ?? Context.RawBody?.Length ?? 0; return Ok(new { bytesReceived = size }); } [StellaGet("stream")] public ResponsePayload StreamResponse() { var data = new byte[1024 * 1024]; // 1MB Random.Shared.NextBytes(data); return Response() .WithBytes(data, "application/octet-stream") .Build(); } } ``` --- ## Basic Request/Response Tests ```csharp namespace StellaOps.Router.Tests; public class BasicRequestResponseTests : IClassFixture { private readonly EndToEndTestFixture _fixture; public BasicRequestResponseTests(EndToEndTestFixture fixture) { _fixture = fixture; } [Fact] public async Task Get_Echo_ReturnsRequestDetails() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/echo"); var content = await response.Content.ReadFromJsonAsync(); // Assert Assert.True(response.IsSuccessStatusCode); Assert.Equal("GET", content?.Method); Assert.Equal("/api/test/echo", content?.Path); } [Fact] public async Task Post_Echo_ReturnsBody() { // Arrange var client = _fixture.GatewayClient; var body = new StringContent("{\"test\": true}", Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync("/api/test/echo", body); var content = await response.Content.ReadFromJsonAsync(); // Assert Assert.True(response.IsSuccessStatusCode); Assert.Contains("test", content?.Body); } [Fact] public async Task Get_WithPathParameter_ExtractsParameter() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/items/12345"); var content = await response.Content.ReadFromJsonAsync(); // Assert Assert.True(response.IsSuccessStatusCode); Assert.Equal("12345", content?.Id); } [Fact] public async Task Get_NonExistentPath_Returns404() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/nonexistent"); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } private record EchoResponse( string Method, string Path, Dictionary Query, Dictionary Claims); private record EchoBodyResponse(string Body); private record ItemResponse(string Id); } ``` --- ## Authentication Tests ```csharp namespace StellaOps.Router.Tests; public class AuthenticationTests : IClassFixture { private readonly EndToEndTestFixture _fixture; public AuthenticationTests(EndToEndTestFixture fixture) { _fixture = fixture; } [Fact] public async Task Protected_WithoutToken_Returns401() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/protected"); // Assert Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task Protected_WithValidToken_Returns200() { // Arrange var client = _fixture.GatewayClient; var token = CreateTestToken(new Dictionary { ["admin"] = "true" }); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Act var response = await client.GetAsync("/api/test/protected"); // Assert Assert.True(response.IsSuccessStatusCode); } [Fact] public async Task Protected_WithInvalidToken_Returns401() { // Arrange var client = _fixture.GatewayClient; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "invalid-token"); // Act var response = await client.GetAsync("/api/test/protected"); // Assert Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task Protected_WithMissingClaim_Returns403() { // Arrange var client = _fixture.GatewayClient; var token = CreateTestToken(new Dictionary { ["user"] = "true" }); // No admin claim client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Act var response = await client.GetAsync("/api/test/protected"); // Assert Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } private string CreateTestToken(Dictionary claims) { // Create a test JWT (would use test key in real implementation) var handler = new JwtSecurityTokenHandler(); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test-key-for-testing-only-12345")); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claimsList = claims.Select(c => new Claim(c.Key, c.Value)).ToList(); claimsList.Add(new Claim("sub", "test-user")); var token = new JwtSecurityToken( issuer: "test", audience: "test", claims: claimsList, expires: DateTime.UtcNow.AddHours(1), signingCredentials: creds); return handler.WriteToken(token); } } ``` --- ## Rate Limiting Tests ```csharp namespace StellaOps.Router.Tests; public class RateLimitingTests : IClassFixture { private readonly EndToEndTestFixture _fixture; public RateLimitingTests(EndToEndTestFixture fixture) { _fixture = fixture; } [Fact] public async Task RateLimit_ExceedingLimit_Returns429() { // Arrange var client = _fixture.GatewayClient; var tasks = new List>(); // Act - Send 100 requests quickly for (int i = 0; i < 100; i++) { tasks.Add(client.GetAsync("/api/test/echo")); } var responses = await Task.WhenAll(tasks); // Assert - Some should be rate limited var rateLimited = responses.Count(r => r.StatusCode == HttpStatusCode.TooManyRequests); Assert.True(rateLimited > 0, "Expected some requests to be rate limited"); } [Fact] public async Task RateLimit_Headers_ArePresent() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/echo"); // Assert Assert.True(response.Headers.Contains("X-RateLimit-Limit")); Assert.True(response.Headers.Contains("X-RateLimit-Remaining")); } [Fact] public async Task RateLimit_PerUser_IsolatesUsers() { // Arrange var client1 = new HttpClient { BaseAddress = new Uri(_fixture.GatewayBaseUrl) }; var client2 = new HttpClient { BaseAddress = new Uri(_fixture.GatewayBaseUrl) }; client1.DefaultRequestHeaders.Add("X-API-Key", "user1-key"); client2.DefaultRequestHeaders.Add("X-API-Key", "user2-key"); // Act - Exhaust rate limit for user1 for (int i = 0; i < 50; i++) { await client1.GetAsync("/api/test/echo"); } // User2 should still have quota var response = await client2.GetAsync("/api/test/echo"); // Assert Assert.True(response.IsSuccessStatusCode); } } ``` --- ## Timeout and Cancellation Tests ```csharp namespace StellaOps.Router.Tests; public class TimeoutAndCancellationTests : IClassFixture { private readonly EndToEndTestFixture _fixture; public TimeoutAndCancellationTests(EndToEndTestFixture fixture) { _fixture = fixture; } [Fact] public async Task Slow_Request_TimesOut() { // Arrange var client = new HttpClient { BaseAddress = new Uri(_fixture.GatewayBaseUrl), Timeout = TimeSpan.FromSeconds(1) }; // Act & Assert await Assert.ThrowsAsync( () => client.GetAsync("/api/test/slow")); } [Fact] public async Task Cancelled_Request_PropagatesCancellation() { // Arrange var client = _fixture.GatewayClient; using var cts = new CancellationTokenSource(); // Act var task = client.GetAsync("/api/test/slow", cts.Token); await Task.Delay(100); cts.Cancel(); // Assert await Assert.ThrowsAsync(() => task); } } ``` --- ## Streaming and Large Payload Tests ```csharp namespace StellaOps.Router.Tests; public class StreamingTests : IClassFixture { private readonly EndToEndTestFixture _fixture; public StreamingTests(EndToEndTestFixture fixture) { _fixture = fixture; } [Fact] public async Task LargeUpload_Succeeds() { // Arrange var client = _fixture.GatewayClient; var data = new byte[1024 * 1024]; // 1MB Random.Shared.NextBytes(data); var content = new ByteArrayContent(data); content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); // Act var response = await client.PostAsync("/api/test/upload", content); var result = await response.Content.ReadFromJsonAsync(); // Assert Assert.True(response.IsSuccessStatusCode); Assert.Equal(data.Length, result?.BytesReceived); } [Fact] public async Task LargeDownload_Succeeds() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/stream"); var data = await response.Content.ReadAsByteArrayAsync(); // Assert Assert.True(response.IsSuccessStatusCode); Assert.Equal(1024 * 1024, data.Length); } private record UploadResponse(long BytesReceived); } ``` --- ## Error Handling Tests ```csharp namespace StellaOps.Router.Tests; public class ErrorHandlingTests : IClassFixture { private readonly EndToEndTestFixture _fixture; public ErrorHandlingTests(EndToEndTestFixture fixture) { _fixture = fixture; } [Fact] public async Task Handler_Exception_Returns500() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/error"); // Assert Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); } [Fact] public async Task Custom_StatusCode_IsPreserved() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/test/status/418"); // Assert Assert.Equal((HttpStatusCode)418, response.StatusCode); } [Fact] public async Task Error_Response_HasCorrectFormat() { // Arrange var client = _fixture.GatewayClient; // Act var response = await client.GetAsync("/api/nonexistent"); var content = await response.Content.ReadFromJsonAsync(); // Assert Assert.NotNull(content?.Error); } private record ErrorResponse(string Error); } ``` --- ## YAML Configuration ```yaml # Test configuration Router: Transports: - Type: InMemory Enabled: true RateLimiting: Enabled: true DefaultTier: free Tiers: free: RequestsPerMinute: 60 authenticated: RequestsPerMinute: 600 Authentication: Enabled: true AllowAnonymous: false TestMode: true ``` --- ## Deliverables 1. `StellaOps.Router.Tests/EndToEndTestFixture.cs` 2. `StellaOps.Router.Tests/TestEndpointHandler.cs` 3. `StellaOps.Router.Tests/BasicRequestResponseTests.cs` 4. `StellaOps.Router.Tests/AuthenticationTests.cs` 5. `StellaOps.Router.Tests/RateLimitingTests.cs` 6. `StellaOps.Router.Tests/TimeoutAndCancellationTests.cs` 7. `StellaOps.Router.Tests/StreamingTests.cs` 8. `StellaOps.Router.Tests/ErrorHandlingTests.cs` 9. Mock implementations for Authority, Rate Limiter 10. CI integration configuration --- ## Next Step Proceed to [Step 27: Reference Example & Migration Skeleton](27-Step.md) to create example implementations.