using FluentAssertions; using Microsoft.AspNetCore.Http; using SIGCM2.Api.Middleware; using Xunit; namespace SIGCM2.Api.Tests.Audit; /// UDT-010 Batch 4 — CorrelationIdMiddleware unit tests (Strict TDD). /// Validates #REQ-AUD-9 (CorrelationId in response header) and population of /// HttpContext.Items entries consumed by AuditContext. public sealed class CorrelationIdMiddlewareTests { private const string HeaderName = "X-Correlation-Id"; [Fact] public async Task Invoke_HeaderAbsent_GeneratesNewCorrelationId() { var ctx = new DefaultHttpContext(); var mw = new CorrelationIdMiddleware(_ => Task.CompletedTask); await mw.InvokeAsync(ctx); ctx.Items.TryGetValue("audit:correlationId", out var value).Should().BeTrue(); value.Should().BeOfType(); ((Guid)value!).Should().NotBe(Guid.Empty); } [Fact] public async Task Invoke_HeaderPresent_UsesClientProvidedCorrelationId() { var expected = Guid.NewGuid(); var ctx = new DefaultHttpContext(); ctx.Request.Headers[HeaderName] = expected.ToString("D"); var mw = new CorrelationIdMiddleware(_ => Task.CompletedTask); await mw.InvokeAsync(ctx); ctx.Items["audit:correlationId"].Should().Be(expected); } [Fact] public async Task Invoke_HeaderIsMalformed_GeneratesNewCorrelationId() { var ctx = new DefaultHttpContext(); ctx.Request.Headers[HeaderName] = "not-a-guid"; var mw = new CorrelationIdMiddleware(_ => Task.CompletedTask); await mw.InvokeAsync(ctx); var stored = (Guid)ctx.Items["audit:correlationId"]!; stored.Should().NotBe(Guid.Empty); } [Fact] public async Task Invoke_SetsResponseHeader_WithCorrelationId() { var ctx = new DefaultHttpContext(); ctx.Response.Body = new MemoryStream(); var mw = new CorrelationIdMiddleware(_ => Task.CompletedTask); await mw.InvokeAsync(ctx); // OnStarting callbacks fire when response starts — simulate by writing await ctx.Response.Body.FlushAsync(); // For DefaultHttpContext + MemoryStream, the OnStarting hook must fire when body writes start. // We assert the header is present after invoking a manual start-write. ctx.Response.Headers.TryGetValue(HeaderName, out var headerValue).Should().BeTrue(); Guid.TryParse(headerValue.ToString(), out _).Should().BeTrue(); } [Fact] public async Task Invoke_SetsIpAndUserAgentInItems() { var ctx = new DefaultHttpContext(); ctx.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("10.20.30.40"); ctx.Request.Headers.UserAgent = "test-agent/1.0"; var mw = new CorrelationIdMiddleware(_ => Task.CompletedTask); await mw.InvokeAsync(ctx); ctx.Items["audit:ip"].Should().Be("10.20.30.40"); ctx.Items["audit:userAgent"].Should().Be("test-agent/1.0"); } [Fact] public async Task Invoke_CallsNextDelegate() { var ctx = new DefaultHttpContext(); var nextCalled = false; var mw = new CorrelationIdMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }); await mw.InvokeAsync(ctx); nextCalled.Should().BeTrue(); } }