refactor(udt-011): Quartz jobs usan TimeProvider (closes #24) #28
@@ -47,7 +47,7 @@ public sealed class AuditJobsTests : IAsyncLifetime
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task PartitionManager_ExtendsFunctionForward_Idempotent()
|
public async Task PartitionManager_ExtendsFunctionForward_Idempotent()
|
||||||
{
|
{
|
||||||
var job = new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance);
|
var job = new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance, TimeProvider.System);
|
||||||
|
|
||||||
// First run: ensure the target boundary exists
|
// First run: ensure the target boundary exists
|
||||||
await job.Execute(MockContext());
|
await job.Execute(MockContext());
|
||||||
@@ -90,7 +90,7 @@ public sealed class AuditJobsTests : IAsyncLifetime
|
|||||||
Ancient5 = DateTime.UtcNow.AddYears(-6),
|
Ancient5 = DateTime.UtcNow.AddYears(-6),
|
||||||
});
|
});
|
||||||
|
|
||||||
var job = new AuditRetentionEnforcerJob(_factory, NullLogger<AuditRetentionEnforcerJob>.Instance);
|
var job = new AuditRetentionEnforcerJob(_factory, NullLogger<AuditRetentionEnforcerJob>.Instance, TimeProvider.System);
|
||||||
await job.Execute(MockContext());
|
await job.Execute(MockContext());
|
||||||
|
|
||||||
var auditCount = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.AuditEvent;");
|
var auditCount = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.AuditEvent;");
|
||||||
@@ -103,10 +103,10 @@ public sealed class AuditJobsTests : IAsyncLifetime
|
|||||||
public async Task IntegrityCheck_AllOk_DoesNotEmitSecurityEvent()
|
public async Task IntegrityCheck_AllOk_DoesNotEmitSecurityEvent()
|
||||||
{
|
{
|
||||||
var security = Substitute.For<ISecurityEventLogger>();
|
var security = Substitute.For<ISecurityEventLogger>();
|
||||||
var job = new AuditIntegrityCheckJob(_factory, security, NullLogger<AuditIntegrityCheckJob>.Instance);
|
var job = new AuditIntegrityCheckJob(_factory, security, NullLogger<AuditIntegrityCheckJob>.Instance, TimeProvider.System);
|
||||||
|
|
||||||
// Ensure partition manager has run first so next-3-months exist
|
// Ensure partition manager has run first so next-3-months exist
|
||||||
await new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance).Execute(MockContext());
|
await new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance, TimeProvider.System).Execute(MockContext());
|
||||||
|
|
||||||
await job.Execute(MockContext());
|
await job.Execute(MockContext());
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<ISecurityEventLogger>();
|
||||||
|
|
||||||
|
var job = new AuditIntegrityCheckJob(
|
||||||
|
DummyFactory(),
|
||||||
|
security,
|
||||||
|
NullLogger<AuditIntegrityCheckJob>.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<ISecurityEventLogger>();
|
||||||
|
|
||||||
|
// Act: the job is constructed with the FakeTimeProvider
|
||||||
|
new AuditIntegrityCheckJob(
|
||||||
|
DummyFactory(),
|
||||||
|
security,
|
||||||
|
NullLogger<AuditIntegrityCheckJob>.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<AuditPartitionManagerJob>.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<AuditPartitionManagerJob>.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<AuditRetentionEnforcerJob>.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<AuditRetentionEnforcerJob>.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user