This commit is contained in:
master
2026-01-07 10:25:34 +02:00
726 changed files with 147397 additions and 1364 deletions

View File

@@ -22,7 +22,8 @@ public class DpopProofValidatorTests
new { typ = 123, alg = "ES256" },
new { htm = "GET", htu = "https://api.test/resource", iat = 0, jti = "1" });
var validator = CreateValidator();
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var validator = CreateValidator(now);
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);
@@ -37,7 +38,8 @@ public class DpopProofValidatorTests
new { typ = "dpop+jwt", alg = 55 },
new { htm = "GET", htu = "https://api.test/resource", iat = 0, jti = "1" });
var validator = CreateValidator();
var now = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
var validator = CreateValidator(now);
var result = await validator.ValidateAsync(proof, "GET", new Uri("https://api.test/resource"));
Assert.False(result.IsValid);

View File

@@ -0,0 +1,269 @@
// <copyright file="VerdictBuilderReplayTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace StellaOps.Verdict.Tests;
/// <summary>
/// Tests for VerdictBuilderService.ReplayFromBundleAsync.
/// RPL-005: Unit tests for VerdictBuilder replay with fixtures.
/// </summary>
[Trait("Category", "Unit")]
public sealed class VerdictBuilderReplayTests : IDisposable
{
private readonly VerdictBuilderService _verdictBuilder;
private readonly string _testDir;
public VerdictBuilderReplayTests()
{
_verdictBuilder = new VerdictBuilderService(
NullLoggerFactory.Instance.CreateLogger<VerdictBuilderService>(),
signer: null);
_testDir = Path.Combine(Path.GetTempPath(), $"verdict-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
}
public void Dispose()
{
if (Directory.Exists(_testDir))
{
Directory.Delete(_testDir, recursive: true);
}
}
#region Helper Methods
private void CreateFile(string relativePath, string content)
{
var fullPath = Path.Combine(_testDir, relativePath.TrimStart('/'));
var dir = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(fullPath, content, Encoding.UTF8);
}
private string GetPath(string relativePath) => Path.Combine(_testDir, relativePath.TrimStart('/'));
#endregion
#region ReplayFromBundleAsync Tests
[Fact]
public async Task ReplayFromBundleAsync_MissingSbom_ReturnsFailure()
{
// Arrange
var request = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("SBOM file not found");
}
[Fact]
public async Task ReplayFromBundleAsync_ValidSbom_ReturnsSuccess()
{
// Arrange
var sbomJson = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": []
}
""";
CreateFile("inputs/sbom.json", sbomJson);
var request = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeTrue();
result.VerdictHash.Should().NotBeNullOrEmpty();
result.VerdictHash.Should().StartWith("cgs:sha256:");
result.EngineVersion.Should().Be("1.0.0");
result.DurationMs.Should().BeGreaterOrEqualTo(0);
}
[Fact]
public async Task ReplayFromBundleAsync_WithVexDocuments_LoadsVexFiles()
{
// Arrange
var sbomJson = """{"bomFormat":"CycloneDX","specVersion":"1.6","version":1,"components":[]}""";
var vex1Json = """{"@context":"https://openvex.dev/ns/v0.2.0","@id":"test-vex-1","statements":[]}""";
var vex2Json = """{"@context":"https://openvex.dev/ns/v0.2.0","@id":"test-vex-2","statements":[]}""";
CreateFile("inputs/sbom.json", sbomJson);
CreateFile("inputs/vex/vex1.json", vex1Json);
CreateFile("inputs/vex/vex2.json", vex2Json);
var request = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom.json"),
VexPath = GetPath("inputs/vex"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeTrue();
result.VerdictHash.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ReplayFromBundleAsync_DeterministicHash_SameInputsProduceSameHash()
{
// Arrange
var sbomJson = """{"bomFormat":"CycloneDX","specVersion":"1.6","version":1,"components":[]}""";
CreateFile("inputs/sbom.json", sbomJson);
var request = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act - replay twice with same inputs
var result1 = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
var result2 = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert - should produce identical hash
result1.Success.Should().BeTrue();
result2.Success.Should().BeTrue();
result1.VerdictHash.Should().Be(result2.VerdictHash);
}
[Fact]
public async Task ReplayFromBundleAsync_DifferentInputs_ProduceDifferentHash()
{
// Arrange
var sbom1 = """{"bomFormat":"CycloneDX","specVersion":"1.6","version":1,"components":[]}""";
var sbom2 = """{"bomFormat":"CycloneDX","specVersion":"1.6","version":2,"components":[]}""";
CreateFile("inputs/sbom1.json", sbom1);
CreateFile("inputs/sbom2.json", sbom2);
var request1 = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom1.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
var request2 = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom2.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result1 = await _verdictBuilder.ReplayFromBundleAsync(request1, TestContext.Current.CancellationToken);
var result2 = await _verdictBuilder.ReplayFromBundleAsync(request2, TestContext.Current.CancellationToken);
// Assert
result1.Success.Should().BeTrue();
result2.Success.Should().BeTrue();
result1.VerdictHash.Should().NotBe(result2.VerdictHash);
}
[Fact]
public async Task ReplayFromBundleAsync_WithPolicyLock_LoadsPolicy()
{
// Arrange
var sbomJson = """{"bomFormat":"CycloneDX","specVersion":"1.6","version":1,"components":[]}""";
var policyJson = """
{
"SchemaVersion": "1.0.0",
"PolicyVersion": "custom-policy-v1",
"RuleHashes": {"critical-rule": "sha256:abc"},
"EngineVersion": "1.0.0",
"GeneratedAt": "2026-01-06T00:00:00Z"
}
""";
CreateFile("inputs/sbom.json", sbomJson);
CreateFile("inputs/policy/policy-lock.json", policyJson);
var request = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom.json"),
PolicyPath = GetPath("inputs/policy/policy-lock.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeTrue();
result.VerdictHash.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ReplayFromBundleAsync_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
var sbomJson = """{"bomFormat":"CycloneDX","specVersion":"1.6","version":1,"components":[]}""";
CreateFile("inputs/sbom.json", sbomJson);
var request = new VerdictReplayRequest
{
SbomPath = GetPath("inputs/sbom.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
using var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => _verdictBuilder.ReplayFromBundleAsync(request, cts.Token).AsTask());
}
[Fact]
public async Task ReplayFromBundleAsync_NullRequest_ThrowsArgumentNullException()
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(
() => _verdictBuilder.ReplayFromBundleAsync(null!, TestContext.Current.CancellationToken).AsTask());
}
#endregion
}