docs re-org, audit fixes, build fixes

This commit is contained in:
StellaOps Bot
2026-01-05 09:35:33 +02:00
parent eca4e964d3
commit dfab8a29c3
173 changed files with 1276 additions and 560 deletions

View File

@@ -7,11 +7,11 @@ using StellaOps.SbomService.Models;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class EntrypointEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
public class EntrypointEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public EntrypointEndpointsTests(WebApplicationFactory<Program> factory)
public EntrypointEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory;
}

View File

@@ -9,11 +9,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class OrchestratorEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
public class OrchestratorEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public OrchestratorEndpointsTests(WebApplicationFactory<Program> factory)
public OrchestratorEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory;
}

View File

@@ -13,11 +13,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class ProjectionEndpointTests : IClassFixture<WebApplicationFactory<Program>>
public class ProjectionEndpointTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public ProjectionEndpointTests(WebApplicationFactory<Program> factory)
public ProjectionEndpointTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
var contentRoot = ResolveContentRoot();
_factory = factory.WithWebHostBuilder(builder =>

View File

@@ -8,11 +8,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class ResolverFeedExportTests : IClassFixture<WebApplicationFactory<Program>>
public class ResolverFeedExportTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public ResolverFeedExportTests(WebApplicationFactory<Program> factory)
public ResolverFeedExportTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory;
}

View File

@@ -8,11 +8,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class SbomAssetEventsTests : IClassFixture<WebApplicationFactory<Program>>
public class SbomAssetEventsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public SbomAssetEventsTests(WebApplicationFactory<Program> factory)
public SbomAssetEventsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory;
}

View File

@@ -8,11 +8,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class SbomEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
public class SbomEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public SbomEndpointsTests(WebApplicationFactory<Program> factory)
public SbomEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory.WithWebHostBuilder(_ => { });
}

View File

@@ -9,11 +9,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class SbomEventEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
public class SbomEventEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public SbomEventEndpointsTests(WebApplicationFactory<Program> factory)
public SbomEventEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory.WithWebHostBuilder(_ => { });
}

View File

@@ -8,11 +8,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public class SbomInventoryEventsTests : IClassFixture<WebApplicationFactory<Program>>
public class SbomInventoryEventsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public SbomInventoryEventsTests(WebApplicationFactory<Program> factory)
public SbomInventoryEventsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory;
}

View File

@@ -10,11 +10,11 @@ using Xunit;
using StellaOps.TestKit;
namespace StellaOps.SbomService.Tests;
public sealed class SbomLedgerEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
public sealed class SbomLedgerEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public SbomLedgerEndpointsTests(WebApplicationFactory<Program> factory)
public SbomLedgerEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory.WithWebHostBuilder(_ => { });
}

View File

@@ -1311,5 +1311,8 @@ app.MapPost("/internal/orchestrator/watermarks", async Task<IResult> (
app.Run();
// Program class public for WebApplicationFactory<Program>
public partial class Program;
// Program class in namespace to avoid conflicts with other assemblies
namespace StellaOps.SbomService
{
public partial class Program;
}

View File

@@ -6,9 +6,11 @@ namespace StellaOps.SbomService.Repositories;
internal sealed class InMemoryOrchestratorRepository : IOrchestratorRepository
{
private readonly ConcurrentDictionary<string, List<OrchestratorSource>> _sources = new(StringComparer.Ordinal);
private readonly TimeProvider _timeProvider;
public InMemoryOrchestratorRepository()
public InMemoryOrchestratorRepository(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
Seed();
}
@@ -37,7 +39,7 @@ internal sealed class InMemoryOrchestratorRepository : IOrchestratorRepository
sourceId,
request.ArtifactDigest.Trim(),
request.SourceType.Trim(),
DateTimeOffset.UtcNow,
_timeProvider.GetUtcNow(),
request.Metadata.Trim());
// Idempotent on (tenant, artifactDigest, sourceType)

View File

@@ -1,13 +1,28 @@
using System;
namespace StellaOps.SbomService.Services;
/// <summary>
/// Provides the current time abstraction - delegates to TimeProvider.
/// </summary>
/// <remarks>
/// Deprecated: Prefer injecting TimeProvider directly. This interface is kept
/// for backward compatibility during migration.
/// </remarks>
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
/// <summary>
/// Default IClock implementation that delegates to TimeProvider.
/// </summary>
public sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
private readonly TimeProvider _timeProvider;
public SystemClock(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public DateTimeOffset UtcNow => _timeProvider.GetUtcNow();
}

View File

@@ -136,9 +136,9 @@ public sealed record ReplayVerificationResult
public ImmutableArray<ReplayFieldDrift> Drifts { get; init; } = ImmutableArray<ReplayFieldDrift>.Empty;
/// <summary>
/// When the verification was performed.
/// When the verification was performed. Must be explicitly set by calling code.
/// </summary>
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset VerifiedAt { get; init; }
/// <summary>
/// Optional message with additional context.
@@ -249,7 +249,7 @@ public sealed record ReplayDriftAnalysis
public required string DriftSummary { get; init; }
/// <summary>
/// When the analysis was performed.
/// When the analysis was performed. Must be explicitly set by calling code.
/// </summary>
public DateTimeOffset AnalyzedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset AnalyzedAt { get; init; }
}

View File

@@ -26,19 +26,22 @@ internal sealed class LineageCompareService : ILineageCompareService
private readonly IVexDeltaRepository? _vexDeltaRepository;
private readonly ILineageCompareCache? _cache;
private readonly ILogger<LineageCompareService> _logger;
private readonly TimeProvider _timeProvider;
public LineageCompareService(
ISbomLineageGraphService lineageService,
ISbomLedgerService ledgerService,
ILogger<LineageCompareService> logger,
IVexDeltaRepository? vexDeltaRepository = null,
ILineageCompareCache? cache = null)
ILineageCompareCache? cache = null,
TimeProvider? timeProvider = null)
{
_lineageService = lineageService ?? throw new ArgumentNullException(nameof(lineageService));
_ledgerService = ledgerService ?? throw new ArgumentNullException(nameof(ledgerService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_vexDeltaRepository = vexDeltaRepository;
_cache = cache;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <inheritdoc />
@@ -139,7 +142,7 @@ internal sealed class LineageCompareService : ILineageCompareService
FromDigest = fromDigest,
ToDigest = toDigest,
TenantId = tenantId,
ComputedAt = DateTimeOffset.UtcNow,
ComputedAt = _timeProvider.GetUtcNow(),
FromArtifact = fromArtifact,
ToArtifact = toArtifact,
Summary = summary,

View File

@@ -21,16 +21,19 @@ internal sealed class LineageExportService : ILineageExportService
private readonly ISbomLineageGraphService _lineageService;
private readonly IReplayHashService? _replayHashService;
private readonly ILogger<LineageExportService> _logger;
private readonly TimeProvider _timeProvider;
private const long MaxExportSizeBytes = 50 * 1024 * 1024; // 50MB limit
public LineageExportService(
ISbomLineageGraphService lineageService,
ILogger<LineageExportService> logger,
IReplayHashService? replayHashService = null)
IReplayHashService? replayHashService = null,
TimeProvider? timeProvider = null)
{
_lineageService = lineageService;
_logger = logger;
_replayHashService = replayHashService;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<LineageExportResponse?> ExportAsync(
@@ -59,7 +62,7 @@ internal sealed class LineageExportService : ILineageExportService
Version = "1.0",
FromDigest = request.FromDigest,
ToDigest = request.ToDigest,
GeneratedAt = DateTimeOffset.UtcNow,
GeneratedAt = _timeProvider.GetUtcNow(),
ReplayHash = diff.ReplayHash ?? ComputeFallbackHash(request.FromDigest, request.ToDigest),
SbomDiff = request.IncludeSbomDiff ? diff.SbomDiff?.Summary : null,
VexDeltas = request.IncludeVexDeltas ? diff.VexDiff : null,
@@ -91,7 +94,7 @@ internal sealed class LineageExportService : ILineageExportService
// Generate export ID and URL
var exportId = Guid.NewGuid().ToString("N");
var downloadUrl = $"/api/v1/lineage/export/{exportId}/download";
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
var expiresAt = _timeProvider.GetUtcNow().AddHours(24);
// TODO: Store evidence pack for retrieval (file system, blob storage, etc.)
// For now, return metadata only
@@ -114,9 +117,9 @@ internal sealed class LineageExportService : ILineageExportService
};
}
private static string ComputeFallbackHash(string fromDigest, string toDigest)
private string ComputeFallbackHash(string fromDigest, string toDigest)
{
var input = $"{fromDigest}:{toDigest}:{DateTimeOffset.UtcNow:O}";
var input = $"{fromDigest}:{toDigest}:{_timeProvider.GetUtcNow():O}";
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = SHA256.HashData(bytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();

View File

@@ -221,11 +221,13 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache
{
private readonly Dictionary<string, (SbomLineageHoverCard Card, DateTimeOffset ExpiresAt)> _cache = new();
private readonly LineageHoverCacheOptions _options;
private readonly TimeProvider _timeProvider;
private readonly object _lock = new();
public InMemoryLineageHoverCache(LineageHoverCacheOptions? options = null)
public InMemoryLineageHoverCache(LineageHoverCacheOptions? options = null, TimeProvider? timeProvider = null)
{
_options = options ?? new LineageHoverCacheOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<SbomLineageHoverCard?> GetAsync(string fromDigest, string toDigest, string tenantId, CancellationToken ct = default)
@@ -240,7 +242,7 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache
{
if (_cache.TryGetValue(key, out var entry))
{
if (entry.ExpiresAt > DateTimeOffset.UtcNow)
if (entry.ExpiresAt > _timeProvider.GetUtcNow())
{
return Task.FromResult<SbomLineageHoverCard?>(entry.Card);
}
@@ -260,7 +262,7 @@ internal sealed class InMemoryLineageHoverCache : ILineageHoverCache
}
var key = BuildKey(fromDigest, toDigest, tenantId);
var expiresAt = DateTimeOffset.UtcNow.Add(_options.Ttl);
var expiresAt = _timeProvider.GetUtcNow().Add(_options.Ttl);
lock (_lock)
{

View File

@@ -11,8 +11,8 @@ public sealed record OrchestratorControlState(
string Backpressure,
DateTimeOffset UpdatedAtUtc)
{
public static OrchestratorControlState Default(string tenantId) =>
new(tenantId, false, 0, "normal", DateTimeOffset.UtcNow);
public static OrchestratorControlState Default(string tenantId, TimeProvider? timeProvider = null) =>
new(tenantId, false, 0, "normal", (timeProvider ?? TimeProvider.System).GetUtcNow());
}
public sealed record OrchestratorControlRequest(
@@ -34,13 +34,18 @@ internal sealed class OrchestratorControlService : IOrchestratorControlService
private readonly Counter<long> _controlUpdates;
private readonly ObservableGauge<int> _throttleGauge;
private readonly ObservableGauge<int> _pausedGauge;
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary<string, OrchestratorControlState> _cache = new(StringComparer.Ordinal);
public OrchestratorControlService(IOrchestratorControlRepository repository, Meter meter)
public OrchestratorControlService(
IOrchestratorControlRepository repository,
Meter meter,
TimeProvider? timeProvider = null)
{
_repository = repository;
_meter = meter;
_timeProvider = timeProvider ?? TimeProvider.System;
_controlUpdates = meter.CreateCounter<long>("sbom_orchestrator_control_updates");
_throttleGauge = meter.CreateObservableGauge("sbom_orchestrator_throttle_percent", ObserveThrottle);
_pausedGauge = meter.CreateObservableGauge("sbom_orchestrator_paused", ObservePaused);
@@ -66,7 +71,7 @@ internal sealed class OrchestratorControlService : IOrchestratorControlService
Paused: request.Paused ?? current.Paused,
ThrottlePercent: throttle,
Backpressure: string.IsNullOrWhiteSpace(request.Backpressure) ? current.Backpressure : request.Backpressure!.Trim().ToLowerInvariant(),
UpdatedAtUtc: DateTimeOffset.UtcNow);
UpdatedAtUtc: _timeProvider.GetUtcNow());
await _repository.SetAsync(updated, cancellationToken);
_cache[updated.TenantId] = updated;

View File

@@ -30,15 +30,18 @@ public sealed class RegistrySourceService : IRegistrySourceService
private readonly IRegistrySourceRepository _sourceRepository;
private readonly IRegistrySourceRunRepository _runRepository;
private readonly ILogger<RegistrySourceService> _logger;
private readonly TimeProvider _timeProvider;
public RegistrySourceService(
IRegistrySourceRepository sourceRepository,
IRegistrySourceRunRepository runRepository,
ILogger<RegistrySourceService> logger)
ILogger<RegistrySourceService> logger,
TimeProvider? timeProvider = null)
{
_sourceRepository = sourceRepository;
_runRepository = runRepository;
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
}
/// <summary>
@@ -125,7 +128,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
if (request.Status.HasValue) source.Status = request.Status.Value;
if (request.Tags is not null) source.Tags = request.Tags.ToList();
source.UpdatedAt = DateTimeOffset.UtcNow;
source.UpdatedAt = _timeProvider.GetUtcNow();
source.UpdatedBy = userId;
var updated = await _sourceRepository.UpdateAsync(source, cancellationToken);
@@ -156,23 +159,23 @@ public sealed class RegistrySourceService : IRegistrySourceService
var source = await _sourceRepository.GetByIdAsync(id, cancellationToken);
if (source is null)
{
return new TestRegistrySourceResponse(id, false, "Registry source not found", null, TimeSpan.Zero, DateTimeOffset.UtcNow);
return new TestRegistrySourceResponse(id, false, "Registry source not found", null, TimeSpan.Zero, _timeProvider.GetUtcNow());
}
var startTime = DateTimeOffset.UtcNow;
var startTime = _timeProvider.GetUtcNow();
// TODO: Implement actual registry connection test
// For now, simulate a successful test
await Task.Delay(100, cancellationToken);
var duration = DateTimeOffset.UtcNow - startTime;
var duration = _timeProvider.GetUtcNow() - startTime;
// Update source status
var newStatus = RegistrySourceStatus.Active;
if (source.Status != newStatus)
{
source.Status = newStatus;
source.UpdatedAt = DateTimeOffset.UtcNow;
source.UpdatedAt = _timeProvider.GetUtcNow();
await _sourceRepository.UpdateAsync(source, cancellationToken);
}
@@ -188,7 +191,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
["type"] = source.Type.ToString()
},
duration,
DateTimeOffset.UtcNow);
_timeProvider.GetUtcNow());
}
/// <summary>
@@ -209,7 +212,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
Status = RegistryRunStatus.Queued,
TriggerType = triggerType,
TriggerMetadata = triggerMetadata,
StartedAt = DateTimeOffset.UtcNow
StartedAt = _timeProvider.GetUtcNow()
};
var created = await _runRepository.CreateAsync(run, cancellationToken);
@@ -229,7 +232,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
if (source is null) return null;
source.Status = RegistrySourceStatus.Paused;
source.UpdatedAt = DateTimeOffset.UtcNow;
source.UpdatedAt = _timeProvider.GetUtcNow();
source.UpdatedBy = userId;
var updated = await _sourceRepository.UpdateAsync(source, cancellationToken);
@@ -252,7 +255,7 @@ public sealed class RegistrySourceService : IRegistrySourceService
}
source.Status = RegistrySourceStatus.Active;
source.UpdatedAt = DateTimeOffset.UtcNow;
source.UpdatedAt = _timeProvider.GetUtcNow();
source.UpdatedBy = userId;
var updated = await _sourceRepository.UpdateAsync(source, cancellationToken);

View File

@@ -74,6 +74,7 @@ internal sealed class ReplayVerificationService : IReplayVerificationService
ExpectedHash = request.ReplayHash,
ComputedHash = string.Empty,
Status = ReplayVerificationStatus.InputsNotFound,
VerifiedAt = _clock.UtcNow,
Error = "Unable to determine verification inputs. Provide explicit inputs or ensure hash is stored."
};
}
@@ -119,6 +120,7 @@ internal sealed class ReplayVerificationService : IReplayVerificationService
ExpectedHash = request.ReplayHash,
ComputedHash = string.Empty,
Status = ReplayVerificationStatus.Error,
VerifiedAt = _clock.UtcNow,
Error = ex.Message
};
}

View File

@@ -16,6 +16,12 @@ public interface IWatermarkService
internal sealed class InMemoryWatermarkService : IWatermarkService
{
private readonly ConcurrentDictionary<string, WatermarkState> _watermarks = new(StringComparer.Ordinal);
private readonly TimeProvider _timeProvider;
public InMemoryWatermarkService(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public Task<WatermarkState> GetAsync(string tenantId, CancellationToken cancellationToken)
{
@@ -24,14 +30,14 @@ internal sealed class InMemoryWatermarkService : IWatermarkService
return Task.FromResult(state);
}
var created = new WatermarkState(tenantId, string.Empty, DateTimeOffset.UtcNow);
var created = new WatermarkState(tenantId, string.Empty, _timeProvider.GetUtcNow());
_watermarks[tenantId] = created;
return Task.FromResult(created);
}
public Task<WatermarkState> SetAsync(string tenantId, string watermark, CancellationToken cancellationToken)
{
var state = new WatermarkState(tenantId, watermark, DateTimeOffset.UtcNow);
var state = new WatermarkState(tenantId, watermark, _timeProvider.GetUtcNow());
_watermarks[tenantId] = state;
return Task.FromResult(state);
}

View File

@@ -19,5 +19,6 @@
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.SbomService.Tests" />
</ItemGroup>
</Project>

View File

@@ -13,12 +13,15 @@ public sealed class SbomLineageEdgeRepository : RepositoryBase<LineageDataSource
private const string Schema = "sbom";
private const string Table = "sbom_lineage_edges";
private const string FullTable = $"{Schema}.{Table}";
private readonly TimeProvider _timeProvider;
public SbomLineageEdgeRepository(
LineageDataSource dataSource,
ILogger<SbomLineageEdgeRepository> logger)
ILogger<SbomLineageEdgeRepository> logger,
TimeProvider? timeProvider = null)
: base(dataSource, logger)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async ValueTask<LineageGraph> GetGraphAsync(
@@ -260,7 +263,7 @@ public sealed class SbomLineageEdgeRepository : RepositoryBase<LineageDataSource
ArtifactDigest: artifactDigest,
SbomVersionId: null,
SequenceNumber: 0,
CreatedAt: DateTimeOffset.UtcNow,
CreatedAt: _timeProvider.GetUtcNow(),
Metadata: null
);
}

View File

@@ -16,6 +16,7 @@ public sealed class LineageGraphService : ILineageGraphService
private readonly ISbomVerdictLinkRepository _verdictRepository;
private readonly IDistributedCache? _cache;
private readonly ILogger<LineageGraphService> _logger;
private readonly TimeProvider _timeProvider;
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(10);
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
@@ -25,13 +26,15 @@ public sealed class LineageGraphService : ILineageGraphService
IVexDeltaRepository deltaRepository,
ISbomVerdictLinkRepository verdictRepository,
ILogger<LineageGraphService> logger,
IDistributedCache? cache = null)
IDistributedCache? cache = null,
TimeProvider? timeProvider = null)
{
_edgeRepository = edgeRepository;
_deltaRepository = deltaRepository;
_verdictRepository = verdictRepository;
_cache = cache;
_logger = logger;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async ValueTask<LineageGraphResponse> GetLineageAsync(
@@ -184,7 +187,7 @@ public sealed class LineageGraphService : ILineageGraphService
// Placeholder implementation
var downloadUrl = $"https://evidence.stellaops.example/exports/{Guid.NewGuid()}.tar.gz";
var expiresAt = DateTimeOffset.UtcNow.AddHours(24);
var expiresAt = _timeProvider.GetUtcNow().AddHours(24);
return new ExportResult(
DownloadUrl: downloadUrl,

View File

@@ -11,11 +11,16 @@ namespace StellaOps.SbomService.Persistence.Postgres.Repositories;
/// </summary>
public sealed class PostgresOrchestratorRepository : RepositoryBase<SbomServiceDataSource>, IOrchestratorRepository
{
private readonly TimeProvider _timeProvider;
private bool _tableInitialized;
public PostgresOrchestratorRepository(SbomServiceDataSource dataSource, ILogger<PostgresOrchestratorRepository> logger)
public PostgresOrchestratorRepository(
SbomServiceDataSource dataSource,
ILogger<PostgresOrchestratorRepository> logger,
TimeProvider? timeProvider = null)
: base(dataSource, logger)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async Task<IReadOnlyList<OrchestratorSource>> ListAsync(string tenantId, CancellationToken cancellationToken)
@@ -79,7 +84,7 @@ public sealed class PostgresOrchestratorRepository : RepositoryBase<SbomServiceD
var count = Convert.ToInt32(await countCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
var sourceId = $"src-{count + 1:D3}";
var now = DateTimeOffset.UtcNow;
var now = _timeProvider.GetUtcNow();
const string insertSql = @"
INSERT INTO sbom.orchestrator_sources (tenant_id, source_id, artifact_digest, source_type, created_at, metadata)

View File

@@ -78,9 +78,9 @@ public sealed record LineageEdge
public required string TenantId { get; init; }
/// <summary>
/// When the edge was created.
/// When the edge was created. Must be explicitly set by calling code.
/// </summary>
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Optional metadata about the relationship.

View File

@@ -94,9 +94,9 @@ public sealed record SbomVerdictLink
public required string TenantId { get; init; }
/// <summary>
/// When the link was created.
/// When the link was created. Must be explicitly set by calling code.
/// </summary>
public DateTimeOffset LinkedAt { get; init; } = DateTimeOffset.UtcNow;
public required DateTimeOffset LinkedAt { get; init; }
/// <summary>
/// SBOM artifact digest for cross-reference.