Files
dmolinari 9bc191c3ae test(udt-011): T400.40 — update tests for TimeProvider injection and explicit now params
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.
2026-04-18 10:12:32 -03:00

102 lines
3.6 KiB
C#

using FluentAssertions;
using Microsoft.Extensions.Options;
using NSubstitute;
using SIGCM2.Application.Audit;
using SIGCM2.Infrastructure.Audit;
using Xunit;
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
/// UDT-010 Batch 6 — SecurityEventLogger unit tests.
/// NOT fail-closed: security events are fire-and-forget writes; actor may be null
/// for login failures.
public sealed class SecurityEventLoggerTests
{
private static SecurityEventLogger Build(
ISecurityEventRepository? repo = null,
IAuditContext? context = null,
AuditOptions? options = null)
{
repo ??= Substitute.For<ISecurityEventRepository>();
context ??= Substitute.For<IAuditContext>();
options ??= new AuditOptions();
return new SecurityEventLogger(repo, context, Options.Create(options), TimeProvider.System);
}
[Fact]
public async Task LogAsync_LoginSuccess_PassesActorAndIpFromContext()
{
var repo = Substitute.For<ISecurityEventRepository>();
var context = Substitute.For<IAuditContext>();
context.Ip.Returns("1.2.3.4");
context.UserAgent.Returns("ua");
var logger = Build(repo, context);
await logger.LogAsync("login", "success", actorUserId: 42);
await repo.Received(1).InsertAsync(
Arg.Any<DateTime>(),
42,
Arg.Is<string?>(s => s == null), // attemptedUsername
Arg.Is<Guid?>(g => g == null), // sessionId
"login", "success",
Arg.Is<string?>(s => s == null), // failureReason
"1.2.3.4",
"ua",
Arg.Is<string?>(s => s == null), // metadata
Arg.Any<CancellationToken>());
}
[Fact]
public async Task LogAsync_LoginFailure_SupportsNullActorAndAttemptedUsername()
{
var repo = Substitute.For<ISecurityEventRepository>();
var logger = Build(repo);
await logger.LogAsync("login", "failure",
actorUserId: null,
attemptedUsername: "juan",
failureReason: "invalid_password");
await repo.Received(1).InsertAsync(
Arg.Any<DateTime>(),
Arg.Is<int?>(i => i == null),
"juan",
Arg.Is<Guid?>(g => g == null),
"login", "failure",
"invalid_password",
Arg.Any<string?>(), Arg.Any<string?>(),
Arg.Is<string?>(s => s == null),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task LogAsync_SanitizesMetadata()
{
var repo = Substitute.For<ISecurityEventRepository>();
var logger = Build(repo);
await logger.LogAsync("login", "failure", attemptedUsername: "x",
metadata: new { token = "leaked", ip = "1.1.1.1" });
await repo.Received(1).InsertAsync(
Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<Guid?>(),
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
Arg.Any<string?>(), Arg.Any<string?>(),
Arg.Is<string?>(m => m != null && !m.Contains("\"token\"") && m.Contains("\"ip\"")),
Arg.Any<CancellationToken>());
}
[Fact]
public async Task LogAsync_DoesNotThrow_WhenActorAndAttemptedUsernameAreBothNull()
{
// This is a legitimate use case (e.g. permission.denied emitted by middleware with an expired token).
var repo = Substitute.For<ISecurityEventRepository>();
var logger = Build(repo);
var act = async () => await logger.LogAsync("permission.denied", "failure");
await act.Should().NotThrowAsync();
}
}