feat(api): GET /audit/events + /health/audit (UDT-010 B10)

AuditController:
- GET /api/v1/audit/events?actorUserId&targetType&targetId&from&to&cursor&limit
- Protected by [RequirePermission("administracion:auditoria:ver")] — reuses
  the existing permission (V005/V006 seed assigns it to admin).
- 400 on limit out of [1,100] or from > to.
- Cursor-based DESC pagination via AuditEventRepository.QueryAsync.

AuditHealthCheck (IHealthCheck):
- Validates SYSTEM_VERSIONING ON on Usuario/Rol/Permiso/RolPermiso.
- Validates partition boundaries exist for next 3 months (both AuditEvent and
  SecurityEvent functions).
- Reports last audit event age (lenient 24h to accommodate dev/test quiet envs).
- Validates HISTORY_RETENTION_PERIOD == 10 YEARS on all 4 tables.
  Key fix during impl: sys.tables.history_retention_period is stored in UNITS
  (1=INFINITE, 3=DAY, 4=WEEK, 5=MONTH, 6=YEAR), NOT seconds. Assertion: period=10
  AND unit=6 (10 YEARS).
- Mapped at /health/audit via app.MapHealthChecks with tag 'audit'.

Tests (Strict TDD, integration against SIGCM2_Test):
- AuditControllerTests (5): without-auth 401, without-permission 403 (cajero),
  admin with filter returns events, invalid limit 400, from>to 400.
- AuditHealthCheckTests (1): returns Healthy with V010 applied.

Suite: 378/378 Application.Tests + 147/147 Api.Tests = 525/525 passing.

Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-7/8, design, tasks#B10}
This commit is contained in:
2026-04-16 17:05:40 -03:00
parent b619c05762
commit 2bb90118ab
5 changed files with 356 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
using System.Net;
using FluentAssertions;
using SIGCM2.TestSupport;
using Xunit;
namespace SIGCM2.Api.Tests.Audit;
/// UDT-010 Batch 10 — /health/audit integration smoke.
[Collection("ApiIntegration")]
public sealed class AuditHealthCheckTests : IClassFixture<TestWebAppFactory>
{
private readonly TestWebAppFactory _factory;
public AuditHealthCheckTests(TestWebAppFactory factory)
{
_factory = factory;
}
[Fact]
public async Task HealthAudit_WithInfraApplied_ReturnsHealthy()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/health/audit");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadAsStringAsync();
body.Should().Contain("Healthy");
}
}