Wires the request-scoped audit context per design #D-2:
Middleware pipeline in Program.cs:
app.UseCors()
app.UseMiddleware<CorrelationIdMiddleware>() // PRE-AUTH
app.UseAuthentication()
app.UseMiddleware<AuditActorMiddleware>() // POST-AUTH
app.UseAuthorization()
app.MapControllers()
SIGCM2.Api/Middleware/CorrelationIdMiddleware.cs:
- Preserves client-sent X-Correlation-Id header when a valid GUID, otherwise
generates Guid.NewGuid(). Stores in HttpContext.Items (audit:correlationId).
- Captures Ip (Connection.RemoteIpAddress) + UserAgent header into Items.
- Echoes the correlation id back via response header (OnStarting + immediate
set — immediate set makes unit testing against DefaultHttpContext reliable).
SIGCM2.Api/Middleware/AuditActorMiddleware.cs:
- Reads JWT 'sub' claim from authenticated HttpContext.User, parses to int,
stores as audit:actorUserId. Anonymous / non-numeric sub leaves it unset.
SIGCM2.Infrastructure/Audit/AuditContext.cs (IAuditContext scoped impl):
- Reads Items entries via IHttpContextAccessor. Returns null / Guid.Empty
when no HttpContext is available (jobs, tests without middleware).
- ActorRoleId intentionally null for now — rol code → id resolution is
deferred; the logger may resolve it at persist time in a later batch.
DI registration (Infrastructure/DependencyInjection.cs):
- services.AddScoped<IAuditContext, AuditContext>()
Tests (Strict TDD):
- CorrelationIdMiddlewareTests (6): generates/preserves/handles-malformed
correlation id, sets response header, captures ip/ua, calls next.
- AuditActorMiddlewareTests (5): authenticated/anonymous/no-sub/non-numeric/
calls-next.
- AuditContextTests (7): reads from Items, null-http-context defaults,
ActorRoleId currently null.
Suite: 355/355 Application.Tests + 141/141 Api.Tests = 496/496 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-3/9, design#D-2, tasks#B4}
88 lines
2.6 KiB
C#
88 lines
2.6 KiB
C#
using System.Security.Claims;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Http;
|
|
using SIGCM2.Api.Middleware;
|
|
using Xunit;
|
|
|
|
namespace SIGCM2.Api.Tests.Audit;
|
|
|
|
/// UDT-010 Batch 4 — AuditActorMiddleware unit tests (Strict TDD).
|
|
/// Reads ActorUserId from the JWT "sub" claim after auth middleware populates HttpContext.User.
|
|
public sealed class AuditActorMiddlewareTests
|
|
{
|
|
[Fact]
|
|
public async Task Invoke_AuthenticatedUserWithSubClaim_SetsActorUserId()
|
|
{
|
|
var ctx = new DefaultHttpContext();
|
|
ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
|
{
|
|
new Claim("sub", "42"),
|
|
new Claim("rol", "admin"),
|
|
}, authenticationType: "Bearer"));
|
|
|
|
var mw = new AuditActorMiddleware(_ => Task.CompletedTask);
|
|
await mw.InvokeAsync(ctx);
|
|
|
|
ctx.Items["audit:actorUserId"].Should().Be(42);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Invoke_AnonymousRequest_LeavesActorUserIdNull()
|
|
{
|
|
var ctx = new DefaultHttpContext();
|
|
// User is an unauthenticated ClaimsPrincipal by default
|
|
var mw = new AuditActorMiddleware(_ => Task.CompletedTask);
|
|
|
|
await mw.InvokeAsync(ctx);
|
|
|
|
ctx.Items.TryGetValue("audit:actorUserId", out var value).Should().BeFalse();
|
|
value.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Invoke_AuthenticatedWithoutSubClaim_LeavesActorUserIdNull()
|
|
{
|
|
var ctx = new DefaultHttpContext();
|
|
ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
|
{
|
|
new Claim("name", "admin"),
|
|
}, authenticationType: "Bearer"));
|
|
|
|
var mw = new AuditActorMiddleware(_ => Task.CompletedTask);
|
|
await mw.InvokeAsync(ctx);
|
|
|
|
ctx.Items.TryGetValue("audit:actorUserId", out _).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Invoke_SubClaimIsNonNumeric_LeavesActorUserIdNull()
|
|
{
|
|
var ctx = new DefaultHttpContext();
|
|
ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
|
{
|
|
new Claim("sub", "not-an-int"),
|
|
}, authenticationType: "Bearer"));
|
|
|
|
var mw = new AuditActorMiddleware(_ => Task.CompletedTask);
|
|
await mw.InvokeAsync(ctx);
|
|
|
|
ctx.Items.TryGetValue("audit:actorUserId", out _).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Invoke_CallsNextDelegate()
|
|
{
|
|
var ctx = new DefaultHttpContext();
|
|
var nextCalled = false;
|
|
var mw = new AuditActorMiddleware(_ =>
|
|
{
|
|
nextCalled = true;
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
await mw.InvokeAsync(ctx);
|
|
|
|
nextCalled.Should().BeTrue();
|
|
}
|
|
}
|