Implement TimeProvider injection for deterministic timestamps across various services and modules

This commit is contained in:
master
2026-01-11 10:12:12 +02:00
parent 7f7eb8b228
commit f6ef1ef337
10 changed files with 54 additions and 18 deletions

View File

@@ -329,10 +329,11 @@ public static class VerdictEndpoints
IVerdictStore store,
HttpContext context,
ILogger<VerdictEndpointsLogger> logger,
TimeProvider timeProvider,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(context);
var deletedCount = await store.DeleteExpiredAsync(tenantId, DateTimeOffset.UtcNow, cancellationToken);
var deletedCount = await store.DeleteExpiredAsync(tenantId, timeProvider.GetUtcNow(), cancellationToken);
logger.LogInformation("Deleted {Count} expired verdicts for tenant {TenantId}", deletedCount, tenantId);

View File

@@ -16,13 +16,16 @@ public sealed class PostgresVerdictStore : IVerdictStore
private readonly IDbContextFactory<VerdictDbContext> _contextFactory;
private readonly ILogger<PostgresVerdictStore> _logger;
private readonly JsonSerializerOptions _jsonOptions;
private readonly TimeProvider _timeProvider;
public PostgresVerdictStore(
IDbContextFactory<VerdictDbContext> contextFactory,
ILogger<PostgresVerdictStore> logger)
ILogger<PostgresVerdictStore> logger,
TimeProvider? timeProvider = null)
{
_contextFactory = contextFactory;
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -122,7 +125,7 @@ public sealed class PostgresVerdictStore : IVerdictStore
if (!query.IncludeExpired)
{
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
queryable = queryable.Where(v => v.ExpiresAt == null || v.ExpiresAt > now);
}
@@ -192,7 +195,7 @@ public sealed class PostgresVerdictStore : IVerdictStore
{
await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
var row = await context.Verdicts
.AsNoTracking()
.Where(v => v.TenantId == tenantId && v.SubjectPurl == purl && v.SubjectCveId == cveId)
@@ -255,7 +258,7 @@ public sealed class PostgresVerdictStore : IVerdictStore
VerdictJson = json,
CreatedAt = DateTimeOffset.TryParse(verdict.Provenance.CreatedAt, out var createdAt)
? createdAt
: DateTimeOffset.UtcNow,
: _timeProvider.GetUtcNow(),
ExpiresAt = expiresAt,
};
}

View File

@@ -77,7 +77,7 @@ public sealed class VerdictRow
// Timestamps
[Column("created_at")]
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public required DateTimeOffset CreatedAt { get; set; }
[Column("expires_at")]
public DateTimeOffset? ExpiresAt { get; set; }

View File

@@ -107,6 +107,17 @@ public sealed record ReachabilityInput(bool IsReachable, double Confidence, stri
/// </summary>
public sealed class VerdictAssemblyService : IVerdictAssemblyService
{
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="VerdictAssemblyService"/> class.
/// </summary>
/// <param name="timeProvider">Time provider for deterministic timestamps.</param>
public VerdictAssemblyService(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public StellaVerdict AssembleVerdict(VerdictAssemblyContext context)
{
var subject = BuildSubject(context);
@@ -115,7 +126,7 @@ public sealed class VerdictAssemblyService : IVerdictAssemblyService
var evidenceGraph = BuildEvidenceGraph(context.ProofBundle);
var policyPath = BuildPolicyPath(context.ProofBundle);
var result = BuildResult(context.PolicyVerdict, context.ProofBundle);
var provenance = BuildProvenance(context);
var provenance = BuildProvenance(context, _timeProvider);
var verdict = new StellaVerdict
{
@@ -379,14 +390,14 @@ public sealed class VerdictAssemblyService : IVerdictAssemblyService
};
}
private static VerdictProvenance BuildProvenance(VerdictAssemblyContext context)
private static VerdictProvenance BuildProvenance(VerdictAssemblyContext context, TimeProvider timeProvider)
{
return new VerdictProvenance
{
Generator = context.Generator,
GeneratorVersion = context.GeneratorVersion,
RunId = context.RunId,
CreatedAt = DateTimeOffset.UtcNow.ToString("o"),
CreatedAt = timeProvider.GetUtcNow().ToString("o"),
PolicyBundleId = context.ProofBundle?.PolicyBundleId,
PolicyBundleVersion = context.ProofBundle?.PolicyBundleVersion,
};