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

@@ -15,6 +15,8 @@ public sealed class SchedulerMongoOptions
public string RunsCollection { get; set; } = "runs";
public string PolicyJobsCollection { get; set; } = "policy_jobs";
public string ImpactSnapshotsCollection { get; set; } = "impact_snapshots";
public string AuditCollection { get; set; } = "audit";

View File

@@ -36,13 +36,19 @@ public interface IPolicyRunJobRepository
PolicyRunMode? mode = null,
IReadOnlyCollection<PolicyRunJobStatus>? statuses = null,
DateTimeOffset? queuedAfter = null,
int limit = 50,
IClientSessionHandle? session = null,
CancellationToken cancellationToken = default);
Task<bool> ReplaceAsync(
PolicyRunJob job,
string? expectedLeaseOwner = null,
IClientSessionHandle? session = null,
CancellationToken cancellationToken = default);
}
int limit = 50,
IClientSessionHandle? session = null,
CancellationToken cancellationToken = default);
Task<bool> ReplaceAsync(
PolicyRunJob job,
string? expectedLeaseOwner = null,
IClientSessionHandle? session = null,
CancellationToken cancellationToken = default);
Task<long> CountAsync(
string tenantId,
PolicyRunMode mode,
IReadOnlyCollection<PolicyRunJobStatus> statuses,
CancellationToken cancellationToken = default);
}

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Internal;
using StellaOps.Scheduler.Storage.Mongo.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Internal;
using StellaOps.Scheduler.Storage.Mongo.Serialization;
namespace StellaOps.Scheduler.Storage.Mongo.Repositories;
@@ -206,16 +207,43 @@ internal sealed class PolicyRunJobRepository : IPolicyRunJobRepository
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return documents
.Select(PolicyRunJobDocumentMapper.FromBsonDocument)
.ToList();
}
public async Task<bool> ReplaceAsync(
PolicyRunJob job,
string? expectedLeaseOwner = null,
IClientSessionHandle? session = null,
CancellationToken cancellationToken = default)
return documents
.Select(PolicyRunJobDocumentMapper.FromBsonDocument)
.ToList();
}
public async Task<long> CountAsync(
string tenantId,
PolicyRunMode mode,
IReadOnlyCollection<PolicyRunJobStatus> statuses,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(tenantId))
{
throw new ArgumentException("Tenant id must be provided.", nameof(tenantId));
}
var filters = new List<FilterDefinition<BsonDocument>>
{
Filter.Eq("tenantId", tenantId),
Filter.Eq("mode", mode.ToString().ToLowerInvariant())
};
if (statuses is { Count: > 0 })
{
var array = new BsonArray(statuses.Select(static status => status.ToString().ToLowerInvariant()));
filters.Add(Filter.In("status", array));
}
var filter = Filter.And(filters);
return await _collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<bool> ReplaceAsync(
PolicyRunJob job,
string? expectedLeaseOwner = null,
IClientSessionHandle? session = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(job);

View File

@@ -0,0 +1,47 @@
using System;
namespace StellaOps.Scheduler.Storage.Mongo.Repositories;
/// <summary>
/// Cursor describing the position of a run in deterministic ordering.
/// </summary>
public sealed record RunListCursor
{
public RunListCursor(DateTimeOffset createdAt, string runId)
{
CreatedAt = NormalizeTimestamp(createdAt);
RunId = NormalizeRunId(runId);
}
/// <summary>
/// Timestamp of the last run observed (UTC).
/// </summary>
public DateTimeOffset CreatedAt { get; }
/// <summary>
/// Identifier of the last run observed.
/// </summary>
public string RunId { get; }
private static DateTimeOffset NormalizeTimestamp(DateTimeOffset value)
{
var utc = value.ToUniversalTime();
return new DateTimeOffset(DateTime.SpecifyKind(utc.DateTime, DateTimeKind.Utc));
}
private static string NormalizeRunId(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Run id must be provided.", nameof(value));
}
var trimmed = value.Trim();
if (trimmed.Length > 256)
{
throw new ArgumentException("Run id exceeds 256 characters.", nameof(value));
}
return trimmed;
}
}

View File

@@ -19,16 +19,21 @@ public sealed class RunQueryOptions
public ImmutableArray<RunState> States { get; init; } = ImmutableArray<RunState>.Empty;
/// <summary>
/// Optional lower bound for creation timestamp (UTC).
/// </summary>
public DateTimeOffset? CreatedAfter { get; init; }
/// <summary>
/// Maximum number of runs to return (default 50 when unspecified).
/// </summary>
public int? Limit { get; init; }
/// <summary>
/// Optional lower bound for creation timestamp (UTC).
/// </summary>
public DateTimeOffset? CreatedAfter { get; init; }
/// <summary>
/// Optional cursor to resume iteration using deterministic ordering.
/// </summary>
public RunListCursor? Cursor { get; init; }
/// <summary>
/// Maximum number of runs to return (default 50 when unspecified).
/// </summary>
public int? Limit { get; init; }
/// <summary>
/// Sort order flag. Defaults to descending by createdAt.
/// </summary>
public bool SortAscending { get; init; }

View File

@@ -127,28 +127,53 @@ internal sealed class RunRepository : IRunRepository
filters.Add(Filter.In("state", options.States.Select(state => state.ToString().ToLowerInvariant())));
}
if (options.CreatedAfter is { } createdAfter)
{
filters.Add(Filter.Gt("createdAt", createdAfter.ToUniversalTime().UtcDateTime));
}
if (options.CreatedAfter is { } createdAfter)
{
filters.Add(Filter.Gt("createdAt", createdAfter.ToUniversalTime().UtcDateTime));
}
if (options.Cursor is { } cursor)
{
var createdAtUtc = cursor.CreatedAt.ToUniversalTime().UtcDateTime;
FilterDefinition<BsonDocument> cursorFilter;
if (options.SortAscending)
{
cursorFilter = Filter.Or(
Filter.Gt("createdAt", createdAtUtc),
Filter.And(
Filter.Eq("createdAt", createdAtUtc),
Filter.Gt("_id", cursor.RunId)));
}
else
{
cursorFilter = Filter.Or(
Filter.Lt("createdAt", createdAtUtc),
Filter.And(
Filter.Eq("createdAt", createdAtUtc),
Filter.Lt("_id", cursor.RunId)));
}
filters.Add(cursorFilter);
}
var combined = Filter.And(filters);
var find = session is null
? _collection.Find(combined)
: _collection.Find(session, combined);
var combined = Filter.And(filters);
var find = session is null
? _collection.Find(combined)
: _collection.Find(session, combined);
var limit = options.Limit is { } specified && specified > 0 ? specified : DefaultListLimit;
find = find.Limit(limit);
var sortDefinition = options.SortAscending
? Sort.Ascending("createdAt")
: Sort.Descending("createdAt");
find = find.Sort(sortDefinition);
var documents = await find.ToListAsync(cancellationToken).ConfigureAwait(false);
return documents.Select(RunDocumentMapper.FromBsonDocument).ToArray();
var limit = options.Limit is { } specified && specified > 0 ? specified : DefaultListLimit;
find = find.Limit(limit);
var sortDefinition = options.SortAscending
? Sort.Combine(Sort.Ascending("createdAt"), Sort.Ascending("_id"))
: Sort.Combine(Sort.Descending("createdAt"), Sort.Descending("_id"));
find = find.Sort(sortDefinition);
var documents = await find.ToListAsync(cancellationToken).ConfigureAwait(false);
return documents.Select(RunDocumentMapper.FromBsonDocument).ToArray();
}
public async Task<IReadOnlyList<Run>> ListByStateAsync(