Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -265,13 +265,16 @@ public sealed record PolicyRunStatus
|
||||
int attempts = 0,
|
||||
string? traceId = null,
|
||||
string? explainUri = null,
|
||||
ImmutableSortedDictionary<string, string>? metadata = null,
|
||||
string? schemaVersion = null)
|
||||
: this(
|
||||
runId,
|
||||
tenantId,
|
||||
policyId,
|
||||
policyVersion,
|
||||
ImmutableSortedDictionary<string, string>? metadata = null,
|
||||
bool cancellationRequested = false,
|
||||
DateTimeOffset? cancellationRequestedAt = null,
|
||||
string? cancellationReason = null,
|
||||
string? schemaVersion = null)
|
||||
: this(
|
||||
runId,
|
||||
tenantId,
|
||||
policyId,
|
||||
policyVersion,
|
||||
mode,
|
||||
status,
|
||||
priority,
|
||||
@@ -282,16 +285,19 @@ public sealed record PolicyRunStatus
|
||||
inputs ?? PolicyRunInputs.Empty,
|
||||
determinismHash,
|
||||
Validation.TrimToNull(errorCode),
|
||||
Validation.TrimToNull(error),
|
||||
attempts,
|
||||
Validation.TrimToNull(traceId),
|
||||
Validation.TrimToNull(explainUri),
|
||||
metadata ?? ImmutableSortedDictionary<string, string>.Empty,
|
||||
schemaVersion)
|
||||
{
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
Validation.TrimToNull(error),
|
||||
attempts,
|
||||
Validation.TrimToNull(traceId),
|
||||
Validation.TrimToNull(explainUri),
|
||||
metadata ?? ImmutableSortedDictionary<string, string>.Empty,
|
||||
cancellationRequested,
|
||||
cancellationRequestedAt,
|
||||
cancellationReason,
|
||||
schemaVersion)
|
||||
{
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PolicyRunStatus(
|
||||
string runId,
|
||||
string tenantId,
|
||||
@@ -307,12 +313,15 @@ public sealed record PolicyRunStatus
|
||||
PolicyRunInputs inputs,
|
||||
string? determinismHash,
|
||||
string? errorCode,
|
||||
string? error,
|
||||
int attempts,
|
||||
string? traceId,
|
||||
string? explainUri,
|
||||
ImmutableSortedDictionary<string, string> metadata,
|
||||
string? schemaVersion = null)
|
||||
string? error,
|
||||
int attempts,
|
||||
string? traceId,
|
||||
string? explainUri,
|
||||
ImmutableSortedDictionary<string, string> metadata,
|
||||
bool cancellationRequested,
|
||||
DateTimeOffset? cancellationRequestedAt,
|
||||
string? cancellationReason,
|
||||
string? schemaVersion = null)
|
||||
{
|
||||
SchemaVersion = SchedulerSchemaVersions.EnsurePolicyRunStatus(schemaVersion);
|
||||
RunId = Validation.EnsureId(runId, nameof(runId));
|
||||
@@ -339,16 +348,19 @@ public sealed record PolicyRunStatus
|
||||
? throw new ArgumentOutOfRangeException(nameof(attempts), attempts, "Attempts must be non-negative.")
|
||||
: attempts;
|
||||
TraceId = Validation.TrimToNull(traceId);
|
||||
ExplainUri = Validation.TrimToNull(explainUri);
|
||||
Metadata = (metadata ?? ImmutableSortedDictionary<string, string>.Empty)
|
||||
.Select(static pair => new KeyValuePair<string, string>(
|
||||
Validation.TrimToNull(pair.Key)?.ToLowerInvariant() ?? string.Empty,
|
||||
Validation.TrimToNull(pair.Value) ?? string.Empty))
|
||||
.Where(static pair => !string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
|
||||
.DistinctBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableSortedDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
|
||||
}
|
||||
ExplainUri = Validation.TrimToNull(explainUri);
|
||||
Metadata = (metadata ?? ImmutableSortedDictionary<string, string>.Empty)
|
||||
.Select(static pair => new KeyValuePair<string, string>(
|
||||
Validation.TrimToNull(pair.Key)?.ToLowerInvariant() ?? string.Empty,
|
||||
Validation.TrimToNull(pair.Value) ?? string.Empty))
|
||||
.Where(static pair => !string.IsNullOrEmpty(pair.Key) && !string.IsNullOrEmpty(pair.Value))
|
||||
.DistinctBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableSortedDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal);
|
||||
CancellationRequested = cancellationRequested;
|
||||
CancellationRequestedAt = Validation.NormalizeTimestamp(cancellationRequestedAt);
|
||||
CancellationReason = Validation.TrimToNull(cancellationReason);
|
||||
}
|
||||
|
||||
public string SchemaVersion { get; }
|
||||
|
||||
@@ -392,13 +404,22 @@ public sealed record PolicyRunStatus
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ExplainUri { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public ImmutableSortedDictionary<string, string> Metadata { get; init; } = ImmutableSortedDictionary<string, string>.Empty;
|
||||
|
||||
public PolicyRunStats Stats { get; init; } = PolicyRunStats.Empty;
|
||||
|
||||
public PolicyRunInputs Inputs { get; init; } = PolicyRunInputs.Empty;
|
||||
}
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public ImmutableSortedDictionary<string, string> Metadata { get; init; } = ImmutableSortedDictionary<string, string>.Empty;
|
||||
|
||||
public PolicyRunStats Stats { get; init; } = PolicyRunStats.Empty;
|
||||
|
||||
public PolicyRunInputs Inputs { get; init; } = PolicyRunInputs.Empty;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public bool CancellationRequested { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public DateTimeOffset? CancellationRequestedAt { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? CancellationReason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated metrics captured for a policy run.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scheduler.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper for translating persisted <see cref="PolicyRunJob"/> documents into
|
||||
/// API-facing <see cref="PolicyRunStatus"/> projections.
|
||||
/// </summary>
|
||||
public static class PolicyRunStatusFactory
|
||||
{
|
||||
public static PolicyRunStatus Create(PolicyRunJob job, DateTimeOffset nowUtc)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
|
||||
var status = MapExecutionStatus(job.Status);
|
||||
var queuedAt = job.QueuedAt ?? job.CreatedAt;
|
||||
var startedAt = job.SubmittedAt;
|
||||
var finishedAt = job.CompletedAt ?? job.CancelledAt;
|
||||
var metadata = job.Metadata ?? ImmutableSortedDictionary<string, string>.Empty;
|
||||
var inputs = job.Inputs ?? PolicyRunInputs.Empty;
|
||||
var policyVersion = job.PolicyVersion
|
||||
?? throw new InvalidOperationException($"Policy run job '{job.Id}' is missing policyVersion.");
|
||||
|
||||
return new PolicyRunStatus(
|
||||
job.RunId ?? job.Id,
|
||||
job.TenantId,
|
||||
job.PolicyId,
|
||||
policyVersion,
|
||||
job.Mode,
|
||||
status,
|
||||
job.Priority,
|
||||
queuedAt,
|
||||
job.Status == PolicyRunJobStatus.Pending ? null : startedAt,
|
||||
finishedAt,
|
||||
PolicyRunStats.Empty,
|
||||
inputs,
|
||||
determinismHash: null,
|
||||
errorCode: null,
|
||||
error: job.Status == PolicyRunJobStatus.Failed ? job.LastError : null,
|
||||
attempts: job.AttemptCount,
|
||||
traceId: null,
|
||||
explainUri: null,
|
||||
metadata,
|
||||
cancellationRequested: job.CancellationRequested,
|
||||
cancellationRequestedAt: job.CancellationRequestedAt,
|
||||
cancellationReason: job.CancellationReason,
|
||||
SchedulerSchemaVersions.PolicyRunStatus);
|
||||
}
|
||||
|
||||
private static PolicyRunExecutionStatus MapExecutionStatus(PolicyRunJobStatus status)
|
||||
=> status switch
|
||||
{
|
||||
PolicyRunJobStatus.Pending => PolicyRunExecutionStatus.Queued,
|
||||
PolicyRunJobStatus.Dispatching => PolicyRunExecutionStatus.Running,
|
||||
PolicyRunJobStatus.Submitted => PolicyRunExecutionStatus.Running,
|
||||
PolicyRunJobStatus.Completed => PolicyRunExecutionStatus.Succeeded,
|
||||
PolicyRunJobStatus.Failed => PolicyRunExecutionStatus.Failed,
|
||||
PolicyRunJobStatus.Cancelled => PolicyRunExecutionStatus.Cancelled,
|
||||
_ => PolicyRunExecutionStatus.Queued
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Scheduler.Models;
|
||||
|
||||
public sealed record PolicySimulationWebhookPayload(
|
||||
[property: JsonPropertyName("tenantId")] string TenantId,
|
||||
[property: JsonPropertyName("simulation")] PolicyRunStatus Simulation,
|
||||
[property: JsonPropertyName("result")] string Result,
|
||||
[property: JsonPropertyName("observedAt")] DateTimeOffset ObservedAt,
|
||||
[property: JsonPropertyName("latencySeconds")] double? LatencySeconds,
|
||||
[property: JsonPropertyName("reason")] string? Reason);
|
||||
|
||||
public static class PolicySimulationWebhookPayloadFactory
|
||||
{
|
||||
public static PolicySimulationWebhookPayload Create(PolicyRunStatus status, DateTimeOffset observedAt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(status);
|
||||
|
||||
var result = status.Status switch
|
||||
{
|
||||
PolicyRunExecutionStatus.Succeeded => "succeeded",
|
||||
PolicyRunExecutionStatus.Failed => "failed",
|
||||
PolicyRunExecutionStatus.Cancelled => "cancelled",
|
||||
PolicyRunExecutionStatus.ReplayPending => "replay_pending",
|
||||
PolicyRunExecutionStatus.Running => "running",
|
||||
_ => "queued"
|
||||
};
|
||||
|
||||
var latencySeconds = CalculateLatencySeconds(status, observedAt);
|
||||
var reason = status.Status switch
|
||||
{
|
||||
PolicyRunExecutionStatus.Failed => status.Error,
|
||||
PolicyRunExecutionStatus.Cancelled => status.CancellationReason,
|
||||
_ => null
|
||||
};
|
||||
|
||||
return new PolicySimulationWebhookPayload(
|
||||
status.TenantId,
|
||||
status,
|
||||
result,
|
||||
observedAt,
|
||||
latencySeconds,
|
||||
reason);
|
||||
}
|
||||
|
||||
private static double? CalculateLatencySeconds(PolicyRunStatus status, DateTimeOffset observedAt)
|
||||
{
|
||||
var started = status.QueuedAt;
|
||||
var finished = status.FinishedAt ?? observedAt;
|
||||
|
||||
if (started == default)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var duration = (finished - started).TotalSeconds;
|
||||
if (duration < 0)
|
||||
{
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
return Math.Round(duration, 4);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public sealed record Run
|
||||
DateTimeOffset? finishedAt = null,
|
||||
string? error = null,
|
||||
IEnumerable<DeltaSummary>? deltas = null,
|
||||
string? retryOf = null,
|
||||
string? schemaVersion = null)
|
||||
: this(
|
||||
id,
|
||||
@@ -35,6 +36,7 @@ public sealed record Run
|
||||
Validation.NormalizeTimestamp(finishedAt),
|
||||
Validation.TrimToNull(error),
|
||||
NormalizeDeltas(deltas),
|
||||
Validation.TrimToNull(retryOf),
|
||||
schemaVersion)
|
||||
{
|
||||
}
|
||||
@@ -53,6 +55,7 @@ public sealed record Run
|
||||
DateTimeOffset? finishedAt,
|
||||
string? error,
|
||||
ImmutableArray<DeltaSummary> deltas,
|
||||
string? retryOf,
|
||||
string? schemaVersion = null)
|
||||
{
|
||||
Id = Validation.EnsureId(id, nameof(id));
|
||||
@@ -69,6 +72,7 @@ public sealed record Run
|
||||
Deltas = deltas.IsDefault
|
||||
? ImmutableArray<DeltaSummary>.Empty
|
||||
: deltas.OrderBy(static delta => delta.ImageDigest, StringComparer.Ordinal).ToImmutableArray();
|
||||
RetryOf = Validation.TrimToNull(retryOf);
|
||||
SchemaVersion = SchedulerSchemaVersions.EnsureRun(schemaVersion);
|
||||
}
|
||||
|
||||
@@ -103,6 +107,9 @@ public sealed record Run
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public ImmutableArray<DeltaSummary> Deltas { get; } = ImmutableArray<DeltaSummary>.Empty;
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? RetryOf { get; }
|
||||
|
||||
private static ImmutableArray<DeltaSummary> NormalizeDeltas(IEnumerable<DeltaSummary>? deltas)
|
||||
{
|
||||
if (deltas is null)
|
||||
|
||||
Reference in New Issue
Block a user