Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Audit/AuditActorMiddlewareTests.cs

88 lines
2.6 KiB
C#
Raw Permalink Normal View History

feat(api): audit context middleware + scoped impl (UDT-010 B4) 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}
2026-04-16 13:32:13 -03:00
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();
}
}