save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -12,10 +12,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.AuditPack/StellaOps.AuditPack.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
<ProjectReference Include="../../StellaOps.Replay.WebService/StellaOps.Replay.WebService.csproj" />

View File

@@ -0,0 +1,457 @@
// <copyright file="DeterminismVerifierTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using StellaOps.Replay.Core;
using Xunit;
namespace StellaOps.Replay.Core.Tests.Unit;
/// <summary>
/// Unit tests for <see cref="DeterminismVerifier"/>.
/// Sprint: SPRINT_20260107_006_005 Task RB-010
/// </summary>
[Trait("Category", "Unit")]
public sealed class DeterminismVerifierTests
{
private readonly Mock<ILogger<DeterminismVerifier>> _logger;
private readonly DeterminismVerifier _sut;
public DeterminismVerifierTests()
{
_logger = new Mock<ILogger<DeterminismVerifier>>();
_sut = new DeterminismVerifier(_logger.Object);
}
#region Verify Tests
[Fact]
public void Verify_IdenticalVerdicts_ReturnsDeterministic()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeTrue();
result.Differences.Should().BeEmpty();
result.DeterminismScore.Should().Be(1.0);
result.OriginalDigest.Should().Be(result.ReplayDigest);
}
[Fact]
public void Verify_DifferentOutcome_ReturnsNonDeterministicWithCriticalDifference()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Fail, "medium", findings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().ContainSingle(d =>
d.Field == "Outcome" &&
d.Severity == DifferenceSeverity.Critical);
result.DeterminismScore.Should().BeLessThan(1.0);
}
[Fact]
public void Verify_DifferentSeverity_ReturnsNonDeterministicWithHighDifference()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "high", findings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().ContainSingle(d =>
d.Field == "Severity" &&
d.Severity == DifferenceSeverity.High);
}
[Fact]
public void Verify_DifferentFindingCount_ReturnsNonDeterministic()
{
// Arrange
var originalFindings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var replayFindings = ImmutableArray<FindingRecord>.Empty;
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", originalFindings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", replayFindings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().Contain(d => d.Field == "FindingCount");
}
[Fact]
public void Verify_MissingFindingInReplay_ReturnsNonDeterministic()
{
// Arrange
var originalFindings = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" },
new FindingRecord { FindingId = "f2", VulnerabilityId = "CVE-2", Component = "pkg:b", Severity = "medium" });
var replayFindings = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" });
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", originalFindings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", replayFindings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().Contain(d =>
d.Field == "Finding:f2" &&
d.OriginalValue == "Present" &&
d.ReplayValue == "Missing");
}
[Fact]
public void Verify_NewFindingInReplay_ReturnsNonDeterministic()
{
// Arrange
var originalFindings = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" });
var replayFindings = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" },
new FindingRecord { FindingId = "f2", VulnerabilityId = "CVE-2", Component = "pkg:b", Severity = "medium" });
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", originalFindings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", replayFindings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().Contain(d =>
d.Field == "Finding:f2" &&
d.OriginalValue == "Missing" &&
d.ReplayValue == "Present");
}
[Fact]
public void Verify_FindingSeverityDiffers_ReturnsNonDeterministic()
{
// Arrange
var originalFindings = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" });
var replayFindings = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "critical" });
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", originalFindings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", replayFindings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().Contain(d =>
d.Field == "Finding:f1:Severity" &&
d.OriginalValue == "high" &&
d.ReplayValue == "critical");
}
[Fact]
public void Verify_DifferentRuleOrder_ReturnsNonDeterministic()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdictWithRules("verdict-1", VerdictOutcome.Pass, "medium", findings,
["rule-1", "rule-2", "rule-3"]);
var replay = CreateVerdictWithRules("verdict-1", VerdictOutcome.Pass, "medium", findings,
["rule-1", "rule-3", "rule-2"]);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.IsDeterministic.Should().BeFalse();
result.Differences.Should().Contain(d =>
d.Field == "RuleOrder" &&
d.Severity == DifferenceSeverity.Low);
}
#endregion
#region ComputeVerdictDigest Tests
[Fact]
public void ComputeVerdictDigest_SameVerdict_ProducesSameDigest()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var verdict1 = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var verdict2 = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
// Act
var digest1 = _sut.ComputeVerdictDigest(verdict1);
var digest2 = _sut.ComputeVerdictDigest(verdict2);
// Assert
digest1.Should().Be(digest2);
}
[Fact]
public void ComputeVerdictDigest_DifferentVerdict_ProducesDifferentDigest()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var verdict1 = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var verdict2 = CreateVerdict("verdict-1", VerdictOutcome.Fail, "medium", findings);
// Act
var digest1 = _sut.ComputeVerdictDigest(verdict1);
var digest2 = _sut.ComputeVerdictDigest(verdict2);
// Assert
digest1.Should().NotBe(digest2);
}
[Fact]
public void ComputeVerdictDigest_ReturnsSha256Format()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var verdict = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
// Act
var digest = _sut.ComputeVerdictDigest(verdict);
// Assert
digest.Should().StartWith("sha256:");
digest.Length.Should().Be(7 + 64); // "sha256:" + 64 hex chars
}
[Fact]
public void ComputeVerdictDigest_FindingOrderDoesNotAffectDigest()
{
// Arrange
var findings1 = ImmutableArray.Create(
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" },
new FindingRecord { FindingId = "f2", VulnerabilityId = "CVE-2", Component = "pkg:b", Severity = "medium" });
var findings2 = ImmutableArray.Create(
new FindingRecord { FindingId = "f2", VulnerabilityId = "CVE-2", Component = "pkg:b", Severity = "medium" },
new FindingRecord { FindingId = "f1", VulnerabilityId = "CVE-1", Component = "pkg:a", Severity = "high" });
var verdict1 = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings1);
var verdict2 = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings2);
// Act
var digest1 = _sut.ComputeVerdictDigest(verdict1);
var digest2 = _sut.ComputeVerdictDigest(verdict2);
// Assert - should be the same due to deterministic ordering in digest calculation
digest1.Should().Be(digest2);
}
#endregion
#region GenerateDiffReport Tests
[Fact]
public void GenerateDiffReport_MatchingResult_ContainsMatchMessage()
{
// Arrange
var result = new VerificationResult
{
OriginalDigest = "sha256:abc123",
ReplayDigest = "sha256:abc123",
IsDeterministic = true,
DeterminismScore = 1.0,
VerifiedAt = DateTimeOffset.UtcNow
};
// Act
var report = _sut.GenerateDiffReport(result);
// Assert
report.Should().Contain("## Result: MATCH");
report.Should().Contain("identical verdict");
}
[Fact]
public void GenerateDiffReport_MismatchResult_ContainsDifferences()
{
// Arrange
var result = new VerificationResult
{
OriginalDigest = "sha256:abc123",
ReplayDigest = "sha256:def456",
IsDeterministic = false,
DeterminismScore = 0.5,
VerifiedAt = DateTimeOffset.UtcNow,
Differences = ImmutableArray.Create(
new VerdictDifference
{
Field = "Outcome",
OriginalValue = "Pass",
ReplayValue = "Fail",
Severity = DifferenceSeverity.Critical,
Explanation = "The final decision differs"
})
};
// Act
var report = _sut.GenerateDiffReport(result);
// Assert
report.Should().Contain("## Result: MISMATCH");
report.Should().Contain("### Outcome");
report.Should().Contain("**Original:** `Pass`");
report.Should().Contain("**Replay:** `Fail`");
report.Should().Contain("## Possible Causes");
}
[Fact]
public void GenerateDiffReport_ContainsDigestTable()
{
// Arrange
var result = new VerificationResult
{
OriginalDigest = "sha256:originaldigest",
ReplayDigest = "sha256:replaydigest",
IsDeterministic = false,
DeterminismScore = 0.8,
VerifiedAt = DateTimeOffset.UtcNow
};
// Act
var report = _sut.GenerateDiffReport(result);
// Assert
report.Should().Contain("## Digests");
report.Should().Contain("`sha256:originaldigest`");
report.Should().Contain("`sha256:replaydigest`");
}
#endregion
#region DeterminismScore Tests
[Fact]
public void Verify_NoDifferences_ScoreIsOne()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.DeterminismScore.Should().Be(1.0);
}
[Fact]
public void Verify_CriticalDifference_ScoreDecreasesSignificantly()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdict("verdict-1", VerdictOutcome.Pass, "medium", findings);
var replay = CreateVerdict("verdict-1", VerdictOutcome.Fail, "medium", findings);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.DeterminismScore.Should().BeLessThanOrEqualTo(0.5); // Critical penalty is 0.5
}
[Fact]
public void Verify_MultipleLowDifferences_ScoreDecreasesModestly()
{
// Arrange
var findings = CreateFindings("finding-1", "CVE-2023-1234", "pkg:npm/lodash@4.17.21", "high");
var original = CreateVerdictWithRules("verdict-1", VerdictOutcome.Pass, "medium", findings,
["rule-1", "rule-2", "rule-3"]);
var replay = CreateVerdictWithRules("verdict-1", VerdictOutcome.Pass, "medium", findings,
["rule-3", "rule-2", "rule-1"]);
// Act
var result = _sut.Verify(original, replay);
// Assert
result.DeterminismScore.Should().BeGreaterThan(0.8); // Low penalty of 0.05
}
#endregion
#region Helper Methods
private static ImmutableArray<FindingRecord> CreateFindings(
string findingId,
string cveId,
string component,
string severity)
{
return ImmutableArray.Create(new FindingRecord
{
FindingId = findingId,
VulnerabilityId = cveId,
Component = component,
Severity = severity
});
}
private static VerdictRecord CreateVerdict(
string verdictId,
VerdictOutcome outcome,
string severity,
ImmutableArray<FindingRecord> findings)
{
return new VerdictRecord
{
VerdictId = verdictId,
Outcome = outcome,
Severity = severity,
PolicyId = "policy-default",
RuleIds = ImmutableArray.Create("rule-1"),
Findings = findings,
RenderedAt = DateTimeOffset.UtcNow
};
}
private static VerdictRecord CreateVerdictWithRules(
string verdictId,
VerdictOutcome outcome,
string severity,
ImmutableArray<FindingRecord> findings,
string[] ruleIds)
{
return new VerdictRecord
{
VerdictId = verdictId,
Outcome = outcome,
Severity = severity,
PolicyId = "policy-default",
RuleIds = ruleIds.ToImmutableArray(),
Findings = findings,
RenderedAt = DateTimeOffset.UtcNow
};
}
#endregion
}

View File

@@ -0,0 +1,452 @@
// <copyright file="InputManifestResolverTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Moq;
using StellaOps.Replay.Core;
using Xunit;
namespace StellaOps.Replay.Core.Tests.Unit;
/// <summary>
/// Unit tests for <see cref="InputManifestResolver"/>.
/// Sprint: SPRINT_20260107_006_005 Task RB-010
/// </summary>
[Trait("Category", "Unit")]
public sealed class InputManifestResolverTests : IDisposable
{
private readonly Mock<IFeedSnapshotStore> _feedStore;
private readonly Mock<IPolicyManifestStore> _policyStore;
private readonly Mock<IVexDocumentStore> _vexStore;
private readonly MemoryCache _cache;
private readonly Mock<ILogger<InputManifestResolver>> _logger;
private readonly InputManifestResolver _sut;
public InputManifestResolverTests()
{
_feedStore = new Mock<IFeedSnapshotStore>();
_policyStore = new Mock<IPolicyManifestStore>();
_vexStore = new Mock<IVexDocumentStore>();
_cache = new MemoryCache(new MemoryCacheOptions());
_logger = new Mock<ILogger<InputManifestResolver>>();
_sut = new InputManifestResolver(
_feedStore.Object,
_policyStore.Object,
_vexStore.Object,
_cache,
_logger.Object);
}
public void Dispose()
{
_cache.Dispose();
}
#region ResolveAsync Tests
[Fact]
public async Task ResolveAsync_EmptyManifest_ReturnsCompleteWithNoData()
{
// Arrange
var manifest = new InputManifest();
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeTrue();
result.Errors.Should().BeEmpty();
result.FeedData.Should().BeNull();
result.PolicyBundle.Should().BeNull();
result.VexDocuments.Should().BeEmpty();
}
[Fact]
public async Task ResolveAsync_FeedSnapshotExists_ResolvesFeedData()
{
// Arrange
var feedHash = "sha256:abc123";
var feedData = new FeedData
{
Hash = feedHash,
Content = "test feed content"u8.ToArray(),
SnapshotAt = DateTimeOffset.UtcNow
};
_feedStore.Setup(x => x.GetAsync(feedHash, It.IsAny<CancellationToken>()))
.ReturnsAsync(feedData);
var manifest = new InputManifest { FeedSnapshotHash = feedHash };
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeTrue();
result.FeedData.Should().Be(feedData);
result.Errors.Should().BeEmpty();
}
[Fact]
public async Task ResolveAsync_FeedSnapshotNotFound_AddsError()
{
// Arrange
var feedHash = "sha256:notfound";
_feedStore.Setup(x => x.GetAsync(feedHash, It.IsAny<CancellationToken>()))
.ReturnsAsync((FeedData?)null);
var manifest = new InputManifest { FeedSnapshotHash = feedHash };
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeFalse();
result.FeedData.Should().BeNull();
result.Errors.Should().ContainSingle(e =>
e.Type == InputType.FeedSnapshot &&
e.Hash == feedHash &&
e.Message.Contains("Not found"));
}
[Fact]
public async Task ResolveAsync_PolicyManifestExists_ResolvesPolicyBundle()
{
// Arrange
var policyHash = "sha256:policy123";
var policyBundle = new PolicyBundle
{
Hash = policyHash,
Content = ImmutableArray.Create((byte)1, (byte)2, (byte)3),
Version = "1.0.0"
};
_policyStore.Setup(x => x.GetAsync(policyHash, It.IsAny<CancellationToken>()))
.ReturnsAsync(policyBundle);
var manifest = new InputManifest { PolicyManifestHash = policyHash };
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeTrue();
result.PolicyBundle.Should().Be(policyBundle);
}
[Fact]
public async Task ResolveAsync_VexDocumentsExist_ResolvesAllDocuments()
{
// Arrange
var vex1 = new VexDocument { Hash = "sha256:vex1", Content = "{}", Format = "OpenVEX" };
var vex2 = new VexDocument { Hash = "sha256:vex2", Content = "{}", Format = "CSAF" };
_vexStore.Setup(x => x.GetAsync("sha256:vex1", It.IsAny<CancellationToken>()))
.ReturnsAsync(vex1);
_vexStore.Setup(x => x.GetAsync("sha256:vex2", It.IsAny<CancellationToken>()))
.ReturnsAsync(vex2);
var manifest = new InputManifest
{
VexDocumentHashes = ImmutableArray.Create("sha256:vex1", "sha256:vex2")
};
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeTrue();
result.VexDocuments.Should().HaveCount(2);
result.VexDocuments.Should().Contain(vex1);
result.VexDocuments.Should().Contain(vex2);
}
[Fact]
public async Task ResolveAsync_PartialVexNotFound_AddsErrorButIncludesFound()
{
// Arrange
var vex1 = new VexDocument { Hash = "sha256:vex1", Content = "{}", Format = "OpenVEX" };
_vexStore.Setup(x => x.GetAsync("sha256:vex1", It.IsAny<CancellationToken>()))
.ReturnsAsync(vex1);
_vexStore.Setup(x => x.GetAsync("sha256:vex2", It.IsAny<CancellationToken>()))
.ReturnsAsync((VexDocument?)null);
var manifest = new InputManifest
{
VexDocumentHashes = ImmutableArray.Create("sha256:vex1", "sha256:vex2")
};
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeFalse();
result.VexDocuments.Should().ContainSingle(v => v.Hash == "sha256:vex1");
result.Errors.Should().ContainSingle(e =>
e.Type == InputType.VexDocument &&
e.Hash == "sha256:vex2");
}
[Fact]
public async Task ResolveAsync_FeedStoreThrowsException_AddsError()
{
// Arrange
var feedHash = "sha256:error";
_feedStore.Setup(x => x.GetAsync(feedHash, It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Connection failed"));
var manifest = new InputManifest { FeedSnapshotHash = feedHash };
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.IsComplete.Should().BeFalse();
result.FeedData.Should().BeNull();
result.Errors.Should().ContainSingle(e =>
e.Type == InputType.FeedSnapshot &&
e.Message.Contains("Connection failed"));
}
[Fact]
public async Task ResolveAsync_PassThroughFields_CopiedToResult()
{
// Arrange
var timestamp = DateTimeOffset.Parse("2026-01-09T12:00:00Z");
var manifest = new InputManifest
{
SourceCodeHash = "sha256:source",
BaseImageDigest = "sha256:baseimage",
ToolchainVersion = "1.2.3",
RandomSeed = 42,
TimestampOverride = timestamp
};
// Act
var result = await _sut.ResolveAsync(manifest);
// Assert
result.SourceCodeHash.Should().Be("sha256:source");
result.BaseImageDigest.Should().Be("sha256:baseimage");
result.ToolchainVersion.Should().Be("1.2.3");
result.RandomSeed.Should().Be(42);
result.TimestampOverride.Should().Be(timestamp);
}
[Fact]
public async Task ResolveAsync_CachesResolvedInputs()
{
// Arrange
var feedHash = "sha256:cacheable";
var feedData = new FeedData
{
Hash = feedHash,
Content = "cached"u8.ToArray(),
SnapshotAt = DateTimeOffset.UtcNow
};
_feedStore.Setup(x => x.GetAsync(feedHash, It.IsAny<CancellationToken>()))
.ReturnsAsync(feedData);
var manifest = new InputManifest { FeedSnapshotHash = feedHash };
// Act - first call
await _sut.ResolveAsync(manifest);
// Act - second call
await _sut.ResolveAsync(manifest);
// Assert - store should only be called once due to caching
_feedStore.Verify(x => x.GetAsync(feedHash, It.IsAny<CancellationToken>()), Times.Once);
}
#endregion
#region ValidateAsync Tests
[Fact]
public async Task ValidateAsync_AllInputsExist_ReturnsValid()
{
// Arrange
_feedStore.Setup(x => x.ExistsAsync("sha256:feed", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_policyStore.Setup(x => x.ExistsAsync("sha256:policy", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_vexStore.Setup(x => x.ExistsAsync("sha256:vex", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
var manifest = new InputManifest
{
FeedSnapshotHash = "sha256:feed",
PolicyManifestHash = "sha256:policy",
VexDocumentHashes = ImmutableArray.Create("sha256:vex")
};
// Act
var result = await _sut.ValidateAsync(manifest);
// Assert
result.IsValid.Should().BeTrue();
result.MissingInputs.Should().BeEmpty();
}
[Fact]
public async Task ValidateAsync_FeedMissing_ReturnsInvalidWithMissingList()
{
// Arrange
_feedStore.Setup(x => x.ExistsAsync("sha256:missing", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var manifest = new InputManifest { FeedSnapshotHash = "sha256:missing" };
// Act
var result = await _sut.ValidateAsync(manifest);
// Assert
result.IsValid.Should().BeFalse();
result.MissingInputs.Should().ContainSingle(m => m.Contains("Feed snapshot") && m.Contains("sha256:missing"));
}
[Fact]
public async Task ValidateAsync_PolicyMissing_ReturnsInvalidWithMissingList()
{
// Arrange
_policyStore.Setup(x => x.ExistsAsync("sha256:missing", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var manifest = new InputManifest { PolicyManifestHash = "sha256:missing" };
// Act
var result = await _sut.ValidateAsync(manifest);
// Assert
result.IsValid.Should().BeFalse();
result.MissingInputs.Should().ContainSingle(m => m.Contains("Policy manifest"));
}
[Fact]
public async Task ValidateAsync_VexMissing_ReturnsInvalidWithMissingList()
{
// Arrange
_vexStore.Setup(x => x.ExistsAsync("sha256:vex1", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
_vexStore.Setup(x => x.ExistsAsync("sha256:vex2", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
var manifest = new InputManifest
{
VexDocumentHashes = ImmutableArray.Create("sha256:vex1", "sha256:vex2")
};
// Act
var result = await _sut.ValidateAsync(manifest);
// Assert
result.IsValid.Should().BeFalse();
result.MissingInputs.Should().ContainSingle(m => m.Contains("VEX document") && m.Contains("sha256:vex2"));
}
[Fact]
public async Task ValidateAsync_EmptyManifest_ReturnsValid()
{
// Arrange
var manifest = new InputManifest();
// Act
var result = await _sut.ValidateAsync(manifest);
// Assert
result.IsValid.Should().BeTrue();
result.MissingInputs.Should().BeEmpty();
}
#endregion
#region InputManifest Model Tests
[Fact]
public void InputManifest_DefaultValues_AreCorrect()
{
// Arrange & Act
var manifest = new InputManifest();
// Assert
manifest.FeedSnapshotHash.Should().BeNull();
manifest.PolicyManifestHash.Should().BeNull();
manifest.SourceCodeHash.Should().BeNull();
manifest.BaseImageDigest.Should().BeNull();
manifest.VexDocumentHashes.Should().BeEmpty();
manifest.ToolchainVersion.Should().BeNull();
manifest.RandomSeed.Should().BeNull();
manifest.TimestampOverride.Should().BeNull();
}
[Fact]
public void InputManifest_WithInitializer_SetsValues()
{
// Arrange & Act
var timestamp = DateTimeOffset.UtcNow;
var manifest = new InputManifest
{
FeedSnapshotHash = "feed",
PolicyManifestHash = "policy",
SourceCodeHash = "source",
BaseImageDigest = "image",
VexDocumentHashes = ImmutableArray.Create("vex1", "vex2"),
ToolchainVersion = "1.0",
RandomSeed = 42,
TimestampOverride = timestamp
};
// Assert
manifest.FeedSnapshotHash.Should().Be("feed");
manifest.PolicyManifestHash.Should().Be("policy");
manifest.SourceCodeHash.Should().Be("source");
manifest.BaseImageDigest.Should().Be("image");
manifest.VexDocumentHashes.Should().HaveCount(2);
manifest.ToolchainVersion.Should().Be("1.0");
manifest.RandomSeed.Should().Be(42);
manifest.TimestampOverride.Should().Be(timestamp);
}
#endregion
#region ResolvedInputs Model Tests
[Fact]
public void ResolvedInputs_DefaultValues_AreCorrect()
{
// Arrange & Act
var resolved = new ResolvedInputs();
// Assert
resolved.FeedData.Should().BeNull();
resolved.PolicyBundle.Should().BeNull();
resolved.VexDocuments.Should().BeEmpty();
resolved.Errors.Should().BeEmpty();
resolved.IsComplete.Should().BeFalse();
}
#endregion
#region InputResolutionError Model Tests
[Fact]
public void InputResolutionError_RecordEquality_Works()
{
// Arrange
var error1 = new InputResolutionError(InputType.FeedSnapshot, "hash1", "message1");
var error2 = new InputResolutionError(InputType.FeedSnapshot, "hash1", "message1");
var error3 = new InputResolutionError(InputType.PolicyManifest, "hash1", "message1");
// Assert
error1.Should().Be(error2);
error1.Should().NotBe(error3);
}
#endregion
}

View File

@@ -13,6 +13,8 @@ using StellaOps.AuditPack.Services;
using StellaOps.Replay.WebService;
using StellaOps.TestKit;
using Xunit;
using AuditPackResult = StellaOps.AuditPack.Services.ReplayExecutionResult;
using AuditPackStatus = StellaOps.AuditPack.Services.ReplayStatus;
namespace StellaOps.Replay.Core.Tests;
@@ -52,12 +54,12 @@ public class VerdictReplayEndpointsTests
};
}
private static ReplayExecutionResult CreateSuccessResult(bool match = true)
private static AuditPackResult CreateSuccessResult(bool match = true)
{
return new ReplayExecutionResult
return new AuditPackResult
{
Success = true,
Status = match ? ReplayStatus.Match : ReplayStatus.Drift,
Status = match ? AuditPackStatus.Match : AuditPackStatus.Drift,
VerdictMatches = match,
DecisionMatches = match,
OriginalVerdictDigest = "sha256:verdict",
@@ -115,7 +117,7 @@ public class VerdictReplayEndpointsTests
ConfidenceScore = 0.95,
ExpectedOutcome = new ReplayOutcomePrediction
{
ExpectedStatus = ReplayStatus.Match,
ExpectedStatus = AuditPackStatus.Match,
ExpectedDecision = "pass"
}
});
@@ -155,14 +157,14 @@ public class VerdictReplayEndpointsTests
public void CompareDivergence_DetectsDifferences()
{
// Arrange
var original = new ReplayExecutionResult
var original = new AuditPackResult
{
Success = true,
OriginalVerdictDigest = "sha256:aaa",
OriginalDecision = "pass"
};
var replayed = new ReplayExecutionResult
var replayed = new AuditPackResult
{
Success = true,
ReplayedVerdictDigest = "sha256:bbb",
@@ -174,8 +176,8 @@ public class VerdictReplayEndpointsTests
};
_mockPredicate.Setup(p => p.CompareDivergence(
It.IsAny<ReplayExecutionResult>(),
It.IsAny<ReplayExecutionResult>()))
It.IsAny<AuditPackResult>(),
It.IsAny<AuditPackResult>()))
.Returns(new ReplayDivergenceReport
{
HasDivergence = true,
@@ -218,10 +220,10 @@ public class VerdictReplayEndpointsTests
public void ReplayExecutionResult_DriftItems_ArePopulated()
{
// Arrange
var result = new ReplayExecutionResult
var result = new AuditPackResult
{
Success = true,
Status = ReplayStatus.Drift,
Status = AuditPackStatus.Drift,
VerdictMatches = false,
Drifts =
[

View File

@@ -11,6 +11,8 @@ using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
using StellaOps.TestKit;
using Xunit;
using AuditPackResult = StellaOps.AuditPack.Services.ReplayExecutionResult;
using AuditPackStatus = StellaOps.AuditPack.Services.ReplayStatus;
namespace StellaOps.Replay.Core.Tests;
@@ -195,7 +197,7 @@ public class VerdictReplayIntegrationTests
{
// Arrange
var attestationService = new ReplayAttestationService();
var replays = new List<(AuditBundleManifest, ReplayExecutionResult)>
var replays = new List<(AuditBundleManifest, AuditPackResult)>
{
(CreateTestManifest("bundle-1"), CreateMatchingReplayResult()),
(CreateTestManifest("bundle-2"), CreateDivergentReplayResult()),
@@ -358,12 +360,12 @@ public class VerdictReplayIntegrationTests
};
}
private static ReplayExecutionResult CreateMatchingReplayResult()
private static AuditPackResult CreateMatchingReplayResult()
{
return new ReplayExecutionResult
return new AuditPackResult
{
Success = true,
Status = ReplayStatus.Match,
Status = AuditPackStatus.Match,
VerdictMatches = true,
DecisionMatches = true,
OriginalVerdictDigest = "sha256:verdict-digest-123",
@@ -376,12 +378,12 @@ public class VerdictReplayIntegrationTests
};
}
private static ReplayExecutionResult CreateDivergentReplayResult()
private static AuditPackResult CreateDivergentReplayResult()
{
return new ReplayExecutionResult
return new AuditPackResult
{
Success = true,
Status = ReplayStatus.Drift,
Status = AuditPackStatus.Drift,
VerdictMatches = false,
DecisionMatches = false,
OriginalVerdictDigest = "sha256:verdict-original",