Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -7,6 +7,7 @@ using StellaOps.Attestation;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public class DsseHelperTests
|
||||
{
|
||||
private sealed class FakeSigner : IAuthoritySigner
|
||||
@@ -18,7 +19,8 @@ public class DsseHelperTests
|
||||
=> Task.FromResult(Convert.FromHexString("deadbeef"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WrapAsync_ProducesDsseEnvelope()
|
||||
{
|
||||
var stmt = new InTotoStatement(
|
||||
@@ -37,7 +39,8 @@ public class DsseHelperTests
|
||||
envelope.Signatures[0].Signature.Should().Be(Convert.ToBase64String(Convert.FromHexString("deadbeef")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PreAuthenticationEncoding_FollowsDsseSpec()
|
||||
{
|
||||
var payloadType = "example/type";
|
||||
|
||||
@@ -31,7 +31,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
// DSSE-8200-013: Cosign-compatible envelope structure tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnvelopeStructure_HasRequiredFields_ForCosignVerification()
|
||||
{
|
||||
// Arrange
|
||||
@@ -45,7 +46,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(result.IsValid, $"Structure validation failed: {string.Join(", ", result.Errors)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnvelopePayload_IsBase64Encoded_InSerializedForm()
|
||||
{
|
||||
// Arrange
|
||||
@@ -70,7 +72,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.Equal(payload, decoded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnvelopeSignature_IsBase64Encoded_InSerializedForm()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +102,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(sigBytes.Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnvelopePayloadType_IsCorrectMimeType_ForInToto()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +116,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.Equal("application/vnd.in-toto+json", envelope.PayloadType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EnvelopeSerialization_ProducesValidJson_WithoutWhitespace()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +141,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
// DSSE-8200-014: Fulcio certificate chain tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FulcioCertificate_HasCodeSigningEku()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -161,7 +167,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(hasCodeSigning, "Certificate should have Code Signing EKU");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FulcioCertificate_HasDigitalSignatureKeyUsage()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -173,7 +180,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(keyUsage.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FulcioCertificate_IsShortLived()
|
||||
{
|
||||
// Arrange - Fulcio certs are typically valid for ~20 minutes
|
||||
@@ -186,7 +194,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(validity.TotalHours <= 24, $"Certificate validity ({validity.TotalHours}h) should be <= 24 hours");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BundleWithCertificate_HasValidPemFormat()
|
||||
{
|
||||
// Arrange
|
||||
@@ -207,7 +216,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
// DSSE-8200-015: Rekor transparency log offline verification tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RekorEntry_HasValidLogIndex()
|
||||
{
|
||||
// Arrange
|
||||
@@ -221,7 +231,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(rekorEntry.LogIndex >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RekorEntry_HasValidIntegratedTime()
|
||||
{
|
||||
// Arrange
|
||||
@@ -238,7 +249,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(integratedTime >= now.AddHours(-1), "Integrated time should not be too old");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RekorEntry_HasValidInclusionProof()
|
||||
{
|
||||
// Arrange
|
||||
@@ -256,7 +268,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.NotEmpty(rekorEntry.InclusionProof.Hashes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RekorEntry_CanonicalizedBody_IsBase64Encoded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -276,7 +289,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.NotNull(json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RekorEntry_InclusionProof_HashesAreBase64()
|
||||
{
|
||||
// Arrange
|
||||
@@ -294,7 +308,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BundleWithRekor_ContainsValidTransparencyEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -310,7 +325,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(bundle.RekorEntry.LogIndex >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RekorEntry_CheckpointFormat_IsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -329,7 +345,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
// Integration tests
|
||||
// ==========================================================================
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FullBundle_SignVerifyRoundtrip_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -349,7 +366,8 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
Assert.True(structureResult.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DeterministicSigning_SamePayload_ProducesConsistentEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -366,6 +384,7 @@ public sealed class DsseCosignCompatibilityTests : IDisposable
|
||||
// Note: Signatures may differ if using randomized ECDSA
|
||||
// (which is the default for security), so we only verify structure
|
||||
Assert.Equal(envelope1.Signatures.Count, envelope2.Signatures.Count);
|
||||
using StellaOps.TestKit;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
@@ -6,13 +6,16 @@ using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using EnvelopeModel = StellaOps.Attestor.Envelope;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
public sealed class DsseEnvelopeSerializerTests
|
||||
{
|
||||
private static readonly byte[] SamplePayload = Encoding.UTF8.GetBytes("deterministic-dsse-payload");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_ProducesDeterministicCompactJson_ForSignaturePermutations()
|
||||
{
|
||||
var signatures = new[]
|
||||
|
||||
@@ -7,6 +7,8 @@ using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
public sealed class EnvelopeSignatureServiceTests
|
||||
@@ -23,7 +25,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
|
||||
private readonly EnvelopeSignatureService service = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignAndVerify_Ed25519_Succeeds()
|
||||
{
|
||||
var signingKey = EnvelopeKey.CreateEd25519Signer(Ed25519Seed, Ed25519Public);
|
||||
@@ -44,7 +47,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
signingKey.KeyId.Should().Be(expectedKeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_Ed25519_InvalidSignature_ReturnsError()
|
||||
{
|
||||
var signingKey = EnvelopeKey.CreateEd25519Signer(Ed25519Seed, Ed25519Public);
|
||||
@@ -62,7 +66,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
verifyResult.Error.Code.Should().Be(EnvelopeSignatureErrorCode.SignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignAndVerify_EcdsaEs256_Succeeds()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
@@ -80,7 +85,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
verifyResult.Value.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sign_WithVerificationOnlyKey_ReturnsMissingPrivateKey()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
@@ -93,7 +99,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
signResult.Error.Code.Should().Be(EnvelopeSignatureErrorCode.MissingPrivateKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_WithMismatchedKeyId_ReturnsError()
|
||||
{
|
||||
var signingKey = EnvelopeKey.CreateEd25519Signer(Ed25519Seed, Ed25519Public);
|
||||
@@ -107,7 +114,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
verifyResult.Error.Code.Should().Be(EnvelopeSignatureErrorCode.KeyIdMismatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_WithInvalidSignatureLength_ReturnsFormatError()
|
||||
{
|
||||
var verifyKey = EnvelopeKey.CreateEd25519Verifier(Ed25519Public);
|
||||
@@ -119,7 +127,8 @@ public sealed class EnvelopeSignatureServiceTests
|
||||
verifyResult.Error.Code.Should().Be(EnvelopeSignatureErrorCode.InvalidSignatureFormat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Verify_WithAlgorithmMismatch_ReturnsError()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -7,11 +7,14 @@ using System.Text.Json;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Envelope.Tests;
|
||||
|
||||
public sealed class DsseEnvelopeSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_WithDefaultOptions_ProducesCompactAndExpandedJson()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("{\"foo\":\"bar\"}");
|
||||
@@ -46,7 +49,8 @@ public sealed class DsseEnvelopeSerializerTests
|
||||
Assert.Equal("bar", preview.GetProperty("json").GetProperty("foo").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_WithCompressionEnabled_EmbedsCompressedPayloadMetadata()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("{\"foo\":\"bar\",\"count\":1}");
|
||||
@@ -87,7 +91,8 @@ public sealed class DsseEnvelopeSerializerTests
|
||||
Assert.Equal(compressedBytes.Length, result.EmbeddedPayloadLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_WithDetachedReference_WritesMetadata()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("detached payload preview");
|
||||
@@ -117,7 +122,8 @@ public sealed class DsseEnvelopeSerializerTests
|
||||
Assert.Equal(reference.MediaType, detached.GetProperty("mediaType").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_CompactOnly_SkipsExpandedPayload()
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes("payload");
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\..\\StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="../../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Attestor.Core.Tests.Fixtures.Rekor;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Core.Tests;
|
||||
|
||||
public sealed class RekorOfflineReceiptVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidReceipt_Succeeds()
|
||||
{
|
||||
var (directory, receiptPath) = CreateTempReceipt(RekorOfflineReceiptFixtures.ReceiptJson);
|
||||
@@ -33,7 +35,8 @@ public sealed class RekorOfflineReceiptVerifierTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_CheckpointPathReference_Succeeds()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), "stellaops-attestor-rekor-offline-" + Guid.NewGuid().ToString("n"));
|
||||
@@ -62,7 +65,8 @@ public sealed class RekorOfflineReceiptVerifierTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_TamperedCheckpointSignature_Fails()
|
||||
{
|
||||
var tampered = MutateReceiptJson(root =>
|
||||
@@ -90,7 +94,8 @@ public sealed class RekorOfflineReceiptVerifierTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_RootHashMismatch_Fails()
|
||||
{
|
||||
var badJson = MutateReceiptJson(root => root["rootHash"] = new string('0', 64));
|
||||
@@ -114,7 +119,8 @@ public sealed class RekorOfflineReceiptVerifierTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_AllowOfflineWithoutSignature_AllowsUnsignedCheckpoint()
|
||||
{
|
||||
var checkpointBodyOnly = RekorOfflineReceiptFixtures.SignedCheckpointNote.Split("\n\n", StringSplitOptions.None)[0] + "\n";
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -39,7 +39,8 @@ namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestationBundleEndpointsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportEndpoint_RequiresAuthentication()
|
||||
{
|
||||
using var factory = new AttestorWebApplicationFactory();
|
||||
@@ -50,7 +51,8 @@ public sealed class AttestationBundleEndpointsTests
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAndImportEndpoints_RoundTripBundles()
|
||||
{
|
||||
using var factory = new AttestorWebApplicationFactory();
|
||||
@@ -64,6 +66,7 @@ public sealed class AttestationBundleEndpointsTests
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var repository = scope.ServiceProvider.GetRequiredService<IAttestorEntryRepository>();
|
||||
using StellaOps.TestKit;
|
||||
var archiveStore = scope.ServiceProvider.GetRequiredService<IAttestorArchiveStore>();
|
||||
|
||||
var entry = new AttestorEntry
|
||||
|
||||
@@ -8,11 +8,13 @@ using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.WebService.Contracts;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestationQueryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryAsync_FiltersAndPagination_Work()
|
||||
{
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
@@ -83,7 +85,8 @@ public sealed class AttestationQueryTests
|
||||
Assert.All(secondPage.Items, item => Assert.DoesNotContain(item.RekorUuid, firstPageIds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryBuildQuery_ValidatesInputs()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
@@ -5,11 +5,13 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorEntryRepositoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task QueryAsync_FiltersAndPagination_Work()
|
||||
{
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
@@ -53,7 +55,8 @@ public sealed class AttestorEntryRepositoryTests
|
||||
Assert.All(secondPage.Items, item => Assert.DoesNotContain(item.RekorUuid, seen));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_EnforcesUniqueBundleSha()
|
||||
{
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
|
||||
@@ -21,6 +21,8 @@ using Org.BouncyCastle.Crypto.Signers;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
[Collection("SmSoftGate")]
|
||||
@@ -28,7 +30,8 @@ public sealed class AttestorSigningServiceTests : IDisposable
|
||||
{
|
||||
private readonly List<string> _temporaryPaths = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_Ed25519Key_ReturnsValidSignature()
|
||||
{
|
||||
var privateKey = new byte[32];
|
||||
@@ -110,7 +113,8 @@ public sealed class AttestorSigningServiceTests : IDisposable
|
||||
Assert.Equal("signed", auditSink.Records[0].Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_KmsKey_ProducesVerifiableSignature()
|
||||
{
|
||||
var kmsRoot = CreateTempDirectory();
|
||||
@@ -215,7 +219,8 @@ public sealed class AttestorSigningServiceTests : IDisposable
|
||||
Assert.Equal("signed", auditSink.Records[0].Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAsync_Sm2Key_ReturnsValidSignature_WhenGateEnabled()
|
||||
{
|
||||
var originalGate = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
|
||||
@@ -312,7 +317,8 @@ public sealed class AttestorSigningServiceTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Sm2Registry_Fails_WhenGateDisabled()
|
||||
{
|
||||
var originalGate = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.Infrastructure.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorStorageTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_PersistsAndFetchesEntry()
|
||||
{
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
@@ -27,7 +29,8 @@ public sealed class AttestorStorageTests
|
||||
Assert.Single(byArtifact);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_UpsertsExistingDocument()
|
||||
{
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
@@ -47,7 +50,8 @@ public sealed class AttestorStorageTests
|
||||
Assert.Equal("pending", stored!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task InMemoryDedupeStore_RoundTripsAndExpires()
|
||||
{
|
||||
var store = new InMemoryAttestorDedupeStore();
|
||||
|
||||
@@ -17,11 +17,14 @@ using StellaOps.Attestor.Infrastructure.Submission;
|
||||
using StellaOps.Attestor.Tests.Support;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorSubmissionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ReturnsDeterministicUuid_OnDuplicateBundle()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -92,7 +95,8 @@ public sealed class AttestorSubmissionServiceTests
|
||||
Assert.Equal(request.Meta.Artifact.Sha256, verificationCache.InvalidatedSubjects[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Validator_ThrowsWhenModeNotAllowed()
|
||||
{
|
||||
var canonicalizer = new DefaultDsseCanonicalizer();
|
||||
@@ -104,7 +108,8 @@ public sealed class AttestorSubmissionServiceTests
|
||||
await Assert.ThrowsAsync<AttestorValidationException>(() => validator.ValidateAsync(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_Throws_WhenMirrorDisabledButRequested()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -163,7 +168,8 @@ public sealed class AttestorSubmissionServiceTests
|
||||
Assert.Equal("mirror_disabled", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ReturnsMirrorMetadata_WhenPreferenceBoth()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -233,7 +239,8 @@ public sealed class AttestorSubmissionServiceTests
|
||||
Assert.Equal("included", result.Mirror.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_UsesMirrorAsCanonical_WhenPreferenceMirror()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
|
||||
@@ -7,13 +7,15 @@ using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Infrastructure.Submission;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorSubmissionValidatorHardeningTests
|
||||
{
|
||||
private static readonly DefaultDsseCanonicalizer Canonicalizer = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ThrowsWhenPayloadExceedsLimit()
|
||||
{
|
||||
var constraints = new AttestorSubmissionConstraints(
|
||||
@@ -28,7 +30,8 @@ public sealed class AttestorSubmissionValidatorHardeningTests
|
||||
Assert.Equal("payload_too_large", exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ThrowsWhenCertificateChainTooLong()
|
||||
{
|
||||
var constraints = new AttestorSubmissionConstraints(
|
||||
@@ -43,7 +46,8 @@ public sealed class AttestorSubmissionValidatorHardeningTests
|
||||
Assert.Equal("certificate_chain_too_long", exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ValidateAsync_FuzzedInputs_DoNotCrash()
|
||||
{
|
||||
var constraints = new AttestorSubmissionConstraints();
|
||||
|
||||
@@ -22,6 +22,8 @@ using StellaOps.Attestor.Verify;
|
||||
using StellaOps.Attestor.Tests.Support;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class AttestorVerificationServiceTests
|
||||
@@ -29,7 +31,8 @@ public sealed class AttestorVerificationServiceTests
|
||||
private static readonly byte[] HmacSecret = Encoding.UTF8.GetBytes("attestor-hmac-secret");
|
||||
private static readonly string HmacSecretBase64 = Convert.ToBase64String(HmacSecret);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsOk_ForExistingUuid()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -122,7 +125,8 @@ public sealed class AttestorVerificationServiceTests
|
||||
Assert.Equal("missing", verifyResult.Report.Transparency.WitnessStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_KmsBundle_Passes_WhenTwoSignaturesRequired()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -213,7 +217,8 @@ public sealed class AttestorVerificationServiceTests
|
||||
Assert.Equal(2, verifyResult.Report.Signatures.RequiredSignatures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_FlagsTamperedBundle()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -426,7 +431,8 @@ public sealed class AttestorVerificationServiceTests
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_OfflineSkipsProofRefreshWhenMissing()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -490,7 +496,8 @@ public sealed class AttestorVerificationServiceTests
|
||||
Assert.Equal(0, rekorClient.ProofRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_OfflineUsesImportedProof()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
@@ -577,7 +584,8 @@ public sealed class AttestorVerificationServiceTests
|
||||
Assert.Equal(0, rekorClient.ProofRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_FailsWhenWitnessRootMismatch()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.WebService.Contracts;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class BulkVerificationContractsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryBuildJob_ReturnsError_WhenItemsMissing()
|
||||
{
|
||||
var options = new AttestorOptions();
|
||||
@@ -22,7 +24,8 @@ public sealed class BulkVerificationContractsTests
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryBuildJob_AppliesDefaults()
|
||||
{
|
||||
var options = new AttestorOptions
|
||||
|
||||
@@ -11,11 +11,14 @@ using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Bulk;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class BulkVerificationWorkerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProcessJobAsync_CompletesAllItems()
|
||||
{
|
||||
var jobStore = new InMemoryBulkVerificationJobStore();
|
||||
|
||||
@@ -10,11 +10,14 @@ using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Verification;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class CachedAttestorVerificationServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ReturnsCachedResult_OnRepeatedCalls()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions());
|
||||
@@ -44,7 +47,8 @@ public sealed class CachedAttestorVerificationServiceTests
|
||||
Assert.Equal(1, inner.VerifyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_BypassesCache_WhenRefreshProofRequested()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions());
|
||||
@@ -75,7 +79,8 @@ public sealed class CachedAttestorVerificationServiceTests
|
||||
Assert.Equal(2, inner.VerifyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_BypassesCache_WhenDescriptorIncomplete()
|
||||
{
|
||||
var options = Options.Create(new AttestorOptions());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -19,7 +20,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
|
||||
private const string InvalidFormatCheckpoint = "not a valid checkpoint";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseCheckpoint_ValidFormat_ExtractsFields()
|
||||
{
|
||||
// Act
|
||||
@@ -32,7 +34,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
Assert.NotNull(result.RootHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseCheckpoint_InvalidFormat_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
@@ -43,7 +46,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
Assert.Contains("Invalid", result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseCheckpoint_EmptyString_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
@@ -54,7 +58,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
Assert.NotNull(result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseCheckpoint_MinimalValidFormat_ExtractsFields()
|
||||
{
|
||||
// Arrange - minimal checkpoint without timestamp
|
||||
@@ -74,7 +79,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
Assert.Equal(32, result.RootHash!.Length); // SHA-256 hash
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseCheckpoint_InvalidBase64Root_ReturnsFailure()
|
||||
{
|
||||
// Arrange - invalid base64 in root hash
|
||||
@@ -92,7 +98,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
Assert.Contains("Invalid root hash", result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseCheckpoint_InvalidTreeSize_ReturnsFailure()
|
||||
{
|
||||
// Arrange - non-numeric tree size
|
||||
@@ -110,7 +117,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
Assert.Contains("Invalid tree size", result.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_NullCheckpoint_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -118,7 +126,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
CheckpointSignatureVerifier.VerifyCheckpoint(null!, [], []));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_NullSignature_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -126,7 +135,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
CheckpointSignatureVerifier.VerifyCheckpoint("checkpoint", null!, []));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_NullPublicKey_ThrowsArgumentNull()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -134,7 +144,8 @@ public sealed class CheckpointSignatureVerifierTests
|
||||
CheckpointSignatureVerifier.VerifyCheckpoint("checkpoint", [], null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyCheckpoint_InvalidFormat_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -11,11 +11,13 @@ using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Infrastructure.Rekor;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class HttpRekorClientTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ParsesResponse()
|
||||
{
|
||||
var payload = new
|
||||
@@ -65,7 +67,8 @@ public sealed class HttpRekorClientTests
|
||||
Assert.Equal("leaf", response.Proof!.Inclusion!.LeafHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_ThrowsOnConflict()
|
||||
{
|
||||
var client = CreateClient(HttpStatusCode.Conflict, new { error = "duplicate" });
|
||||
@@ -96,7 +99,8 @@ public sealed class HttpRekorClientTests
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => rekorClient.SubmitAsync(request, backend));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetProofAsync_ReturnsNullOnNotFound()
|
||||
{
|
||||
var client = CreateClient(HttpStatusCode.NotFound, new { });
|
||||
|
||||
@@ -13,11 +13,14 @@ using StellaOps.Attestor.Core.Transparency;
|
||||
using StellaOps.Attestor.Infrastructure.Transparency;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class HttpTransparencyWitnessClientTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetObservationAsync_CachesSuccessfulResponses()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler(_ =>
|
||||
@@ -78,7 +81,8 @@ public sealed class HttpTransparencyWitnessClientTests
|
||||
Assert.Equal(1, handler.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetObservationAsync_ReturnsErrorObservation_OnNonSuccess()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.BadGateway));
|
||||
@@ -121,7 +125,8 @@ public sealed class HttpTransparencyWitnessClientTests
|
||||
Assert.Equal(1, handler.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetObservationAsync_ReturnsCachedErrorObservation_OnException()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler(_ => throw new HttpRequestException("boom"));
|
||||
|
||||
@@ -8,13 +8,15 @@ using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Infrastructure.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class LiveDedupeStoreTests
|
||||
{
|
||||
private const string Category = "LiveTTL";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Redis_dedupe_entry_sets_time_to_live()
|
||||
{
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class MerkleProofVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashLeaf_ProducesDeterministicHash()
|
||||
{
|
||||
var data = "test data"u8.ToArray();
|
||||
@@ -17,7 +19,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal(32, hash1.Length); // SHA-256 produces 32 bytes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashLeaf_IncludesLeafPrefix()
|
||||
{
|
||||
var data = Array.Empty<byte>();
|
||||
@@ -29,7 +32,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal(32, hash.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashInterior_ProducesDeterministicHash()
|
||||
{
|
||||
var left = new byte[] { 1, 2, 3 };
|
||||
@@ -41,7 +45,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HashInterior_OrderMatters()
|
||||
{
|
||||
var a = new byte[] { 1, 2, 3 };
|
||||
@@ -53,7 +58,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.NotEqual(hashAB, hashBA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_SingleLeafTree_Succeeds()
|
||||
{
|
||||
var leafData = "single leaf"u8.ToArray();
|
||||
@@ -70,7 +76,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.True(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_LeftLeaf_Succeeds()
|
||||
{
|
||||
var leaf0Data = "leaf 0"u8.ToArray();
|
||||
@@ -91,7 +98,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.True(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_RightLeaf_Succeeds()
|
||||
{
|
||||
var leaf0Data = "leaf 0"u8.ToArray();
|
||||
@@ -112,7 +120,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.True(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_InvalidLeafHash_Fails()
|
||||
{
|
||||
var leaf0Data = "leaf 0"u8.ToArray();
|
||||
@@ -135,7 +144,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_WrongRootHash_Fails()
|
||||
{
|
||||
var leaf0Hash = MerkleProofVerifier.HashLeaf("leaf 0"u8.ToArray());
|
||||
@@ -152,7 +162,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_InvalidIndex_Fails()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
@@ -168,7 +179,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_NegativeIndex_Fails()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
@@ -183,7 +195,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_ZeroTreeSize_Fails()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
@@ -198,7 +211,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HexToBytes_ConvertsCorrectly()
|
||||
{
|
||||
var hex = "0102030405";
|
||||
@@ -209,7 +223,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HexToBytes_Handles0xPrefix()
|
||||
{
|
||||
var hex = "0x0102030405";
|
||||
@@ -220,7 +235,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BytesToHex_ConvertsCorrectly()
|
||||
{
|
||||
var bytes = new byte[] { 0xAB, 0xCD, 0xEF };
|
||||
@@ -230,7 +246,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal("abcdef", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_WithEmptyPath_ReturnsSingleLeaf()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
@@ -245,7 +262,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Equal(leafHash, root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_WithEmptyPath_NonSingleTree_ReturnsNull()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
@@ -259,7 +277,8 @@ public sealed class MerkleProofVerifierTests
|
||||
Assert.Null(root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_FourLeafTree_AllPositions()
|
||||
{
|
||||
// Build a 4-leaf tree manually
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Text.Json;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -35,7 +37,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
""",
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_SingleLeafTree_Succeeds()
|
||||
{
|
||||
// Arrange - single leaf tree (tree size = 1)
|
||||
@@ -54,7 +57,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_LeftLeaf_Succeeds()
|
||||
{
|
||||
// Arrange - two-leaf tree, verify left leaf
|
||||
@@ -78,7 +82,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_RightLeaf_Succeeds()
|
||||
{
|
||||
// Arrange - two-leaf tree, verify right leaf
|
||||
@@ -102,7 +107,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_FourLeafTree_AllPositions_Succeed()
|
||||
{
|
||||
// Arrange - four-leaf balanced tree
|
||||
@@ -147,7 +153,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_WrongLeafHash_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +179,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_WrongRootHash_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -195,7 +203,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_InvalidLeafIndex_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -214,7 +223,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_NegativeLeafIndex_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -233,7 +243,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyInclusion_ZeroTreeSize_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -252,7 +263,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_EmptyProof_SingleLeaf_ReturnsLeafHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -271,7 +283,8 @@ public sealed class RekorInclusionVerificationIntegrationTests
|
||||
Assert.Equal(leaf, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_EmptyProof_MultiLeaf_ReturnsNull()
|
||||
{
|
||||
// Arrange - empty proof for multi-leaf tree is invalid
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Verify\StellaOps.Attestor.Verify.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -22,13 +22,15 @@ using StellaOps.Attestor.Infrastructure.Verification;
|
||||
using StellaOps.Attestor.Verify;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class TimeSkewValidationIntegrationTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedNow = new(2025, 12, 18, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_WhenSkewRejected_Throws_WhenFailOnRejectEnabled()
|
||||
{
|
||||
var options = CreateOptions(new TimeSkewOptions
|
||||
@@ -52,7 +54,8 @@ public sealed class TimeSkewValidationIntegrationTests
|
||||
await Assert.ThrowsAsync<TimeSkewValidationException>(() => submissionService.SubmitAsync(request, context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SubmitAsync_WhenSkewRejected_Succeeds_WhenFailOnRejectDisabled()
|
||||
{
|
||||
var options = CreateOptions(new TimeSkewOptions
|
||||
@@ -77,7 +80,8 @@ public sealed class TimeSkewValidationIntegrationTests
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.Uuid));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WhenSkewRejected_ReturnsFailed_WhenFailOnRejectEnabled()
|
||||
{
|
||||
var options = CreateOptions(new TimeSkewOptions
|
||||
@@ -139,7 +143,8 @@ public sealed class TimeSkewValidationIntegrationTests
|
||||
Assert.Contains(result.Issues, issue => issue.StartsWith("time_skew_rejected:", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_WhenSkewRejected_DoesNotFail_WhenFailOnRejectDisabled()
|
||||
{
|
||||
var options = CreateOptions(new TimeSkewOptions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public class TimeSkewValidatorTests
|
||||
@@ -14,7 +15,8 @@ public class TimeSkewValidatorTests
|
||||
FailOnReject = true
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhenDisabled_ReturnsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
@@ -31,7 +33,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Contains("disabled", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WhenNoIntegratedTime_ReturnsSkipped()
|
||||
{
|
||||
// Arrange
|
||||
@@ -46,7 +49,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Contains("No integrated time", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(0)] // No skew
|
||||
[InlineData(5)] // 5 seconds ago
|
||||
[InlineData(30)] // 30 seconds ago
|
||||
@@ -67,7 +71,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.InRange(result.SkewSeconds, secondsAgo - 1, secondsAgo + 1);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(60)] // At warn threshold
|
||||
[InlineData(120)] // 2 minutes
|
||||
[InlineData(299)] // Just under reject threshold
|
||||
@@ -87,7 +92,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Contains("warning threshold", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(300)] // At reject threshold
|
||||
[InlineData(600)] // 10 minutes
|
||||
[InlineData(3600)] // 1 hour
|
||||
@@ -107,7 +113,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Contains("rejection threshold", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(5)] // 5 seconds in future (OK)
|
||||
[InlineData(30)] // 30 seconds in future (OK)
|
||||
[InlineData(60)] // At max future threshold (OK)
|
||||
@@ -127,7 +134,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.True(result.SkewSeconds < 0); // Negative means future
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(61)] // Just over max future
|
||||
[InlineData(120)] // 2 minutes in future
|
||||
[InlineData(3600)] // 1 hour in future
|
||||
@@ -147,7 +155,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Contains("Future timestamp", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_UsesCurrentTimeWhenLocalTimeNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
@@ -162,7 +171,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.InRange(result.SkewSeconds, 9, 12); // Allow for test execution time
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_CustomThresholds_AreRespected()
|
||||
{
|
||||
// Arrange
|
||||
@@ -184,7 +194,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Equal(TimeSkewStatus.Warning, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_ReturnsCorrectTimestamps()
|
||||
{
|
||||
// Arrange
|
||||
@@ -201,7 +212,8 @@ public class TimeSkewValidatorTests
|
||||
Assert.Equal(30, result.SkewSeconds, precision: 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ThrowsOnNullOptions()
|
||||
{
|
||||
// Act & Assert
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-preview.*" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0-preview.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BinaryFingerprintEvidenceGenerator.cs
|
||||
// Sprint: SPRINT_20251226_014_BINIDX
|
||||
// Task: SCANINT-11 — Implement proof segment generation in Attestor
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.ProofChain.Models;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
using StellaOps.Canonical.Json;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Generators;
|
||||
|
||||
/// <summary>
|
||||
/// Generates binary fingerprint evidence proof segments for scanner findings.
|
||||
/// Creates attestable evidence of binary vulnerability matches.
|
||||
/// </summary>
|
||||
public sealed class BinaryFingerprintEvidenceGenerator
|
||||
{
|
||||
private const string ToolId = "stellaops.binaryindex";
|
||||
private const string ToolVersion = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Generate a proof segment from binary vulnerability findings.
|
||||
/// </summary>
|
||||
public ProofBlob Generate(BinaryFingerprintEvidencePredicate predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
var predicateJson = JsonSerializer.SerializeToDocument(predicate, GetJsonOptions());
|
||||
var dataHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(predicateJson));
|
||||
|
||||
// Create subject ID from binary key and scan context
|
||||
var subjectId = $"binary:{predicate.BinaryIdentity.BinaryKey}";
|
||||
if (predicate.ScanContext is not null)
|
||||
{
|
||||
subjectId = $"{predicate.ScanContext.ScanId}:{subjectId}";
|
||||
}
|
||||
|
||||
// Create evidence entry for each match
|
||||
var evidences = new List<ProofEvidence>();
|
||||
foreach (var match in predicate.Matches)
|
||||
{
|
||||
var matchData = JsonSerializer.SerializeToDocument(match, GetJsonOptions());
|
||||
var matchHash = CanonJson.Sha256Prefixed(CanonJson.Canonicalize(matchData));
|
||||
|
||||
evidences.Add(new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:binary:{predicate.BinaryIdentity.BinaryKey}:{match.CveId}",
|
||||
Type = EvidenceType.BinaryFingerprint,
|
||||
Source = match.Method,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = matchData,
|
||||
DataHash = matchHash
|
||||
});
|
||||
}
|
||||
|
||||
// Determine proof type based on matches
|
||||
var proofType = DetermineProofType(predicate.Matches);
|
||||
var confidence = ComputeAggregateConfidence(predicate.Matches);
|
||||
|
||||
var proof = new ProofBlob
|
||||
{
|
||||
ProofId = "", // Will be computed by ProofHashing.WithHash
|
||||
SubjectId = subjectId,
|
||||
Type = proofType,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = evidences,
|
||||
Method = "binary_fingerprint_evidence",
|
||||
Confidence = confidence,
|
||||
ToolVersion = ToolVersion,
|
||||
SnapshotId = GenerateSnapshotId()
|
||||
};
|
||||
|
||||
return ProofHashing.WithHash(proof);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate proof segments for multiple binary findings in batch.
|
||||
/// </summary>
|
||||
public ImmutableArray<ProofBlob> GenerateBatch(
|
||||
IEnumerable<BinaryFingerprintEvidencePredicate> predicates)
|
||||
{
|
||||
var results = new List<ProofBlob>();
|
||||
|
||||
foreach (var predicate in predicates)
|
||||
{
|
||||
results.Add(Generate(predicate));
|
||||
}
|
||||
|
||||
return results.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a BinaryFingerprintEvidencePredicate from scan findings.
|
||||
/// </summary>
|
||||
public static BinaryFingerprintEvidencePredicate CreatePredicate(
|
||||
BinaryIdentityInfo identity,
|
||||
string layerDigest,
|
||||
IEnumerable<BinaryVulnMatchInfo> matches,
|
||||
ScanContextInfo? scanContext = null)
|
||||
{
|
||||
return new BinaryFingerprintEvidencePredicate
|
||||
{
|
||||
BinaryIdentity = identity,
|
||||
LayerDigest = layerDigest,
|
||||
Matches = matches.ToImmutableArray(),
|
||||
ScanContext = scanContext
|
||||
};
|
||||
}
|
||||
|
||||
private static ProofBlobType DetermineProofType(ImmutableArray<BinaryVulnMatchInfo> matches)
|
||||
{
|
||||
if (matches.IsDefaultOrEmpty)
|
||||
{
|
||||
return ProofBlobType.Unknown;
|
||||
}
|
||||
|
||||
// Check if all matches have fix status indicating fixed
|
||||
var allFixed = matches.All(m =>
|
||||
m.FixStatus?.State?.Equals("fixed", StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
if (allFixed)
|
||||
{
|
||||
return ProofBlobType.BackportFixed;
|
||||
}
|
||||
|
||||
// Check if any match is vulnerable
|
||||
var anyVulnerable = matches.Any(m =>
|
||||
m.FixStatus?.State?.Equals("vulnerable", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
m.FixStatus is null);
|
||||
|
||||
if (anyVulnerable)
|
||||
{
|
||||
return ProofBlobType.Vulnerable;
|
||||
}
|
||||
|
||||
// Check for not_affected
|
||||
var allNotAffected = matches.All(m =>
|
||||
m.FixStatus?.State?.Equals("not_affected", StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
if (allNotAffected)
|
||||
{
|
||||
return ProofBlobType.NotAffected;
|
||||
}
|
||||
|
||||
return ProofBlobType.Unknown;
|
||||
}
|
||||
|
||||
private static double ComputeAggregateConfidence(ImmutableArray<BinaryVulnMatchInfo> matches)
|
||||
{
|
||||
if (matches.IsDefaultOrEmpty)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Use average confidence, weighted by match method
|
||||
var weightedSum = 0.0;
|
||||
var totalWeight = 0.0;
|
||||
|
||||
foreach (var match in matches)
|
||||
{
|
||||
var methodWeight = match.Method switch
|
||||
{
|
||||
"buildid_catalog" => 1.0,
|
||||
"fingerprint_match" => 0.8,
|
||||
"range_match" => 0.6,
|
||||
_ => 0.5
|
||||
};
|
||||
|
||||
weightedSum += (double)match.Confidence * methodWeight;
|
||||
totalWeight += methodWeight;
|
||||
}
|
||||
|
||||
return totalWeight > 0 ? Math.Min(weightedSum / totalWeight, 0.98) : 0.0;
|
||||
}
|
||||
|
||||
private static string GenerateSnapshotId()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToString("yyyyMMdd-HHmmss") + "-UTC";
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions GetJsonOptions()
|
||||
{
|
||||
return new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BinaryFingerprintEvidencePredicate.cs
|
||||
// Sprint: SPRINT_20251226_014_BINIDX
|
||||
// Task: SCANINT-10 — Create binary_fingerprint_evidence proof segment type
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
/// <summary>
|
||||
/// Predicate for binary fingerprint evidence proof segment.
|
||||
/// Contains evidence of binary vulnerability matches with fingerprint and fix status.
|
||||
/// Schema version: 1.0.0
|
||||
/// </summary>
|
||||
public sealed record BinaryFingerprintEvidencePredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Predicate type URI.
|
||||
/// </summary>
|
||||
public const string PredicateType = "https://stellaops.dev/predicates/binary-fingerprint-evidence@v1";
|
||||
|
||||
/// <summary>
|
||||
/// Schema version for this predicate format.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = "1.0.0";
|
||||
|
||||
/// <summary>
|
||||
/// Binary identity information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("binary_identity")]
|
||||
public required BinaryIdentityInfo BinaryIdentity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Layer digest where binary was found.
|
||||
/// </summary>
|
||||
[JsonPropertyName("layer_digest")]
|
||||
public required string LayerDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability matches for this binary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("matches")]
|
||||
public required ImmutableArray<BinaryVulnMatchInfo> Matches { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan context metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scan_context")]
|
||||
public ScanContextInfo? ScanContext { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary identity information.
|
||||
/// </summary>
|
||||
public sealed record BinaryIdentityInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Binary format (elf, pe, macho).
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
public required string Format { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// GNU Build-ID if available.
|
||||
/// </summary>
|
||||
[JsonPropertyName("build_id")]
|
||||
public string? BuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 hash of the binary file.
|
||||
/// </summary>
|
||||
[JsonPropertyName("file_sha256")]
|
||||
public required string FileSha256 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target architecture (x86_64, aarch64, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("architecture")]
|
||||
public required string Architecture { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary key for lookups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("binary_key")]
|
||||
public required string BinaryKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path within the container filesystem.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability match information.
|
||||
/// </summary>
|
||||
public sealed record BinaryVulnMatchInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cve_id")]
|
||||
public required string CveId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Match method (buildid_catalog, fingerprint_match, range_match).
|
||||
/// </summary>
|
||||
[JsonPropertyName("method")]
|
||||
public required string Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Match confidence score (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required decimal Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerable package PURL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vulnerable_purl")]
|
||||
public required string VulnerablePurl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Fix status if known.
|
||||
/// </summary>
|
||||
[JsonPropertyName("fix_status")]
|
||||
public FixStatusInfo? FixStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Similarity score if fingerprint match.
|
||||
/// </summary>
|
||||
[JsonPropertyName("similarity")]
|
||||
public decimal? Similarity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Matched function name if available.
|
||||
/// </summary>
|
||||
[JsonPropertyName("matched_function")]
|
||||
public string? MatchedFunction { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix status information from distro backport detection.
|
||||
/// </summary>
|
||||
public sealed record FixStatusInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Fix state (fixed, vulnerable, not_affected, wontfix, unknown).
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public required string State { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version where fix was applied.
|
||||
/// </summary>
|
||||
[JsonPropertyName("fixed_version")]
|
||||
public string? FixedVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detection method (changelog, patch_analysis, advisory).
|
||||
/// </summary>
|
||||
[JsonPropertyName("method")]
|
||||
public required string Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence in the fix status (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required decimal Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scan context metadata.
|
||||
/// </summary>
|
||||
public sealed record ScanContextInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Scan identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scan_id")]
|
||||
public required string ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Container image reference.
|
||||
/// </summary>
|
||||
[JsonPropertyName("image_ref")]
|
||||
public string? ImageRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Container image digest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("image_digest")]
|
||||
public string? ImageDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detected distribution.
|
||||
/// </summary>
|
||||
[JsonPropertyName("distro")]
|
||||
public string? Distro { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detected distribution release.
|
||||
/// </summary>
|
||||
[JsonPropertyName("release")]
|
||||
public string? Release { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan timestamp (UTC ISO-8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("scanned_at")]
|
||||
public required string ScannedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.ProofChain.Predicates.AI;
|
||||
using StellaOps.Attestor.ProofChain.MediaTypes;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Verification;
|
||||
|
||||
/// <summary>
|
||||
/// Verification step for AI-generated artifacts within proof bundles.
|
||||
/// Verifies authority classification, model identifiers, determinism, and evidence backing.
|
||||
/// Sprint: SPRINT_20251226_018_AI_attestations
|
||||
/// Task: AIATTEST-21
|
||||
/// </summary>
|
||||
public sealed class AIArtifactVerificationStep : IVerificationStep
|
||||
{
|
||||
private readonly IProofBundleStore _proofStore;
|
||||
private readonly IAIEvidenceResolver? _evidenceResolver;
|
||||
private readonly AIAuthorityThresholds _thresholds;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public string Name => "ai_artifact";
|
||||
|
||||
public AIArtifactVerificationStep(
|
||||
IProofBundleStore proofStore,
|
||||
ILogger logger,
|
||||
IAIEvidenceResolver? evidenceResolver = null,
|
||||
AIAuthorityThresholds? thresholds = null)
|
||||
{
|
||||
_proofStore = proofStore ?? throw new ArgumentNullException(nameof(proofStore));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_evidenceResolver = evidenceResolver;
|
||||
_thresholds = thresholds ?? new AIAuthorityThresholds();
|
||||
}
|
||||
|
||||
public async Task<VerificationStepResult> ExecuteAsync(
|
||||
VerificationContext context,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// Get the proof bundle
|
||||
var bundle = await _proofStore.GetBundleAsync(context.ProofBundleId, ct);
|
||||
if (bundle is null)
|
||||
{
|
||||
return CreatePassedResult(stopwatch.Elapsed, "No proof bundle found, skipping AI verification");
|
||||
}
|
||||
|
||||
// Find AI artifact statements
|
||||
var aiStatements = bundle.Statements
|
||||
.Where(s => IsAIPredicateType(s.PredicateType))
|
||||
.ToList();
|
||||
|
||||
if (aiStatements.Count == 0)
|
||||
{
|
||||
// No AI artifacts to verify - pass
|
||||
return CreatePassedResult(stopwatch.Elapsed, "No AI artifacts in bundle");
|
||||
}
|
||||
|
||||
// Verify each AI artifact
|
||||
var verificationResults = new List<AIArtifactVerificationResult>();
|
||||
foreach (var statement in aiStatements)
|
||||
{
|
||||
var result = await VerifyAIArtifactAsync(statement, ct);
|
||||
verificationResults.Add(result);
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
return new VerificationStepResult
|
||||
{
|
||||
StepName = Name,
|
||||
Passed = false,
|
||||
Duration = stopwatch.Elapsed,
|
||||
ErrorMessage = result.ErrorMessage,
|
||||
Details = $"AI artifact verification failed for {statement.PredicateType}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Store verification results for downstream use
|
||||
context.SetData("aiArtifactResults", verificationResults);
|
||||
|
||||
var summary = BuildVerificationSummary(verificationResults);
|
||||
|
||||
return new VerificationStepResult
|
||||
{
|
||||
StepName = Name,
|
||||
Passed = true,
|
||||
Duration = stopwatch.Elapsed,
|
||||
Details = summary
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AI artifact verification failed with exception");
|
||||
return new VerificationStepResult
|
||||
{
|
||||
StepName = Name,
|
||||
Passed = false,
|
||||
Duration = stopwatch.Elapsed,
|
||||
ErrorMessage = $"Exception: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AIArtifactVerificationResult> VerifyAIArtifactAsync(
|
||||
ProofStatement statement,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var predicateJson = JsonSerializer.Serialize(statement.Predicate);
|
||||
|
||||
// Parse base predicate fields
|
||||
AIArtifactBasePredicate? basePredicate = null;
|
||||
try
|
||||
{
|
||||
basePredicate = statement.PredicateType switch
|
||||
{
|
||||
var t when t.Contains("explanation", StringComparison.OrdinalIgnoreCase) =>
|
||||
JsonSerializer.Deserialize<AIExplanationPredicate>(predicateJson),
|
||||
var t when t.Contains("remediation", StringComparison.OrdinalIgnoreCase) =>
|
||||
JsonSerializer.Deserialize<AIRemediationPlanPredicate>(predicateJson),
|
||||
var t when t.Contains("vexdraft", StringComparison.OrdinalIgnoreCase) =>
|
||||
JsonSerializer.Deserialize<AIVexDraftPredicate>(predicateJson),
|
||||
var t when t.Contains("policydraft", StringComparison.OrdinalIgnoreCase) =>
|
||||
JsonSerializer.Deserialize<AIPolicyDraftPredicate>(predicateJson),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = false,
|
||||
ArtifactId = "unknown",
|
||||
PredicateType = statement.PredicateType,
|
||||
ErrorMessage = $"Failed to parse AI predicate: {ex.Message}"
|
||||
};
|
||||
}
|
||||
|
||||
if (basePredicate is null)
|
||||
{
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = false,
|
||||
ArtifactId = "unknown",
|
||||
PredicateType = statement.PredicateType,
|
||||
ErrorMessage = "Unrecognized AI predicate type"
|
||||
};
|
||||
}
|
||||
|
||||
// Verify artifact ID format
|
||||
if (!IsValidArtifactId(basePredicate.ArtifactId))
|
||||
{
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = false,
|
||||
ArtifactId = basePredicate.ArtifactId,
|
||||
PredicateType = statement.PredicateType,
|
||||
ErrorMessage = "Invalid artifact ID format (expected sha256:<64-hex-chars>)"
|
||||
};
|
||||
}
|
||||
|
||||
// Verify model identifier
|
||||
if (!IsValidModelId(basePredicate.ModelId))
|
||||
{
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = false,
|
||||
ArtifactId = basePredicate.ArtifactId,
|
||||
PredicateType = statement.PredicateType,
|
||||
ErrorMessage = $"Invalid model identifier: {basePredicate.ModelId}"
|
||||
};
|
||||
}
|
||||
|
||||
// Verify determinism for replay capability
|
||||
var determinismResult = VerifyDeterminism(basePredicate.DecodingParams);
|
||||
if (!determinismResult.IsDeterministic)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"AI artifact {ArtifactId} is not deterministic: {Reason}",
|
||||
basePredicate.ArtifactId, determinismResult.Reason);
|
||||
}
|
||||
|
||||
// Verify output hash format
|
||||
if (!IsValidHash(basePredicate.OutputHash))
|
||||
{
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = false,
|
||||
ArtifactId = basePredicate.ArtifactId,
|
||||
PredicateType = statement.PredicateType,
|
||||
ErrorMessage = "Invalid output hash format"
|
||||
};
|
||||
}
|
||||
|
||||
// Verify input hashes
|
||||
foreach (var inputHash in basePredicate.InputHashes)
|
||||
{
|
||||
if (!IsValidHash(inputHash))
|
||||
{
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = false,
|
||||
ArtifactId = basePredicate.ArtifactId,
|
||||
PredicateType = statement.PredicateType,
|
||||
ErrorMessage = $"Invalid input hash format: {inputHash}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Re-classify authority to verify claimed classification
|
||||
var classifier = new AIAuthorityClassifier(_thresholds, ResolveEvidence);
|
||||
AIAuthorityClassificationResult? classificationResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
classificationResult = statement.PredicateType switch
|
||||
{
|
||||
var t when t.Contains("explanation", StringComparison.OrdinalIgnoreCase) =>
|
||||
classifier.ClassifyExplanation(JsonSerializer.Deserialize<AIExplanationPredicate>(predicateJson)!),
|
||||
var t when t.Contains("remediation", StringComparison.OrdinalIgnoreCase) =>
|
||||
classifier.ClassifyRemediationPlan(JsonSerializer.Deserialize<AIRemediationPlanPredicate>(predicateJson)!),
|
||||
var t when t.Contains("vexdraft", StringComparison.OrdinalIgnoreCase) =>
|
||||
classifier.ClassifyVexDraft(JsonSerializer.Deserialize<AIVexDraftPredicate>(predicateJson)!),
|
||||
var t when t.Contains("policydraft", StringComparison.OrdinalIgnoreCase) =>
|
||||
classifier.ClassifyPolicyDraft(JsonSerializer.Deserialize<AIPolicyDraftPredicate>(predicateJson)!),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to re-classify AI artifact {ArtifactId}", basePredicate.ArtifactId);
|
||||
}
|
||||
|
||||
// Warn if claimed authority is higher than verified
|
||||
if (classificationResult is not null &&
|
||||
basePredicate.Authority > classificationResult.Authority)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"AI artifact {ArtifactId} claims {Claimed} authority but verification shows {Actual}",
|
||||
basePredicate.ArtifactId, basePredicate.Authority, classificationResult.Authority);
|
||||
}
|
||||
|
||||
return new AIArtifactVerificationResult
|
||||
{
|
||||
IsValid = true,
|
||||
ArtifactId = basePredicate.ArtifactId,
|
||||
PredicateType = statement.PredicateType,
|
||||
ModelId = basePredicate.ModelId.ToString(),
|
||||
ClaimedAuthority = basePredicate.Authority,
|
||||
VerifiedAuthority = classificationResult?.Authority,
|
||||
QualityScore = classificationResult?.QualityScore,
|
||||
IsDeterministic = determinismResult.IsDeterministic,
|
||||
CanAutoProcess = classificationResult?.CanAutoProcess ?? false
|
||||
};
|
||||
}
|
||||
|
||||
private bool ResolveEvidence(string evidenceRef)
|
||||
{
|
||||
if (_evidenceResolver is null)
|
||||
{
|
||||
// Assume resolvable if no resolver configured
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _evidenceResolver.CanResolve(evidenceRef);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to resolve evidence ref {Ref}", evidenceRef);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAIPredicateType(string predicateType)
|
||||
{
|
||||
return predicateType.Contains("ai.", StringComparison.OrdinalIgnoreCase) ||
|
||||
predicateType.Contains("explanation", StringComparison.OrdinalIgnoreCase) ||
|
||||
predicateType.Contains("remediation", StringComparison.OrdinalIgnoreCase) ||
|
||||
predicateType.Contains("vexdraft", StringComparison.OrdinalIgnoreCase) ||
|
||||
predicateType.Contains("policydraft", StringComparison.OrdinalIgnoreCase) ||
|
||||
AIArtifactMediaTypes.IsAIArtifactMediaType(predicateType);
|
||||
}
|
||||
|
||||
private static bool IsValidArtifactId(string artifactId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(artifactId)) return false;
|
||||
if (!artifactId.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var hexPart = artifactId[7..];
|
||||
return hexPart.Length == 64 && hexPart.All(c => Uri.IsHexDigit(c));
|
||||
}
|
||||
|
||||
private static bool IsValidModelId(AIModelIdentifier modelId)
|
||||
{
|
||||
return !string.IsNullOrEmpty(modelId.Provider) &&
|
||||
!string.IsNullOrEmpty(modelId.Model) &&
|
||||
!string.IsNullOrEmpty(modelId.Version);
|
||||
}
|
||||
|
||||
private static bool IsValidHash(string hash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hash)) return false;
|
||||
|
||||
// Support sha256: and sha384: and sha512: prefixes
|
||||
var parts = hash.Split(':');
|
||||
if (parts.Length != 2) return false;
|
||||
|
||||
var algo = parts[0].ToLowerInvariant();
|
||||
var hexPart = parts[1];
|
||||
|
||||
var expectedLength = algo switch
|
||||
{
|
||||
"sha256" => 64,
|
||||
"sha384" => 96,
|
||||
"sha512" => 128,
|
||||
_ => -1
|
||||
};
|
||||
|
||||
if (expectedLength < 0) return false;
|
||||
return hexPart.Length == expectedLength && hexPart.All(c => Uri.IsHexDigit(c));
|
||||
}
|
||||
|
||||
private static (bool IsDeterministic, string? Reason) VerifyDeterminism(AIDecodingParameters decodingParams)
|
||||
{
|
||||
if (decodingParams.Temperature > 0)
|
||||
{
|
||||
return (false, $"Temperature {decodingParams.Temperature} > 0");
|
||||
}
|
||||
|
||||
if (!decodingParams.Seed.HasValue)
|
||||
{
|
||||
return (false, "No seed specified");
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
private static string BuildVerificationSummary(List<AIArtifactVerificationResult> results)
|
||||
{
|
||||
var suggestions = results.Count(r => r.ClaimedAuthority == AIArtifactAuthority.Suggestion);
|
||||
var evidenceBacked = results.Count(r => r.ClaimedAuthority == AIArtifactAuthority.EvidenceBacked);
|
||||
var authorityThreshold = results.Count(r => r.ClaimedAuthority == AIArtifactAuthority.AuthorityThreshold);
|
||||
var deterministic = results.Count(r => r.IsDeterministic);
|
||||
var autoProcessable = results.Count(r => r.CanAutoProcess);
|
||||
|
||||
return $"Verified {results.Count} AI artifact(s): " +
|
||||
$"{suggestions} suggestion(s), {evidenceBacked} evidence-backed, {authorityThreshold} authority-threshold; " +
|
||||
$"{deterministic} deterministic, {autoProcessable} auto-processable";
|
||||
}
|
||||
|
||||
private static VerificationStepResult CreatePassedResult(TimeSpan duration, string details) => new()
|
||||
{
|
||||
StepName = "ai_artifact",
|
||||
Passed = true,
|
||||
Duration = duration,
|
||||
Details = details
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a single AI artifact.
|
||||
/// </summary>
|
||||
public sealed record AIArtifactVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether verification passed.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Artifact ID that was verified.
|
||||
/// </summary>
|
||||
public required string ArtifactId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Predicate type.
|
||||
/// </summary>
|
||||
public required string PredicateType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Model identifier string.
|
||||
/// </summary>
|
||||
public string? ModelId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Authority claimed by the artifact.
|
||||
/// </summary>
|
||||
public AIArtifactAuthority? ClaimedAuthority { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Authority determined by verification.
|
||||
/// </summary>
|
||||
public AIArtifactAuthority? VerifiedAuthority { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Quality score from classification.
|
||||
/// </summary>
|
||||
public double? QualityScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the artifact is deterministic (replayable).
|
||||
/// </summary>
|
||||
public bool IsDeterministic { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the artifact can be auto-processed without human review.
|
||||
/// </summary>
|
||||
public bool CanAutoProcess { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if verification failed.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for resolving evidence references.
|
||||
/// </summary>
|
||||
public interface IAIEvidenceResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if an evidence reference can be resolved.
|
||||
/// </summary>
|
||||
bool CanResolve(string evidenceRef);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve an evidence reference and return its content hash.
|
||||
/// </summary>
|
||||
Task<string?> ResolveAsync(string evidenceRef, CancellationToken ct = default);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.GraphRoot.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.GraphRoot.Tests;
|
||||
|
||||
public class GraphRootAttestorTests
|
||||
@@ -43,7 +44,8 @@ public class GraphRootAttestorTests
|
||||
NullLogger<GraphRootAttestor>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_ValidRequest_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
@@ -60,7 +62,8 @@ public class GraphRootAttestorTests
|
||||
Assert.Equal(2, result.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_SortsNodeIds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -96,7 +99,8 @@ public class GraphRootAttestorTests
|
||||
Assert.Equal("z-node", thirdNodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_SortsEdgeIds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -130,7 +134,8 @@ public class GraphRootAttestorTests
|
||||
Assert.Equal("z-edge", secondEdgeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_IncludesInputDigestsInLeaves()
|
||||
{
|
||||
// Arrange
|
||||
@@ -165,14 +170,16 @@ public class GraphRootAttestorTests
|
||||
Assert.Contains("sha256:params", digestStrings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => _attestor.AttestAsync(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_KeyResolverReturnsNull_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -189,7 +196,8 @@ public class GraphRootAttestorTests
|
||||
Assert.Contains("Unable to resolve signing key", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_CancellationRequested_ThrowsOperationCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -201,7 +209,8 @@ public class GraphRootAttestorTests
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() => _attestor.AttestAsync(request, cts.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AttestAsync_ReturnsCorrectGraphType()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||
using StellaOps.Attestor.GraphRoot.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.GraphRoot.Tests;
|
||||
|
||||
public class GraphRootModelsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootAttestationRequest_RequiredProperties_Set()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -33,7 +35,8 @@ public class GraphRootModelsTests
|
||||
Assert.Empty(request.EvidenceIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootAttestationRequest_OptionalProperties_HaveDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -55,7 +58,8 @@ public class GraphRootModelsTests
|
||||
Assert.Empty(request.EvidenceIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootPredicate_RequiredProperties_Set()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -88,7 +92,8 @@ public class GraphRootModelsTests
|
||||
Assert.Equal(15, predicate.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootAttestation_HasCorrectDefaults()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -129,13 +134,15 @@ public class GraphRootModelsTests
|
||||
Assert.Equal(GraphRootPredicateTypes.GraphRootV1, attestation.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootPredicateTypes_HasCorrectValue()
|
||||
{
|
||||
Assert.Equal("https://stella-ops.org/attestation/graph-root/v1", GraphRootPredicateTypes.GraphRootV1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootVerificationResult_ValidResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -155,7 +162,8 @@ public class GraphRootModelsTests
|
||||
Assert.Equal(5, result.NodeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphRootVerificationResult_InvalidResult_HasReason()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -173,7 +181,8 @@ public class GraphRootModelsTests
|
||||
Assert.NotEqual(result.ExpectedRoot, result.ComputedRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphNodeData_RequiredProperty()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -188,7 +197,8 @@ public class GraphRootModelsTests
|
||||
Assert.Equal("optional content", node.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphEdgeData_AllProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
@@ -205,7 +215,8 @@ public class GraphRootModelsTests
|
||||
Assert.Equal("target-node", edge.TargetNodeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GraphInputDigests_AllDigests()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
@@ -24,6 +24,7 @@ using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.GraphRoot.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.GraphRoot.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -123,7 +124,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
|
||||
#region Full Pipeline Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_CreateAndVerify_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +150,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.Equal(request.EdgeIds.Count, verifyResult.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_LargeGraph_Succeeds()
|
||||
{
|
||||
// Arrange - Large graph with 1000 nodes and 2000 edges
|
||||
@@ -167,7 +170,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.Equal(2000, verifyResult.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_AllGraphTypes_Succeed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -197,7 +201,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
|
||||
#region Rekor Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_WithRekor_IncludesLogIndex()
|
||||
{
|
||||
// Arrange
|
||||
@@ -245,7 +250,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_RekorFailure_ContinuesWithoutLogIndex()
|
||||
{
|
||||
// Arrange
|
||||
@@ -281,7 +287,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.Null(result.RekorLogIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_RekorFailure_ThrowsWhenConfigured()
|
||||
{
|
||||
// Arrange
|
||||
@@ -317,7 +324,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
|
||||
#region Tamper Detection Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_ModifiedNode_VerificationFails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -344,7 +352,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.NotEqual(verifyResult.ExpectedRoot, verifyResult.ComputedRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_ModifiedEdge_VerificationFails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -369,7 +378,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.Contains("Root mismatch", verifyResult.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_AddedNode_VerificationFails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -394,7 +404,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.NotEqual(request.NodeIds.Count, verifyResult.NodeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_RemovedNode_VerificationFails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -420,7 +431,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
|
||||
#region Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_SameInputs_ProducesSameRoot()
|
||||
{
|
||||
// Arrange
|
||||
@@ -463,7 +475,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
Assert.Equal(result1.RootHash, result2.RootHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullPipeline_DifferentNodeOrder_ProducesSameRoot()
|
||||
{
|
||||
// Arrange
|
||||
@@ -507,7 +520,8 @@ public class GraphRootPipelineIntegrationTests
|
||||
|
||||
#region DI Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DependencyInjection_RegistersServices()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -2,19 +2,22 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.GraphRoot.Tests;
|
||||
|
||||
public class Sha256MerkleRootComputerTests
|
||||
{
|
||||
private readonly Sha256MerkleRootComputer _computer = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Algorithm_ReturnsSha256()
|
||||
{
|
||||
Assert.Equal("sha256", _computer.Algorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_SingleLeaf_ReturnsHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -29,7 +32,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.Equal(32, root.Length); // SHA-256 produces 32 bytes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_TwoLeaves_CombinesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -45,7 +49,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_OddLeaves_DuplicatesLast()
|
||||
{
|
||||
// Arrange
|
||||
@@ -64,7 +69,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_Deterministic_SameInputSameOutput()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +90,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.Equal(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_DifferentInputs_DifferentOutputs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +106,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.NotEqual(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_OrderMatters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +130,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.NotEqual(rootAB, rootBA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_EmptyList_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,14 +141,16 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.Throws<ArgumentException>(() => _computer.ComputeRoot(leaves));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_NullInput_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => _computer.ComputeRoot(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_LargeTree_HandlesCorrectly()
|
||||
{
|
||||
// Arrange - create 100 leaves
|
||||
@@ -157,7 +168,8 @@ public class Sha256MerkleRootComputerTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeRoot_PowerOfTwo_HandlesCorrectly()
|
||||
{
|
||||
// Arrange - 8 leaves (power of 2)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.GraphRoot\StellaOps.Attestor.GraphRoot.csproj" />
|
||||
<ProjectReference Include="..\..\..\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="../../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -11,11 +11,13 @@ using StellaOps.Attestor.Bundle.Models;
|
||||
using StellaOps.Attestor.Bundle.Serialization;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundle.Tests;
|
||||
|
||||
public class SigstoreBundleBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithAllComponents_CreatesBundleSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +40,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.VerificationMaterial.Certificate.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithPublicKeyInsteadOfCertificate_CreatesBundleSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -59,7 +62,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.VerificationMaterial.Certificate.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithRekorEntry_IncludesTlogEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -86,7 +90,8 @@ public class SigstoreBundleBuilderTests
|
||||
entry.KindVersion.Version.Should().Be("0.0.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithMultipleRekorEntries_IncludesAllEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -108,7 +113,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.VerificationMaterial.TlogEntries![1].LogIndex.Should().Be("2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithInclusionProof_AddsToLastEntry()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +144,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.VerificationMaterial.TlogEntries![0].InclusionProof!.TreeSize.Should().Be("100000");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithTimestamps_IncludesTimestampData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -160,7 +167,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.VerificationMaterial.TimestampVerificationData!.Rfc3161Timestamps.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_WithCustomMediaType_UsesCustomType()
|
||||
{
|
||||
// Arrange
|
||||
@@ -179,7 +187,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.MediaType.Should().Be("application/vnd.dev.sigstore.bundle.v0.2+json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_MissingDsseEnvelope_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -194,7 +203,8 @@ public class SigstoreBundleBuilderTests
|
||||
.WithMessage("*DSSE*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_MissingCertificateAndPublicKey_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -212,7 +222,8 @@ public class SigstoreBundleBuilderTests
|
||||
.WithMessage("*certificate*public key*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithInclusionProof_WithoutRekorEntry_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -235,7 +246,8 @@ public class SigstoreBundleBuilderTests
|
||||
.WithMessage("*Rekor entry*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildJson_ReturnsSerializedBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -255,7 +267,8 @@ public class SigstoreBundleBuilderTests
|
||||
json.Should().Contain("\"dsseEnvelope\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildUtf8Bytes_ReturnsSerializedBytes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -275,7 +288,8 @@ public class SigstoreBundleBuilderTests
|
||||
json.Should().Contain("\"mediaType\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithDsseEnvelope_FromObject_SetsEnvelopeCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -297,7 +311,8 @@ public class SigstoreBundleBuilderTests
|
||||
bundle.DsseEnvelope.PayloadType.Should().Be("custom/type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WithCertificate_FromBytes_SetsCertificateCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -12,11 +12,13 @@ using StellaOps.Attestor.Bundle.Models;
|
||||
using StellaOps.Attestor.Bundle.Serialization;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundle.Tests;
|
||||
|
||||
public class SigstoreBundleSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_ValidBundle_ProducesValidJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -32,7 +34,8 @@ public class SigstoreBundleSerializerTests
|
||||
json.Should().Contain("\"dsseEnvelope\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializeToUtf8Bytes_ValidBundle_ProducesValidBytes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -47,7 +50,8 @@ public class SigstoreBundleSerializerTests
|
||||
json.Should().Contain("\"mediaType\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Deserialize_ValidJson_ReturnsBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -63,7 +67,8 @@ public class SigstoreBundleSerializerTests
|
||||
bundle.VerificationMaterial.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Deserialize_Utf8Bytes_ReturnsBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -78,7 +83,8 @@ public class SigstoreBundleSerializerTests
|
||||
bundle.MediaType.Should().Be(SigstoreBundleConstants.MediaTypeV03);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RoundTrip_SerializeDeserialize_PreservesData()
|
||||
{
|
||||
// Arrange
|
||||
@@ -98,7 +104,8 @@ public class SigstoreBundleSerializerTests
|
||||
.Should().Be(original.VerificationMaterial.Certificate!.RawBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RoundTrip_WithTlogEntries_PreservesEntries()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +123,8 @@ public class SigstoreBundleSerializerTests
|
||||
entry.KindVersion.Kind.Should().Be("dsse");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryDeserialize_ValidJson_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -130,7 +138,8 @@ public class SigstoreBundleSerializerTests
|
||||
bundle.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryDeserialize_InvalidJson_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +153,8 @@ public class SigstoreBundleSerializerTests
|
||||
bundle.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryDeserialize_NullOrEmpty_ReturnsFalse()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -153,7 +163,8 @@ public class SigstoreBundleSerializerTests
|
||||
SigstoreBundleSerializer.TryDeserialize(" ", out _).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Deserialize_MissingMediaType_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange - JSON that deserializes but fails validation
|
||||
@@ -167,7 +178,8 @@ public class SigstoreBundleSerializerTests
|
||||
.WithMessage("*mediaType*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Deserialize_MissingDsseEnvelope_ThrowsSigstoreBundleException()
|
||||
{
|
||||
// Arrange - JSON with null dsseEnvelope
|
||||
@@ -181,7 +193,8 @@ public class SigstoreBundleSerializerTests
|
||||
.WithMessage("*dsseEnvelope*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Serialize_NullBundle_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act
|
||||
|
||||
@@ -12,13 +12,16 @@ using StellaOps.Attestor.Bundle.Models;
|
||||
using StellaOps.Attestor.Bundle.Verification;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundle.Tests;
|
||||
|
||||
public class SigstoreBundleVerifierTests
|
||||
{
|
||||
private readonly SigstoreBundleVerifier _verifier = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_MissingDsseEnvelope_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -40,7 +43,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.MissingDsseEnvelope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_MissingCertificateAndPublicKey_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -64,7 +68,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.MissingCertificate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_EmptyMediaType_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -91,7 +96,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.InvalidBundleStructure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_NoSignaturesInEnvelope_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -114,7 +120,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_InvalidSignature_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -137,7 +144,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_ValidEcdsaSignature_ReturnsPassed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -166,7 +174,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Checks.DsseSignature.Should().Be(CheckResult.Passed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_TamperedPayload_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -197,7 +206,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.DsseSignatureInvalid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_WithVerificationTimeInPast_ValidatesCertificate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -230,7 +240,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Errors.Should().Contain(e => e.Code == BundleVerificationErrorCode.CertificateNotYetValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_SkipsInclusionProofWhenNotPresent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -258,7 +269,8 @@ public class SigstoreBundleVerifierTests
|
||||
result.Checks.TransparencyLog.Should().Be(CheckResult.Skipped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_NullBundle_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Bundle\StellaOps.Attestor.Bundle.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Attestor.Bundling.Models;
|
||||
using StellaOps.Attestor.Bundling.Services;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
public class AttestationBundlerTests
|
||||
@@ -36,7 +37,8 @@ public class AttestationBundlerTests
|
||||
_options = Options.Create(new BundlingOptions());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBundleAsync_WithAttestations_CreatesDeterministicBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -60,7 +62,8 @@ public class AttestationBundlerTests
|
||||
bundle.Metadata.BundleId.Should().Be(bundle.MerkleTree.Root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBundleAsync_SameAttestationsShuffled_SameMerkleRoot()
|
||||
{
|
||||
// Arrange
|
||||
@@ -89,7 +92,8 @@ public class AttestationBundlerTests
|
||||
bundle1.Metadata.BundleId.Should().Be(bundle2.Metadata.BundleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBundleAsync_NoAttestations_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +109,8 @@ public class AttestationBundlerTests
|
||||
() => bundler.CreateBundleAsync(request));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBundleAsync_WithOrgSigning_SignsBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -145,7 +150,8 @@ public class AttestationBundlerTests
|
||||
bundle.OrgSignature.Algorithm.Should().Be("ECDSA_P256");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_ValidBundle_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -169,7 +175,8 @@ public class AttestationBundlerTests
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_TamperedBundle_ReturnsMerkleRootMismatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -200,7 +207,8 @@ public class AttestationBundlerTests
|
||||
result.Issues.Should().Contain(i => i.Code == "MERKLE_ROOT_MISMATCH");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBundleAsync_RespectsTenantFilter()
|
||||
{
|
||||
// Arrange
|
||||
@@ -225,7 +233,8 @@ public class AttestationBundlerTests
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateBundleAsync_RespectsMaxAttestationsLimit()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Attestor.Bundling.Abstractions;
|
||||
using StellaOps.Attestor.Bundling.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
public class BundleAggregatorTests
|
||||
@@ -23,7 +24,8 @@ public class BundleAggregatorTests
|
||||
|
||||
#region Date Range Filtering Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_WithDateRange_ReturnsOnlyAttestationsInRange()
|
||||
{
|
||||
// Arrange
|
||||
@@ -48,7 +50,8 @@ public class BundleAggregatorTests
|
||||
results.Should().NotContain(a => a.EntryId == "att-4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_InclusiveBoundaries_IncludesEdgeAttestations()
|
||||
{
|
||||
// Arrange
|
||||
@@ -69,7 +72,8 @@ public class BundleAggregatorTests
|
||||
results.Should().Contain(a => a.EntryId == "att-end");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_EmptyRange_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +97,8 @@ public class BundleAggregatorTests
|
||||
|
||||
#region Tenant Filtering Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_WithTenantFilter_ReturnsOnlyTenantAttestations()
|
||||
{
|
||||
// Arrange
|
||||
@@ -114,7 +119,8 @@ public class BundleAggregatorTests
|
||||
results.Should().OnlyContain(a => a.EntryId.StartsWith("att-1") || a.EntryId.StartsWith("att-2"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_WithoutTenantFilter_ReturnsAllTenants()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +144,8 @@ public class BundleAggregatorTests
|
||||
|
||||
#region Predicate Type Filtering Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_WithPredicateTypes_ReturnsOnlyMatchingTypes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -161,7 +168,8 @@ public class BundleAggregatorTests
|
||||
results.Should().OnlyContain(a => a.PredicateType == "verdict.stella/v1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_WithMultiplePredicateTypes_ReturnsAllMatchingTypes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -188,7 +196,8 @@ public class BundleAggregatorTests
|
||||
|
||||
#region Count Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountAsync_ReturnsCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -207,7 +216,8 @@ public class BundleAggregatorTests
|
||||
count.Should().Be(50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountAsync_WithFilters_ReturnsFilteredCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -229,7 +239,8 @@ public class BundleAggregatorTests
|
||||
|
||||
#region Ordering Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AggregateAsync_ReturnsDeterministicOrder()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -16,6 +16,8 @@ using StellaOps.Attestor.Bundling.Configuration;
|
||||
using StellaOps.Attestor.Bundling.Models;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -39,7 +41,8 @@ public class BundleWorkflowIntegrationTests
|
||||
|
||||
#region Full Workflow Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullWorkflow_CreateStoreRetrieveVerify_Succeeds()
|
||||
{
|
||||
// Arrange: Add test attestations
|
||||
@@ -85,7 +88,8 @@ public class BundleWorkflowIntegrationTests
|
||||
verificationResult.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullWorkflow_WithoutOrgSignature_StillWorks()
|
||||
{
|
||||
// Arrange
|
||||
@@ -109,7 +113,8 @@ public class BundleWorkflowIntegrationTests
|
||||
retrieved.Attestations.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullWorkflow_EmptyPeriod_CreatesEmptyBundle()
|
||||
{
|
||||
// Arrange: No attestations added
|
||||
@@ -125,7 +130,8 @@ public class BundleWorkflowIntegrationTests
|
||||
bundle.Attestations.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullWorkflow_LargeBundle_HandlesCorrectly()
|
||||
{
|
||||
// Arrange: Add many attestations
|
||||
@@ -151,7 +157,8 @@ public class BundleWorkflowIntegrationTests
|
||||
|
||||
#region Tenant Isolation Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullWorkflow_TenantIsolation_CreatesSeperateBundles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +185,8 @@ public class BundleWorkflowIntegrationTests
|
||||
|
||||
#region Scheduler Job Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SchedulerJob_ExecutesAndCreatesBundles()
|
||||
{
|
||||
// Arrange: Add attestations for previous month
|
||||
@@ -204,7 +212,8 @@ public class BundleWorkflowIntegrationTests
|
||||
(await _store.ExistsAsync(jobResult.BundleId)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SchedulerJob_MultiTenant_CreatesBundlesForEachTenant()
|
||||
{
|
||||
// Arrange
|
||||
@@ -226,7 +235,8 @@ public class BundleWorkflowIntegrationTests
|
||||
resultX.BundleId.Should().NotBe(resultY.BundleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SchedulerJob_AppliesRetentionPolicy()
|
||||
{
|
||||
// Arrange: Create old bundle
|
||||
|
||||
@@ -14,6 +14,7 @@ using StellaOps.Attestor.Bundling.Abstractions;
|
||||
using StellaOps.Attestor.Bundling.Models;
|
||||
using StellaOps.Attestor.Bundling.Signing;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
public class KmsOrgKeySignerTests
|
||||
@@ -29,7 +30,8 @@ public class KmsOrgKeySignerTests
|
||||
|
||||
#region SignBundleAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignBundleAsync_ValidKey_ReturnsSignature()
|
||||
{
|
||||
// Arrange
|
||||
@@ -54,7 +56,8 @@ public class KmsOrgKeySignerTests
|
||||
result.SignedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignBundleAsync_KeyNotFound_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -73,7 +76,8 @@ public class KmsOrgKeySignerTests
|
||||
.WithMessage($"*'{keyId}'*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignBundleAsync_InactiveKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +97,8 @@ public class KmsOrgKeySignerTests
|
||||
.WithMessage($"*'{keyId}'*not active*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignBundleAsync_ExpiredKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -120,7 +125,8 @@ public class KmsOrgKeySignerTests
|
||||
.WithMessage($"*'{keyId}'*expired*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignBundleAsync_WithCertificateChain_IncludesChainInSignature()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +156,8 @@ public class KmsOrgKeySignerTests
|
||||
|
||||
#region VerifyBundleAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_ValidSignature_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -186,7 +193,8 @@ public class KmsOrgKeySignerTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_InvalidSignature_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -221,7 +229,8 @@ public class KmsOrgKeySignerTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_KmsThrowsException_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -260,7 +269,8 @@ public class KmsOrgKeySignerTests
|
||||
|
||||
#region GetActiveKeyIdAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetActiveKeyIdAsync_ConfiguredActiveKey_ReturnsConfiguredKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -281,7 +291,8 @@ public class KmsOrgKeySignerTests
|
||||
result.Should().Be("configured-active-key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetActiveKeyIdAsync_NoConfiguredKey_ReturnsNewestActiveKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -305,7 +316,8 @@ public class KmsOrgKeySignerTests
|
||||
result.Should().Be("key-2025"); // Newest active key
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetActiveKeyIdAsync_NoActiveKeys_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -326,7 +338,8 @@ public class KmsOrgKeySignerTests
|
||||
.WithMessage("*No active signing key*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetActiveKeyIdAsync_ExcludesExpiredKeys()
|
||||
{
|
||||
// Arrange
|
||||
@@ -353,7 +366,8 @@ public class KmsOrgKeySignerTests
|
||||
|
||||
#region ListKeysAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ListKeysAsync_ReturnsAllKeysFromKms()
|
||||
{
|
||||
// Arrange
|
||||
@@ -382,7 +396,8 @@ public class KmsOrgKeySignerTests
|
||||
|
||||
#region LocalOrgKeySigner Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LocalOrgKeySigner_SignAndVerify_Roundtrip()
|
||||
{
|
||||
// Arrange
|
||||
@@ -402,7 +417,8 @@ public class KmsOrgKeySignerTests
|
||||
signature.Algorithm.Should().Be("ECDSA_P256");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LocalOrgKeySigner_VerifyWithWrongDigest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -421,7 +437,8 @@ public class KmsOrgKeySignerTests
|
||||
isValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LocalOrgKeySigner_VerifyWithUnknownKey_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -442,7 +459,8 @@ public class KmsOrgKeySignerTests
|
||||
isValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LocalOrgKeySigner_GetActiveKeyId_ReturnsActiveKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -458,7 +476,8 @@ public class KmsOrgKeySignerTests
|
||||
activeKeyId.Should().Be("key-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LocalOrgKeySigner_NoActiveKey_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -472,7 +491,8 @@ public class KmsOrgKeySignerTests
|
||||
.WithMessage("*No active signing key*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LocalOrgKeySigner_ListKeys_ReturnsAllKeys()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Attestor.Bundling.Abstractions;
|
||||
using StellaOps.Attestor.Bundling.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
public class OrgKeySignerTests
|
||||
@@ -26,7 +27,8 @@ public class OrgKeySignerTests
|
||||
|
||||
#region Sign/Verify Roundtrip Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_ValidBundle_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -47,7 +49,8 @@ public class OrgKeySignerTests
|
||||
isValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_DifferentContent_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +65,8 @@ public class OrgKeySignerTests
|
||||
isValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SignAndVerify_SameContentDifferentCalls_BothValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -86,7 +90,8 @@ public class OrgKeySignerTests
|
||||
|
||||
#region Certificate Chain Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Sign_IncludesCertificateChain()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +110,8 @@ public class OrgKeySignerTests
|
||||
|
||||
#region Key ID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Sign_WithDifferentKeyIds_ProducesDifferentSignatures()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +129,8 @@ public class OrgKeySignerTests
|
||||
signature1.Signature.Should().NotBe(signature2.Signature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Verify_WithWrongKeyId_Fails()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +151,8 @@ public class OrgKeySignerTests
|
||||
|
||||
#region Empty/Null Input Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Sign_EmptyDigest_StillSigns()
|
||||
{
|
||||
// Arrange
|
||||
@@ -165,7 +173,8 @@ public class OrgKeySignerTests
|
||||
|
||||
#region Algorithm Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("ECDSA_P256")]
|
||||
[InlineData("Ed25519")]
|
||||
[InlineData("RSA_PSS_SHA256")]
|
||||
@@ -187,7 +196,8 @@ public class OrgKeySignerTests
|
||||
|
||||
#region Timestamp Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Sign_IncludesAccurateTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -13,6 +13,7 @@ using StellaOps.Attestor.Bundling.Abstractions;
|
||||
using StellaOps.Attestor.Bundling.Configuration;
|
||||
using StellaOps.Attestor.Bundling.Services;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Bundling.Tests;
|
||||
|
||||
public class RetentionPolicyEnforcerTests
|
||||
@@ -32,7 +33,8 @@ public class RetentionPolicyEnforcerTests
|
||||
|
||||
#region CalculateExpiryDate Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateExpiryDate_DefaultSettings_ReturnsCreatedPlusDefaultMonths()
|
||||
{
|
||||
// Arrange
|
||||
@@ -47,7 +49,8 @@ public class RetentionPolicyEnforcerTests
|
||||
expiryDate.Should().Be(new DateTimeOffset(2026, 6, 15, 10, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateExpiryDate_WithTenantOverride_UsesTenantSpecificRetention()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +78,8 @@ public class RetentionPolicyEnforcerTests
|
||||
defaultExpiry.Should().Be(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); // +24 months
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateExpiryDate_TenantOverrideBelowMinimum_UsesMinimum()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +103,8 @@ public class RetentionPolicyEnforcerTests
|
||||
expiry.Should().Be(new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateExpiryDate_TenantOverrideAboveMaximum_UsesMaximum()
|
||||
{
|
||||
// Arrange
|
||||
@@ -123,7 +128,8 @@ public class RetentionPolicyEnforcerTests
|
||||
expiry.Should().Be(new DateTimeOffset(2034, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CalculateExpiryDate_WithBundleListItem_UsesCreatedAtFromItem()
|
||||
{
|
||||
// Arrange
|
||||
@@ -142,7 +148,8 @@ public class RetentionPolicyEnforcerTests
|
||||
|
||||
#region EnforceAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_WhenDisabled_ReturnsEarlyWithZeroCounts()
|
||||
{
|
||||
// Arrange
|
||||
@@ -164,7 +171,8 @@ public class RetentionPolicyEnforcerTests
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_WithExpiredBundles_DeletesWhenActionIsDelete()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +206,8 @@ public class RetentionPolicyEnforcerTests
|
||||
_storeMock.Verify(x => x.DeleteBundleAsync("expired-1", It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_WithExpiredBundles_ArchivesWhenActionIsArchive()
|
||||
{
|
||||
// Arrange
|
||||
@@ -231,7 +240,8 @@ public class RetentionPolicyEnforcerTests
|
||||
_archiverMock.Verify(x => x.ArchiveAsync("expired-1", "glacier", It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_WithExpiredBundles_MarksOnlyWhenActionIsMarkOnly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -262,7 +272,8 @@ public class RetentionPolicyEnforcerTests
|
||||
_storeMock.Verify(x => x.DeleteBundleAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_BundleInGracePeriod_MarksExpiredButDoesNotDelete()
|
||||
{
|
||||
// Arrange
|
||||
@@ -293,7 +304,8 @@ public class RetentionPolicyEnforcerTests
|
||||
_storeMock.Verify(x => x.DeleteBundleAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_BundlePastGracePeriod_DeletesBundle()
|
||||
{
|
||||
// Arrange
|
||||
@@ -326,7 +338,8 @@ public class RetentionPolicyEnforcerTests
|
||||
_storeMock.Verify(x => x.DeleteBundleAsync("past-grace-1", It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_BundleApproachingExpiry_SendsNotification()
|
||||
{
|
||||
// Arrange
|
||||
@@ -360,7 +373,8 @@ public class RetentionPolicyEnforcerTests
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_NoArchiverConfigured_ReturnsFailureForArchiveAction()
|
||||
{
|
||||
// Arrange
|
||||
@@ -389,7 +403,8 @@ public class RetentionPolicyEnforcerTests
|
||||
result.Failures[0].Reason.Should().Be("Archive unavailable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_DeleteFails_RecordsFailure()
|
||||
{
|
||||
// Arrange
|
||||
@@ -422,7 +437,8 @@ public class RetentionPolicyEnforcerTests
|
||||
result.Failures[0].Reason.Should().Be("Delete failed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task EnforceAsync_RespectsMaxBundlesPerRun_StopsFetchingAfterLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -475,7 +491,8 @@ public class RetentionPolicyEnforcerTests
|
||||
|
||||
#region GetApproachingExpiryAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetApproachingExpiryAsync_ReturnsBundlesWithinCutoff()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Bundling\StellaOps.Attestor.Bundling.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,8 @@ using Moq;
|
||||
using StellaOps.Attestor.Offline.Abstractions;
|
||||
using StellaOps.Attestor.Offline.Services;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Offline.Tests;
|
||||
|
||||
public class FileSystemRootStoreTests : IDisposable
|
||||
@@ -37,7 +39,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_WithNoCertificates_ReturnsEmptyCollection()
|
||||
{
|
||||
// Arrange
|
||||
@@ -51,7 +54,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_WithPemFile_ReturnsCertificates()
|
||||
{
|
||||
// Arrange
|
||||
@@ -70,7 +74,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots[0].Subject.Should().Be("CN=Test Fulcio Root");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_WithDirectory_LoadsAllPemFiles()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +98,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_CachesCertificates_OnSecondCall()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +121,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots1[0].Subject.Should().Be(roots2[0].Subject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ImportRootsAsync_WithValidPem_SavesCertificates()
|
||||
{
|
||||
// Arrange
|
||||
@@ -136,7 +143,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
Directory.EnumerateFiles(targetDir, "*.pem").Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ImportRootsAsync_WithMissingFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +156,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
() => store.ImportRootsAsync("/nonexistent/path.pem", RootType.Fulcio));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ImportRootsAsync_InvalidatesCacheAfterImport()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +187,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
updatedRoots.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ListRootsAsync_ReturnsCorrectInfo()
|
||||
{
|
||||
// Arrange
|
||||
@@ -200,7 +210,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots[0].Thumbprint.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetOrgKeyByIdAsync_WithMatchingThumbprint_ReturnsCertificate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -227,7 +238,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
found!.Subject.Should().Be("CN=Org Signing Key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetOrgKeyByIdAsync_WithNoMatch_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -246,7 +258,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
found.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetRekorKeysAsync_WithPemFile_ReturnsCertificates()
|
||||
{
|
||||
// Arrange
|
||||
@@ -265,7 +278,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
keys[0].Subject.Should().Be("CN=Rekor Key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LoadPem_WithMultipleCertificates_ReturnsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -286,7 +300,8 @@ public class FileSystemRootStoreTests : IDisposable
|
||||
roots.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetFulcioRootsAsync_WithOfflineKitPath_LoadsFromKit()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -17,6 +17,8 @@ using StellaOps.Attestor.Offline.Models;
|
||||
using StellaOps.Attestor.Offline.Services;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Offline.Tests;
|
||||
|
||||
public class OfflineCertChainValidatorTests
|
||||
@@ -32,7 +34,8 @@ public class OfflineCertChainValidatorTests
|
||||
_config = Options.Create(new OfflineVerificationConfig());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithValidCertChain_ChainIsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +58,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.Issues.Should().NotContain(i => i.Code.Contains("CERT"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithUntrustedRoot_ChainIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -80,7 +84,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.Issues.Should().Contain(i => i.Code.StartsWith("CERT"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithMissingCertChain_ReturnsIssue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -102,7 +107,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.Issues.Should().Contain(i => i.Code.StartsWith("CERT") || i.Code.Contains("CHAIN"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithExpiredCert_ChainIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -126,7 +132,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.Issues.Should().Contain(i => i.Code.StartsWith("CERT"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithNotYetValidCert_ChainIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +157,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.Issues.Should().Contain(i => i.Code.StartsWith("CERT"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundle_WithMultipleAttestations_ValidatesCertChainsForAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -176,7 +184,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.CertificateChainValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_CertChainValidationSkipped_WhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
@@ -197,7 +206,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.Issues.Should().NotContain(i => i.Code.Contains("CERT_CHAIN"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithSelfSignedLeaf_ChainIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -220,7 +230,8 @@ public class OfflineCertChainValidatorTests
|
||||
result.CertificateChainValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestation_WithEmptyRootStore_ChainIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -20,6 +20,7 @@ using StellaOps.Attestor.ProofChain.Merkle;
|
||||
// Alias to resolve ambiguity
|
||||
using Severity = StellaOps.Attestor.Offline.Models.VerificationIssueSeverity;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Offline.Tests;
|
||||
|
||||
public class OfflineVerifierTests
|
||||
@@ -44,7 +45,8 @@ public class OfflineVerifierTests
|
||||
.ReturnsAsync(new X509Certificate2Collection());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_ValidBundle_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -66,7 +68,8 @@ public class OfflineVerifierTests
|
||||
result.Issues.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_TamperedMerkleRoot_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +102,8 @@ public class OfflineVerifierTests
|
||||
result.Issues.Should().Contain(i => i.Code == "MERKLE_ROOT_MISMATCH");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_MissingOrgSignature_WhenRequired_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +126,8 @@ public class OfflineVerifierTests
|
||||
result.Issues.Should().Contain(i => i.Code == "ORG_SIG_MISSING");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_WithValidOrgSignature_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -159,7 +164,8 @@ public class OfflineVerifierTests
|
||||
result.OrgSignatureKeyId.Should().Be("org-key-2025");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestationAsync_ValidAttestation_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -179,7 +185,8 @@ public class OfflineVerifierTests
|
||||
result.SignaturesValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAttestationAsync_EmptySignature_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +217,8 @@ public class OfflineVerifierTests
|
||||
result.Issues.Should().Contain(i => i.Code == "DSSE_NO_SIGNATURES");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetVerificationSummariesAsync_ReturnsAllAttestations()
|
||||
{
|
||||
// Arrange
|
||||
@@ -230,7 +238,8 @@ public class OfflineVerifierTests
|
||||
summaries.Should().OnlyContain(s => s.VerificationStatus == AttestationVerificationStatus.Valid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_StrictMode_FailsOnWarnings()
|
||||
{
|
||||
// Arrange
|
||||
@@ -269,7 +278,8 @@ public class OfflineVerifierTests
|
||||
result.Issues.Should().Contain(i => i.Severity == Severity.Warning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyBundleAsync_DeterministicOrdering_SameMerkleValidation()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Offline\StellaOps.Attestor.Offline.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Persistence\StellaOps.Attestor.Persistence.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Attestor.Persistence.Entities;
|
||||
using StellaOps.Attestor.Persistence.Repositories;
|
||||
using StellaOps.Attestor.Persistence.Services;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,7 +24,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
_matcher = new TrustAnchorMatcher(_repository, NullLogger<TrustAnchorMatcher>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindMatchAsync_ExactPattern_MatchesCorrectly()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/lodash@4.17.21", ["key-1"]);
|
||||
@@ -35,7 +37,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
result!.Anchor.AnchorId.Should().Be(anchor.AnchorId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindMatchAsync_WildcardPattern_MatchesPackages()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1"]);
|
||||
@@ -47,7 +50,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
result!.MatchedPattern.Should().Be("pkg:npm/*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindMatchAsync_DoubleWildcard_MatchesNestedPaths()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/@scope/**", ["key-1"]);
|
||||
@@ -58,7 +62,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
result.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindMatchAsync_MultipleMatches_ReturnsMoreSpecific()
|
||||
{
|
||||
var genericAnchor = CreateAnchor("pkg:npm/*", ["key-generic"], policyRef: "generic");
|
||||
@@ -71,7 +76,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
result!.Anchor.PolicyRef.Should().Be("specific");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindMatchAsync_NoMatch_ReturnsNull()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1"]);
|
||||
@@ -82,7 +88,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IsKeyAllowedAsync_AllowedKey_ReturnsTrue()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1", "key-2"]);
|
||||
@@ -93,7 +100,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IsKeyAllowedAsync_DisallowedKey_ReturnsFalse()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1"]);
|
||||
@@ -104,7 +112,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
allowed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IsKeyAllowedAsync_RevokedKey_ReturnsFalse()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1"], revokedKeys: ["key-1"]);
|
||||
@@ -115,7 +124,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
allowed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IsPredicateAllowedAsync_NoRestrictions_AllowsAll()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1"]);
|
||||
@@ -129,7 +139,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task IsPredicateAllowedAsync_WithRestrictions_EnforcesAllowlist()
|
||||
{
|
||||
var anchor = CreateAnchor("pkg:npm/*", ["key-1"]);
|
||||
@@ -140,7 +151,8 @@ public sealed class TrustAnchorMatcherTests
|
||||
(await _matcher.IsPredicateAllowedAsync("pkg:npm/lodash@4.17.21", "random.predicate/v1")).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("pkg:npm/*", "pkg:npm/lodash@4.17.21", true)]
|
||||
[InlineData("pkg:npm/lodash@*", "pkg:npm/lodash@4.17.21", true)]
|
||||
[InlineData("pkg:npm/lodash@4.17.*", "pkg:npm/lodash@4.17.21", true)]
|
||||
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Attestor.ProofChain.Json;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
public class ContentAddressedIdGeneratorTests
|
||||
@@ -25,7 +26,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
|
||||
#region Evidence ID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_SameInput_ProducesSameId()
|
||||
{
|
||||
var predicate = CreateTestEvidencePredicate();
|
||||
@@ -37,7 +39,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1.ToString(), id2.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_DifferentInput_ProducesDifferentId()
|
||||
{
|
||||
var predicate1 = CreateTestEvidencePredicate() with { Source = "scanner-v1" };
|
||||
@@ -49,7 +52,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_IgnoresExistingEvidenceId()
|
||||
{
|
||||
var predicate1 = CreateTestEvidencePredicate() with { EvidenceId = null };
|
||||
@@ -61,7 +65,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeEvidenceId_ReturnsValidFormat()
|
||||
{
|
||||
var predicate = CreateTestEvidencePredicate();
|
||||
@@ -76,7 +81,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
|
||||
#region Reasoning ID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeReasoningId_SameInput_ProducesSameId()
|
||||
{
|
||||
var predicate = CreateTestReasoningPredicate();
|
||||
@@ -87,7 +93,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeReasoningId_DifferentInput_ProducesDifferentId()
|
||||
{
|
||||
var predicate1 = CreateTestReasoningPredicate() with { PolicyVersion = "v1" };
|
||||
@@ -103,7 +110,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
|
||||
#region VEX Verdict ID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeVexVerdictId_SameInput_ProducesSameId()
|
||||
{
|
||||
var predicate = CreateTestVexPredicate();
|
||||
@@ -114,7 +122,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeVexVerdictId_DifferentStatus_ProducesDifferentId()
|
||||
{
|
||||
var predicate1 = CreateTestVexPredicate() with { Status = "affected" };
|
||||
@@ -130,7 +139,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
|
||||
#region Proof Bundle ID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeProofBundleId_SameInput_ProducesSameId()
|
||||
{
|
||||
var sbomEntryId = CreateTestSbomEntryId();
|
||||
@@ -144,7 +154,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeProofBundleId_EvidenceIds_SortedBeforeMerkle()
|
||||
{
|
||||
var sbomEntryId = CreateTestSbomEntryId();
|
||||
@@ -161,7 +172,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeProofBundleId_DifferentEvidence_ProducesDifferentId()
|
||||
{
|
||||
var sbomEntryId = CreateTestSbomEntryId();
|
||||
@@ -177,7 +189,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.NotEqual(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeProofBundleId_EmptyEvidence_Throws()
|
||||
{
|
||||
var sbomEntryId = CreateTestSbomEntryId();
|
||||
@@ -193,7 +206,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
|
||||
#region Graph Revision ID Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeGraphRevisionId_SameInput_ProducesSameId()
|
||||
{
|
||||
var nodeIds = new[] { "node1", "node2" };
|
||||
@@ -209,7 +223,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(id1, id2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeGraphRevisionId_DifferentInput_ProducesDifferentId()
|
||||
{
|
||||
var nodeIds = new[] { "node1", "node2" };
|
||||
@@ -227,7 +242,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
|
||||
#region SBOM Digest Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeSbomDigest_SameInput_ProducesSameDigest()
|
||||
{
|
||||
var sbomJson = """{"name":"test","version":"1.0"}"""u8;
|
||||
@@ -238,7 +254,8 @@ public class ContentAddressedIdGeneratorTests
|
||||
Assert.Equal(digest1, digest2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeSbomEntryId_SameInput_ProducesSameId()
|
||||
{
|
||||
var sbomJson = """{"name":"test","version":"1.0"}"""u8;
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
|
||||
using StellaOps.Attestor.ProofChain.Identifiers;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
public class ContentAddressedIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidSha256_ReturnsId()
|
||||
{
|
||||
var input = "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
|
||||
@@ -21,7 +23,8 @@ public class ContentAddressedIdTests
|
||||
Assert.Equal("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", result.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ValidSha512_ReturnsId()
|
||||
{
|
||||
var digest = new string('a', 128); // SHA-512 is 128 hex chars
|
||||
@@ -32,7 +35,8 @@ public class ContentAddressedIdTests
|
||||
Assert.Equal(digest, result.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_NormalizesToLowercase()
|
||||
{
|
||||
var input = "SHA256:A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2";
|
||||
@@ -42,7 +46,8 @@ public class ContentAddressedIdTests
|
||||
Assert.Equal("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", result.Digest);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("invalid")]
|
||||
[InlineData(":digest")]
|
||||
[InlineData("algo:")]
|
||||
@@ -51,7 +56,8 @@ public class ContentAddressedIdTests
|
||||
Assert.Throws<FormatException>(() => ContentAddressedId.Parse(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Parse_EmptyOrWhitespace_ThrowsArgumentException(string input)
|
||||
@@ -59,14 +65,16 @@ public class ContentAddressedIdTests
|
||||
Assert.Throws<ArgumentException>(() => ContentAddressedId.Parse(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_InvalidDigestLength_Throws()
|
||||
{
|
||||
var input = "sha256:abc"; // Too short
|
||||
Assert.Throws<FormatException>(() => ContentAddressedId.Parse(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToString_ReturnsCanonicalFormat()
|
||||
{
|
||||
var input = "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
|
||||
@@ -78,7 +86,8 @@ public class ContentAddressedIdTests
|
||||
|
||||
public class EvidenceIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ValidDigest_CreatesId()
|
||||
{
|
||||
var digest = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
|
||||
@@ -88,7 +97,8 @@ public class EvidenceIdTests
|
||||
Assert.Equal(digest, id.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToString_ReturnsCanonicalFormat()
|
||||
{
|
||||
var digest = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
|
||||
@@ -100,7 +110,8 @@ public class EvidenceIdTests
|
||||
|
||||
public class ReasoningIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ValidDigest_CreatesId()
|
||||
{
|
||||
var digest = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3";
|
||||
@@ -113,7 +124,8 @@ public class ReasoningIdTests
|
||||
|
||||
public class VexVerdictIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ValidDigest_CreatesId()
|
||||
{
|
||||
var digest = "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4";
|
||||
@@ -126,7 +138,8 @@ public class VexVerdictIdTests
|
||||
|
||||
public class ProofBundleIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ValidDigest_CreatesId()
|
||||
{
|
||||
var digest = "d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5";
|
||||
@@ -141,7 +154,8 @@ public class SbomEntryIdTests
|
||||
{
|
||||
private static readonly string SbomDigest = $"sha256:{new string('a', 64)}";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_WithVersion_CreatesId()
|
||||
{
|
||||
var id = new SbomEntryId(SbomDigest, "pkg:npm/lodash", "4.17.21");
|
||||
@@ -151,7 +165,8 @@ public class SbomEntryIdTests
|
||||
Assert.Equal("4.17.21", id.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_WithoutVersion_CreatesId()
|
||||
{
|
||||
var id = new SbomEntryId(SbomDigest, "pkg:npm/lodash");
|
||||
@@ -161,14 +176,16 @@ public class SbomEntryIdTests
|
||||
Assert.Null(id.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToString_WithVersion_IncludesVersion()
|
||||
{
|
||||
var id = new SbomEntryId(SbomDigest, "pkg:npm/lodash", "4.17.21");
|
||||
Assert.Equal($"{SbomDigest}:pkg:npm/lodash@4.17.21", id.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToString_WithoutVersion_OmitsVersion()
|
||||
{
|
||||
var id = new SbomEntryId(SbomDigest, "pkg:npm/lodash");
|
||||
@@ -178,7 +195,8 @@ public class SbomEntryIdTests
|
||||
|
||||
public class GraphRevisionIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ValidDigest_CreatesId()
|
||||
{
|
||||
var digest = "e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6";
|
||||
@@ -187,7 +205,8 @@ public class GraphRevisionIdTests
|
||||
Assert.Equal(digest, id.Digest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToString_ReturnsGrvFormat()
|
||||
{
|
||||
var digest = "e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6";
|
||||
@@ -199,7 +218,8 @@ public class GraphRevisionIdTests
|
||||
|
||||
public class TrustAnchorIdTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ValidGuid_CreatesId()
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
@@ -208,7 +228,8 @@ public class TrustAnchorIdTests
|
||||
Assert.Equal(guid, id.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToString_ReturnsGuidString()
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
|
||||
@@ -9,13 +9,16 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.ProofChain.Json;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
public sealed class JsonCanonicalizerTests
|
||||
{
|
||||
private readonly IJsonCanonicalizer _canonicalizer = new Rfc8785JsonCanonicalizer();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_SortsKeys()
|
||||
{
|
||||
var input = """{"z": 1, "a": 2}"""u8;
|
||||
@@ -30,7 +33,8 @@ public sealed class JsonCanonicalizerTests
|
||||
Assert.True(aIndex < zIndex, "Keys should be sorted alphabetically");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_RemovesWhitespace()
|
||||
{
|
||||
var input = """{ "key" : "value" }"""u8;
|
||||
@@ -41,7 +45,8 @@ public sealed class JsonCanonicalizerTests
|
||||
Assert.Equal("{\"key\":\"value\"}", outputStr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_PreservesUnicodeContent()
|
||||
{
|
||||
var text = "hello 世界 \U0001F30D";
|
||||
@@ -52,7 +57,8 @@ public sealed class JsonCanonicalizerTests
|
||||
Assert.Equal(text, document.RootElement.GetProperty("text").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_SameInput_ProducesSameOutput()
|
||||
{
|
||||
var input = """{"key": "value", "nested": {"b": 2, "a": 1}}"""u8;
|
||||
@@ -63,7 +69,8 @@ public sealed class JsonCanonicalizerTests
|
||||
Assert.Equal(output1, output2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_Arrays_PreservesOrder()
|
||||
{
|
||||
var input = """{"items": [3, 1, 2]}"""u8;
|
||||
@@ -73,7 +80,8 @@ public sealed class JsonCanonicalizerTests
|
||||
Assert.Contains("[3,1,2]", outputStr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_BooleanValues_LowerCase()
|
||||
{
|
||||
var input = """{"t": true, "f": false}"""u8;
|
||||
@@ -86,7 +94,8 @@ public sealed class JsonCanonicalizerTests
|
||||
Assert.DoesNotContain("False", outputStr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Canonicalize_EmptyObject_ReturnsEmptyBraces()
|
||||
{
|
||||
var input = "{}"u8;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
using System.Text;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
public class MerkleTreeBuilderTests
|
||||
@@ -19,7 +20,8 @@ public class MerkleTreeBuilderTests
|
||||
_builder = new DeterministicMerkleTreeBuilder();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_SingleLeaf_ReturnsSha256OfLeaf()
|
||||
{
|
||||
var leaf = Encoding.UTF8.GetBytes("single leaf");
|
||||
@@ -31,7 +33,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(32, root.Length); // SHA-256 produces 32 bytes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_TwoLeaves_ReturnsCombinedHash()
|
||||
{
|
||||
var leaf1 = Encoding.UTF8.GetBytes("leaf1");
|
||||
@@ -44,7 +47,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_SameInput_ProducesSameRoot()
|
||||
{
|
||||
var leaf1 = Encoding.UTF8.GetBytes("leaf1");
|
||||
@@ -57,7 +61,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_DifferentOrder_ProducesDifferentRoot()
|
||||
{
|
||||
var leaf1 = Encoding.UTF8.GetBytes("leaf1");
|
||||
@@ -72,7 +77,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.NotEqual(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_OddNumberOfLeaves_HandlesCorrectly()
|
||||
{
|
||||
var leaves = new ReadOnlyMemory<byte>[]
|
||||
@@ -88,7 +94,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_ManyLeaves_ProducesDeterministicRoot()
|
||||
{
|
||||
var leaves = new ReadOnlyMemory<byte>[100];
|
||||
@@ -103,7 +110,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_EmptyLeaves_Throws()
|
||||
{
|
||||
var leaves = Array.Empty<ReadOnlyMemory<byte>>();
|
||||
@@ -111,7 +119,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Throws<ArgumentException>(() => _builder.ComputeMerkleRoot(leaves));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_PowerOfTwoLeaves_ProducesBalancedTree()
|
||||
{
|
||||
var leaves = new ReadOnlyMemory<byte>[]
|
||||
@@ -128,7 +137,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMerkleRoot_BinaryData_HandlesBinaryInput()
|
||||
{
|
||||
var binary1 = new byte[] { 0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD };
|
||||
@@ -141,7 +151,8 @@ public class MerkleTreeBuilderTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Text;
|
||||
using StellaOps.Attestor.ProofChain.Merkle;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -25,7 +26,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
|
||||
#region Task #10: Merkle Tree Determinism Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MerkleRoot_SameInputDifferentRuns_ProducesIdenticalRoot()
|
||||
{
|
||||
// Arrange - simulate a proof spine with SBOM, evidence, reasoning, VEX
|
||||
@@ -44,7 +46,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
Assert.Equal(root2, root3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MerkleRoot_EvidenceOrderIsNormalized_ProducesSameRoot()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +65,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
Assert.Equal(root1, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void MerkleRoot_DifferentSbom_ProducesDifferentRoot()
|
||||
{
|
||||
// Arrange
|
||||
@@ -82,7 +86,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
|
||||
#region Task #11: Full Pipeline Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Pipeline_CompleteProofSpine_AssemblesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -105,7 +110,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
Assert.StartsWith("sha256:", FormatAsId(root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Pipeline_EmptyEvidence_HandlesGracefully()
|
||||
{
|
||||
// Arrange - minimal proof spine with no evidence
|
||||
@@ -122,7 +128,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Pipeline_ManyEvidenceItems_ScalesEfficiently()
|
||||
{
|
||||
// Arrange - large number of evidence items
|
||||
@@ -147,7 +154,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
|
||||
#region Task #12: Cross-Platform Verification Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CrossPlatform_KnownVector_ProducesExpectedRoot()
|
||||
{
|
||||
// Arrange - known test vector for cross-platform verification
|
||||
@@ -174,7 +182,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
Assert.Equal(root, root2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CrossPlatform_Utf8Encoding_HandlesBinaryCorrectly()
|
||||
{
|
||||
// Arrange - IDs with special characters (should be UTF-8 encoded)
|
||||
@@ -191,7 +200,8 @@ public class ProofSpineAssemblyIntegrationTests
|
||||
Assert.Equal(32, root.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CrossPlatform_BinaryDigests_HandleRawBytes()
|
||||
{
|
||||
// Arrange - actual SHA-256 digests (64 hex chars)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.ProofChain.Tests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.StandardPredicates.Parsers;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.StandardPredicates.Tests;
|
||||
|
||||
public class StandardPredicateRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_ValidParser_SuccessfullyRegisters()
|
||||
{
|
||||
// Arrange
|
||||
@@ -23,7 +25,8 @@ public class StandardPredicateRegistryTests
|
||||
foundParser.Should().BeSameAs(parser);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_DuplicatePredicateType_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -39,7 +42,8 @@ public class StandardPredicateRegistryTests
|
||||
.WithMessage("*already registered*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_NullPredicateType_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -51,7 +55,8 @@ public class StandardPredicateRegistryTests
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_NullParser_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +67,8 @@ public class StandardPredicateRegistryTests
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryGetParser_RegisteredType_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
@@ -79,7 +85,8 @@ public class StandardPredicateRegistryTests
|
||||
foundParser!.PredicateType.Should().Be(parser.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryGetParser_UnregisteredType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +100,8 @@ public class StandardPredicateRegistryTests
|
||||
foundParser.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetRegisteredTypes_NoRegistrations_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +114,8 @@ public class StandardPredicateRegistryTests
|
||||
types.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetRegisteredTypes_MultipleRegistrations_ReturnsSortedList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -131,7 +140,8 @@ public class StandardPredicateRegistryTests
|
||||
types[2].Should().Be(spdxParser.PredicateType); // https://spdx.dev/Document
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetRegisteredTypes_ReturnsReadOnlyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -147,7 +157,8 @@ public class StandardPredicateRegistryTests
|
||||
types.GetType().Name.Should().Contain("ReadOnly");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registry_ThreadSafety_ConcurrentRegistrations()
|
||||
{
|
||||
// Arrange
|
||||
@@ -168,7 +179,8 @@ public class StandardPredicateRegistryTests
|
||||
registeredTypes.Should().BeInAscendingOrder();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Registry_ThreadSafety_ConcurrentReads()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.StandardPredicates\StellaOps.Attestor.StandardPredicates.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,13 +3,15 @@ using System.Text.Json.Nodes;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Types.Tests;
|
||||
|
||||
public class AttestationGoldenSamplesTests
|
||||
{
|
||||
private const string ExpectedSubjectDigest = "d5f5e54d1e1a4c3c7b18961ea7cadb88ec0a93a9f2f40f0e823d9184c83e4d72";
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EverySampleIsCanonicalAndComplete()
|
||||
{
|
||||
var samplesDirectory = Path.Combine(AppContext.BaseDirectory, "samples");
|
||||
|
||||
@@ -3,11 +3,14 @@ using FluentAssertions;
|
||||
using Json.Schema;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Attestor.Types.Tests;
|
||||
|
||||
public sealed class SmartDiffSchemaValidationTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SmartDiffSchema_ValidatesSamplePredicate()
|
||||
{
|
||||
var schemaPath = Path.Combine(AppContext.BaseDirectory, "schemas", "stellaops-smart-diff.v1.schema.json");
|
||||
@@ -72,7 +75,8 @@ public sealed class SmartDiffSchemaValidationTests
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SmartDiffSchema_RejectsInvalidReachabilityClass()
|
||||
{
|
||||
var schemaPath = Path.Combine(AppContext.BaseDirectory, "schemas", "stellaops-smart-diff.v1.schema.json");
|
||||
|
||||
@@ -19,4 +19,7 @@
|
||||
<None Include="..\..\StellaOps.Attestor.Types\samples\**\*.json" LinkBase="samples" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="..\..\StellaOps.Attestor.Types\schemas\**\*.schema.json" LinkBase="schemas" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user