Files
dmolinari 0b4af4c332 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

90 lines
2.4 KiB
C#

using FluentAssertions;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using SIGCM2.Application.Audit;
using SIGCM2.Infrastructure.Audit;
using Xunit;
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
/// UDT-010 Batch 4 — AuditContext (scoped IAuditContext impl) unit tests.
/// Reads audit fields from HttpContext.Items populated by CorrelationIdMiddleware + AuditActorMiddleware.
public sealed class AuditContextTests
{
private static (AuditContext ctx, DefaultHttpContext http) Build()
{
var http = new DefaultHttpContext();
var accessor = Substitute.For<IHttpContextAccessor>();
accessor.HttpContext.Returns(http);
return (new AuditContext(accessor), http);
}
[Fact]
public void ReadsActorUserIdFromItems()
{
var (ctx, http) = Build();
http.Items["audit:actorUserId"] = 42;
ctx.ActorUserId.Should().Be(42);
}
[Fact]
public void ActorUserId_IsNull_WhenNotPresent()
{
var (ctx, _) = Build();
ctx.ActorUserId.Should().BeNull();
}
[Fact]
public void ReadsIpAndUserAgentFromItems()
{
var (ctx, http) = Build();
http.Items["audit:ip"] = "10.20.30.40";
http.Items["audit:userAgent"] = "ua/1.0";
ctx.Ip.Should().Be("10.20.30.40");
ctx.UserAgent.Should().Be("ua/1.0");
}
[Fact]
public void ReadsCorrelationIdFromItems()
{
var (ctx, http) = Build();
var id = Guid.NewGuid();
http.Items["audit:correlationId"] = id;
ctx.CorrelationId.Should().Be(id);
}
[Fact]
public void CorrelationId_IsEmpty_WhenNotPresent()
{
var (ctx, _) = Build();
ctx.CorrelationId.Should().Be(Guid.Empty);
}
[Fact]
public void AllFields_AreNull_WhenHttpContextIsNull()
{
var accessor = Substitute.For<IHttpContextAccessor>();
accessor.HttpContext.Returns((HttpContext?)null);
var ctx = new AuditContext(accessor);
ctx.ActorUserId.Should().BeNull();
ctx.ActorRoleId.Should().BeNull();
ctx.Ip.Should().BeNull();
ctx.UserAgent.Should().BeNull();
ctx.CorrelationId.Should().Be(Guid.Empty);
}
[Fact]
public void ActorRoleId_IsNull_Always_InB4()
{
// B4 middleware does not resolve rol code -> id; future batches may.
var (ctx, http) = Build();
http.Items["audit:actorUserId"] = 42;
ctx.ActorRoleId.Should().BeNull();
}
}