101 lines
3.3 KiB
C#
101 lines
3.3 KiB
C#
|
|
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>();
|
||
|
|
((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();
|
||
|
|
}
|
||
|
|
}
|