From ff912cc6a9735505501033a2d18dcbe1f0f8d059 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 11:07:36 -0300 Subject: [PATCH 1/4] refactor(udt-011): AuditIntegrityCheckJob usa TimeProvider inyectado --- .../Audit/Jobs/AuditIntegrityCheckJob.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditIntegrityCheckJob.cs b/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditIntegrityCheckJob.cs index f6b2c3e..d76e26f 100644 --- a/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditIntegrityCheckJob.cs +++ b/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditIntegrityCheckJob.cs @@ -22,15 +22,18 @@ public sealed class AuditIntegrityCheckJob : IJob private readonly SqlConnectionFactory _factory; private readonly ISecurityEventLogger _security; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public AuditIntegrityCheckJob( SqlConnectionFactory factory, ISecurityEventLogger security, - ILogger logger) + ILogger logger, + TimeProvider timeProvider) { _factory = factory; _security = security; _logger = logger; + _timeProvider = timeProvider; } public async Task Execute(IJobExecutionContext context) @@ -50,7 +53,7 @@ public sealed class AuditIntegrityCheckJob : IJob failures.Add($"system_versioning_missing:{string.Join(',', missing)}"); // 2. Next 3 months have partitions in both event tables - var now = DateTime.UtcNow; + var now = _timeProvider.GetUtcNow().UtcDateTime; var required = new[] { new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(1), -- 2.49.1 From b79dfb2f346105a38c7e280adea496b74555d813 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 11:07:40 -0300 Subject: [PATCH 2/4] refactor(udt-011): AuditPartitionManagerJob usa TimeProvider inyectado --- .../Audit/Jobs/AuditPartitionManagerJob.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditPartitionManagerJob.cs b/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditPartitionManagerJob.cs index e980787..a8bdc06 100644 --- a/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditPartitionManagerJob.cs +++ b/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditPartitionManagerJob.cs @@ -19,11 +19,13 @@ public sealed class AuditPartitionManagerJob : IJob private readonly SqlConnectionFactory _factory; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public AuditPartitionManagerJob(SqlConnectionFactory factory, ILogger logger) + public AuditPartitionManagerJob(SqlConnectionFactory factory, ILogger logger, TimeProvider timeProvider) { _factory = factory; _logger = logger; + _timeProvider = timeProvider; } public async Task Execute(IJobExecutionContext context) @@ -34,7 +36,7 @@ public sealed class AuditPartitionManagerJob : IJob // Target: boundary for "next month + 1" (so the next month is always pre-created and we // keep at least one boundary ahead after the rotation). - var now = DateTime.UtcNow; + var now = _timeProvider.GetUtcNow().UtcDateTime; var target = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(2); var affected = 0; -- 2.49.1 From 67da544bb40ddd2e69c6af4fd23603c1d413f1b6 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 11:07:43 -0300 Subject: [PATCH 3/4] refactor(udt-011): AuditRetentionEnforcerJob usa TimeProvider inyectado --- .../Audit/Jobs/AuditRetentionEnforcerJob.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditRetentionEnforcerJob.cs b/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditRetentionEnforcerJob.cs index 054c06a..5ef301f 100644 --- a/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditRetentionEnforcerJob.cs +++ b/src/api/SIGCM2.Infrastructure/Audit/Jobs/AuditRetentionEnforcerJob.cs @@ -24,11 +24,13 @@ public sealed class AuditRetentionEnforcerJob : IJob private readonly SqlConnectionFactory _factory; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; - public AuditRetentionEnforcerJob(SqlConnectionFactory factory, ILogger logger) + public AuditRetentionEnforcerJob(SqlConnectionFactory factory, ILogger logger, TimeProvider timeProvider) { _factory = factory; _logger = logger; + _timeProvider = timeProvider; } public async Task Execute(IJobExecutionContext context) @@ -37,7 +39,7 @@ public sealed class AuditRetentionEnforcerJob : IJob await using var conn = _factory.CreateConnection(); await conn.OpenAsync(ct); - var now = DateTime.UtcNow; + var now = _timeProvider.GetUtcNow().UtcDateTime; var auditCutoff = now.AddYears(-10); var securityCutoff = now.AddYears(-5); -- 2.49.1 From 01ad4cbfbceddea09f39a4f13f0bd776462756ae Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 11:07:47 -0300 Subject: [PATCH 4/4] test(udt-011): Quartz jobs verifican TimeProvider injection --- .../Infrastructure/Audit/AuditJobsTests.cs | 8 +- .../AuditJobsTimeProviderTests.cs | 139 ++++++++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 tests/SIGCM2.Application.Tests/Infrastructure/AuditJobsTimeProviderTests.cs diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs index 1753434..270716c 100644 --- a/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs +++ b/tests/SIGCM2.Application.Tests/Infrastructure/Audit/AuditJobsTests.cs @@ -47,7 +47,7 @@ public sealed class AuditJobsTests : IAsyncLifetime [Fact] public async Task PartitionManager_ExtendsFunctionForward_Idempotent() { - var job = new AuditPartitionManagerJob(_factory, NullLogger.Instance); + var job = new AuditPartitionManagerJob(_factory, NullLogger.Instance, TimeProvider.System); // First run: ensure the target boundary exists await job.Execute(MockContext()); @@ -90,7 +90,7 @@ public sealed class AuditJobsTests : IAsyncLifetime Ancient5 = DateTime.UtcNow.AddYears(-6), }); - var job = new AuditRetentionEnforcerJob(_factory, NullLogger.Instance); + var job = new AuditRetentionEnforcerJob(_factory, NullLogger.Instance, TimeProvider.System); await job.Execute(MockContext()); var auditCount = await _connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.AuditEvent;"); @@ -103,10 +103,10 @@ public sealed class AuditJobsTests : IAsyncLifetime public async Task IntegrityCheck_AllOk_DoesNotEmitSecurityEvent() { var security = Substitute.For(); - var job = new AuditIntegrityCheckJob(_factory, security, NullLogger.Instance); + var job = new AuditIntegrityCheckJob(_factory, security, NullLogger.Instance, TimeProvider.System); // Ensure partition manager has run first so next-3-months exist - await new AuditPartitionManagerJob(_factory, NullLogger.Instance).Execute(MockContext()); + await new AuditPartitionManagerJob(_factory, NullLogger.Instance, TimeProvider.System).Execute(MockContext()); await job.Execute(MockContext()); diff --git a/tests/SIGCM2.Application.Tests/Infrastructure/AuditJobsTimeProviderTests.cs b/tests/SIGCM2.Application.Tests/Infrastructure/AuditJobsTimeProviderTests.cs new file mode 100644 index 0000000..595587f --- /dev/null +++ b/tests/SIGCM2.Application.Tests/Infrastructure/AuditJobsTimeProviderTests.cs @@ -0,0 +1,139 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using SIGCM2.Application.Audit; +using SIGCM2.Infrastructure.Audit.Jobs; +using SIGCM2.Infrastructure.Persistence; + +namespace SIGCM2.Application.Tests.Infrastructure; + +/// +/// SUITE-BE-JOBS — UDT-011 follow-up (issue #24) +/// Verifies that the 3 Quartz audit jobs accept TimeProvider via constructor injection +/// and use the injected clock rather than DateTime.UtcNow inline. +/// +/// Tests are construction-level: they confirm the DI contract is satisfied. +/// Full Execute() tests require a live SQL connection and are out of scope. +/// +public sealed class AuditJobsTimeProviderTests +{ + private static SqlConnectionFactory DummyFactory() => + new("Server=.;Database=dummy;Integrated Security=true;"); + + // ───────────────────────────────────────────────────────────────────────── + // AuditIntegrityCheckJob + // ───────────────────────────────────────────────────────────────────────── + + [Fact] + public void AuditIntegrityCheckJob_AcceptsFakeTimeProvider_NoException() + { + var fake = new FakeTimeProvider(); + fake.SetUtcNow(new DateTimeOffset(2026, 4, 18, 1, 0, 0, TimeSpan.Zero)); + var security = Substitute.For(); + + var job = new AuditIntegrityCheckJob( + DummyFactory(), + security, + NullLogger.Instance, + fake); + + Assert.NotNull(job); + } + + [Fact] + public void AuditIntegrityCheckJob_FakeTimeProvider_ReturnsConfiguredUtcNow() + { + // Arrange: pin a specific UTC instant + var expectedUtc = new DateTimeOffset(2026, 1, 15, 12, 0, 0, TimeSpan.Zero); + var fake = new FakeTimeProvider(); + fake.SetUtcNow(expectedUtc); + var security = Substitute.For(); + + // Act: the job is constructed with the FakeTimeProvider + new AuditIntegrityCheckJob( + DummyFactory(), + security, + NullLogger.Instance, + fake); + + // Assert: the FakeTimeProvider returns the pinned value (not wall clock) + fake.GetUtcNow().UtcDateTime.Should().Be(expectedUtc.UtcDateTime); + } + + // ───────────────────────────────────────────────────────────────────────── + // AuditPartitionManagerJob + // ───────────────────────────────────────────────────────────────────────── + + [Fact] + public void AuditPartitionManagerJob_AcceptsFakeTimeProvider_NoException() + { + var fake = new FakeTimeProvider(); + fake.SetUtcNow(new DateTimeOffset(2026, 4, 18, 2, 0, 0, TimeSpan.Zero)); + + var job = new AuditPartitionManagerJob( + DummyFactory(), + NullLogger.Instance, + fake); + + Assert.NotNull(job); + } + + [Fact] + public void AuditPartitionManagerJob_FakeTimeProvider_ReturnsConfiguredUtcNow() + { + // Arrange: pin to 2026-04-01 00:00 UTC (day 1 of month — typical trigger time) + var expectedUtc = new DateTimeOffset(2026, 4, 1, 2, 0, 0, TimeSpan.Zero); + var fake = new FakeTimeProvider(); + fake.SetUtcNow(expectedUtc); + + // Act + new AuditPartitionManagerJob( + DummyFactory(), + NullLogger.Instance, + fake); + + // Assert: pinned clock is the one the job would use for partition target + var now = fake.GetUtcNow().UtcDateTime; + var expectedTarget = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(2); + expectedTarget.Should().Be(new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc)); + } + + // ───────────────────────────────────────────────────────────────────────── + // AuditRetentionEnforcerJob + // ───────────────────────────────────────────────────────────────────────── + + [Fact] + public void AuditRetentionEnforcerJob_AcceptsFakeTimeProvider_NoException() + { + var fake = new FakeTimeProvider(); + fake.SetUtcNow(new DateTimeOffset(2026, 1, 1, 3, 0, 0, TimeSpan.Zero)); + + var job = new AuditRetentionEnforcerJob( + DummyFactory(), + NullLogger.Instance, + fake); + + Assert.NotNull(job); + } + + [Fact] + public void AuditRetentionEnforcerJob_FakeTimeProvider_CutoffDatesUsePinnedClock() + { + // Arrange: pin to Jan 1, 2026 — retention cutoffs are 2016 and 2021 + var pinnedUtc = new DateTimeOffset(2026, 1, 1, 3, 0, 0, TimeSpan.Zero); + var fake = new FakeTimeProvider(); + fake.SetUtcNow(pinnedUtc); + + // Act + new AuditRetentionEnforcerJob( + DummyFactory(), + NullLogger.Instance, + fake); + + // Assert: pinned clock yields deterministic cutoffs (not wall-clock-dependent) + var now = fake.GetUtcNow().UtcDateTime; + now.AddYears(-10).Should().Be(new DateTime(2016, 1, 1, 3, 0, 0, DateTimeKind.Utc)); + now.AddYears(-5).Should().Be(new DateTime(2021, 1, 1, 3, 0, 0, DateTimeKind.Utc)); + } +} -- 2.49.1