Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly.
- Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps.
- Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges.
- Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges.
- Set up project file for the test project with necessary dependencies and configurations.
- Include JSON fixture files for testing purposes.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -0,0 +1,129 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure;
using StellaOps.Findings.Ledger.Infrastructure.Policy;
using StellaOps.Findings.Ledger.Options;
using StellaOps.Findings.Ledger.Services;
namespace StellaOps.Findings.Ledger.Infrastructure.Projection;
public sealed class LedgerProjectionWorker : BackgroundService
{
private readonly ILedgerEventStream _eventStream;
private readonly IFindingProjectionRepository _repository;
private readonly IPolicyEvaluationService _policyEvaluationService;
private readonly TimeProvider _timeProvider;
private readonly LedgerServiceOptions.ProjectionOptions _options;
private readonly ILogger<LedgerProjectionWorker> _logger;
public LedgerProjectionWorker(
ILedgerEventStream eventStream,
IFindingProjectionRepository repository,
IPolicyEvaluationService policyEvaluationService,
IOptions<LedgerServiceOptions> options,
TimeProvider timeProvider,
ILogger<LedgerProjectionWorker> logger)
{
_eventStream = eventStream ?? throw new ArgumentNullException(nameof(eventStream));
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_policyEvaluationService = policyEvaluationService ?? throw new ArgumentNullException(nameof(policyEvaluationService));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_options = (options ?? throw new ArgumentNullException(nameof(options))).Value.Projection;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
ProjectionCheckpoint checkpoint;
try
{
checkpoint = await _repository.GetCheckpointAsync(stoppingToken).ConfigureAwait(false);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(ex, "Failed to load ledger projection checkpoint.");
throw;
}
while (!stoppingToken.IsCancellationRequested)
{
IReadOnlyList<LedgerEventRecord> batch;
try
{
batch = await _eventStream.ReadNextBatchAsync(checkpoint, _options.BatchSize, stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to read ledger events for projection replay.");
await DelayAsync(stoppingToken).ConfigureAwait(false);
continue;
}
if (batch.Count == 0)
{
await DelayAsync(stoppingToken).ConfigureAwait(false);
continue;
}
foreach (var record in batch)
{
try
{
await ApplyAsync(record, stoppingToken).ConfigureAwait(false);
checkpoint = checkpoint with
{
LastRecordedAt = record.RecordedAt,
LastEventId = record.EventId,
UpdatedAt = _timeProvider.GetUtcNow()
};
await _repository.SaveCheckpointAsync(checkpoint, stoppingToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
return;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to project ledger event {EventId} for tenant {TenantId}.", record.EventId, record.TenantId);
await DelayAsync(stoppingToken).ConfigureAwait(false);
break;
}
}
}
}
private async Task ApplyAsync(LedgerEventRecord record, CancellationToken cancellationToken)
{
var current = await _repository.GetAsync(record.TenantId, record.FindingId, record.PolicyVersion, cancellationToken).ConfigureAwait(false);
var evaluation = await _policyEvaluationService.EvaluateAsync(record, current, cancellationToken).ConfigureAwait(false);
var result = LedgerProjectionReducer.Reduce(record, current, evaluation);
await _repository.UpsertAsync(result.Projection, cancellationToken).ConfigureAwait(false);
await _repository.InsertHistoryAsync(result.History, cancellationToken).ConfigureAwait(false);
if (result.Action is not null)
{
await _repository.InsertActionAsync(result.Action, cancellationToken).ConfigureAwait(false);
}
}
private async Task DelayAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(_options.IdleDelay, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
}
}