Fix all test compilation errors caused by T400.10/T400.20/T400.30: - Handler constructors: add TimeProvider.System as last argument - Domain mutator calls: add DateTime.UtcNow as explicit 'now' argument - AuditLogger/SecurityEventLogger Build() helpers: add TimeProvider.System - JwtService test constructors: add TimeProvider.System Cat2 coverage already present in TimeProviderArgentinaExtensionsTests.cs: FakeTimeProvider proves GetArgentinaToday() returns ART civil date, not UTC.
155 lines
6.0 KiB
C#
155 lines
6.0 KiB
C#
using FluentAssertions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using SIGCM2.Application.Audit;
|
|
using SIGCM2.Domain.Exceptions;
|
|
using SIGCM2.Infrastructure.Audit;
|
|
using Xunit;
|
|
|
|
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
|
|
|
/// UDT-010 Batch 6 — AuditLogger unit tests (Strict TDD).
|
|
/// Covers #REQ-AUD-3/4/5: enriches from IAuditContext, fail-closed on missing actor,
|
|
/// sanitizes metadata via JsonSanitizer + AuditOptions.
|
|
public sealed class AuditLoggerTests
|
|
{
|
|
private static AuditLogger Build(
|
|
IAuditContext? context = null,
|
|
IAuditEventRepository? repo = null,
|
|
AuditOptions? options = null)
|
|
{
|
|
context ??= Substitute.For<IAuditContext>();
|
|
repo ??= Substitute.For<IAuditEventRepository>();
|
|
options ??= new AuditOptions();
|
|
var optsWrapper = Options.Create(options);
|
|
return new AuditLogger(context, repo, optsWrapper, TimeProvider.System);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogAsync_WithAllContext_PassesEnrichedValuesToRepo()
|
|
{
|
|
var context = Substitute.For<IAuditContext>();
|
|
var correlationId = Guid.NewGuid();
|
|
context.ActorUserId.Returns(42);
|
|
context.ActorRoleId.Returns(7);
|
|
context.Ip.Returns("10.0.0.5");
|
|
context.UserAgent.Returns("ua/1.0");
|
|
context.CorrelationId.Returns(correlationId);
|
|
|
|
var repo = Substitute.For<IAuditEventRepository>();
|
|
var logger = Build(context, repo);
|
|
|
|
await logger.LogAsync("usuario.create", "Usuario", "99",
|
|
metadata: new { username = "juan" });
|
|
|
|
await repo.Received(1).InsertAsync(
|
|
Arg.Any<DateTime>(),
|
|
42,
|
|
7,
|
|
"usuario.create",
|
|
"Usuario",
|
|
"99",
|
|
correlationId,
|
|
"10.0.0.5",
|
|
"ua/1.0",
|
|
Arg.Is<string?>(m => m != null && m.Contains("\"username\"") && m.Contains("juan")),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogAsync_WithoutActorUserId_ThrowsAuditContextMissingException()
|
|
{
|
|
var context = Substitute.For<IAuditContext>();
|
|
context.ActorUserId.Returns((int?)null);
|
|
context.CorrelationId.Returns(Guid.NewGuid());
|
|
var repo = Substitute.For<IAuditEventRepository>();
|
|
var logger = Build(context, repo);
|
|
|
|
var act = async () => await logger.LogAsync("usuario.create", "Usuario", "1");
|
|
|
|
await act.Should().ThrowAsync<AuditContextMissingException>();
|
|
await repo.DidNotReceive().InsertAsync(
|
|
Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
|
Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
|
|
Arg.Any<string?>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogAsync_SanitizesMetadata_StripsBlacklistedKeys()
|
|
{
|
|
var context = Substitute.For<IAuditContext>();
|
|
context.ActorUserId.Returns(1);
|
|
var repo = Substitute.For<IAuditEventRepository>();
|
|
var logger = Build(context, repo);
|
|
|
|
await logger.LogAsync("usuario.update", "Usuario", "1",
|
|
metadata: new { password = "secret", email = "e@x" });
|
|
|
|
await repo.Received(1).InsertAsync(
|
|
Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
|
Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
|
|
Arg.Is<string?>(m => m != null && !m.Contains("\"password\"") && m.Contains("\"email\"")),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogAsync_NullMetadata_PassesNullToRepo()
|
|
{
|
|
var context = Substitute.For<IAuditContext>();
|
|
context.ActorUserId.Returns(1);
|
|
var repo = Substitute.For<IAuditEventRepository>();
|
|
var logger = Build(context, repo);
|
|
|
|
await logger.LogAsync("usuario.deactivate", "Usuario", "1");
|
|
|
|
await repo.Received(1).InsertAsync(
|
|
Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
|
|
"usuario.deactivate", "Usuario", "1",
|
|
Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
|
|
Arg.Is<string?>(m => m == null),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogAsync_RepositoryThrows_ExceptionBubblesUp()
|
|
{
|
|
var context = Substitute.For<IAuditContext>();
|
|
context.ActorUserId.Returns(1);
|
|
var repo = Substitute.For<IAuditEventRepository>();
|
|
repo.InsertAsync(
|
|
Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
|
Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
|
|
Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
|
.Returns<long>(_ => throw new InvalidOperationException("simulated db failure"));
|
|
|
|
var logger = Build(context, repo);
|
|
|
|
var act = async () => await logger.LogAsync("usuario.create", "Usuario", "1");
|
|
|
|
// Fail-closed: exception MUST bubble to caller (caller's TransactionScope will roll back).
|
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
|
.WithMessage("simulated db failure");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LogAsync_UsesCustomSanitizedKeys_FromOptions()
|
|
{
|
|
var context = Substitute.For<IAuditContext>();
|
|
context.ActorUserId.Returns(1);
|
|
var repo = Substitute.For<IAuditEventRepository>();
|
|
var logger = Build(context, repo, new AuditOptions { SanitizedKeys = new[] { "internalId" } });
|
|
|
|
await logger.LogAsync("x.y", "T", "1", metadata: new { internalId = "secret", visible = "ok" });
|
|
|
|
await repo.Received(1).InsertAsync(
|
|
Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<int?>(),
|
|
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
|
|
Arg.Any<Guid?>(), Arg.Any<string?>(), Arg.Any<string?>(),
|
|
Arg.Is<string?>(m => m != null && !m.Contains("\"internalId\"") && m.Contains("\"visible\"")),
|
|
Arg.Any<CancellationToken>());
|
|
}
|
|
}
|