sprints and audit work
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user