Refactor code structure and optimize performance across multiple modules

This commit is contained in:
StellaOps Bot
2025-12-26 20:03:22 +02:00
parent c786faae84
commit b4fc66feb6
3353 changed files with 88254 additions and 1590657 deletions

View File

@@ -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";

View File

@@ -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;
}
// ==========================================================================

View File

@@ -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[]

View File

@@ -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);

View File

@@ -18,5 +18,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -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");

View File

@@ -25,6 +25,7 @@
<ItemGroup>
<ProjectReference Include="..\\..\\StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="../../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -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";

View File

@@ -26,6 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>

View File

@@ -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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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");

View File

@@ -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();

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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());

View File

@@ -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

View File

@@ -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 { });

View File

@@ -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"));

View File

@@ -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()
{

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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
};
}
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)]

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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)

View File

@@ -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>

View File

@@ -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()
{

View File

@@ -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

View File

@@ -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>

View File

@@ -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");

View File

@@ -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");

View File

@@ -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>