// // Copyright (c) StellaOps. Licensed under the BUSL-1.1. // Sprint: SPRINT_20260112_004_LB_evidence_card_core (EVPCARD-LB-004) // Description: Tests for EvidenceCardService // using System.Collections.Immutable; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Determinism; using StellaOps.Evidence.Pack; using StellaOps.Evidence.Pack.Models; using StellaOps.TestKit; using Xunit; namespace StellaOps.Evidence.Pack.Tests; public sealed class EvidenceCardServiceTests { private readonly FixedGuidProvider _guidProvider = new(Guid.Parse("11111111-1111-1111-1111-111111111111")); private readonly TestTimeProvider _timeProvider = new(new DateTimeOffset(2026, 1, 14, 10, 0, 0, TimeSpan.Zero)); [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateCardAsync_WithValidRequest_ReturnsCard() { var service = CreateService(); var request = new EvidenceCardRequest { FindingId = "CVE-2024-12345", ArtifactDigest = "sha256:abc123", ComponentPurl = "pkg:npm/lodash@4.17.21", TenantId = "tenant-1" }; var card = await service.CreateCardAsync(request); Assert.NotNull(card); Assert.Equal("11111111111111111111111111111111", card.CardId); Assert.Equal("CVE-2024-12345", card.Subject.FindingId); Assert.Equal("sha256:abc123", card.Subject.ArtifactDigest); Assert.NotNull(card.Envelope); Assert.NotNull(card.SbomExcerpt); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateCardAsync_SetsGeneratedAtFromTimeProvider() { var service = CreateService(); var request = new EvidenceCardRequest { FindingId = "CVE-2024-12345", ArtifactDigest = "sha256:abc123", TenantId = "tenant-1" }; var card = await service.CreateCardAsync(request); Assert.Equal(_timeProvider.GetUtcNow(), card.GeneratedAt); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateCardAsync_WithComponentPurl_ExtractsComponentInfo() { var service = CreateService(); var request = new EvidenceCardRequest { FindingId = "CVE-2024-12345", ArtifactDigest = "sha256:abc123", ComponentPurl = "pkg:npm/lodash@4.17.21", TenantId = "tenant-1" }; var card = await service.CreateCardAsync(request); Assert.Single(card.SbomExcerpt.Components); Assert.Equal("pkg:npm/lodash@4.17.21", card.SbomExcerpt.Components[0].Purl); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ExportCardAsync_Json_ReturnsValidJson() { var service = CreateService(); var card = await CreateTestCard(service); var export = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json); Assert.Equal("application/json", export.ContentType); Assert.StartsWith("sha256:", export.ContentDigest); var json = Encoding.UTF8.GetString(export.Content); using var document = JsonDocument.Parse(json); Assert.Equal(JsonValueKind.Object, document.RootElement.ValueKind); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ExportCardAsync_CompactJson_IsSmallerThanIndented() { var service = CreateService(); var card = await CreateTestCard(service); var jsonExport = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json); var compactExport = await service.ExportCardAsync(card, EvidenceCardExportFormat.CompactJson); Assert.True(compactExport.Content.Length < jsonExport.Content.Length); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ExportCardAsync_CanonicalJson_IsDeterministic() { var service1 = CreateService(); var service2 = CreateService(); var card1 = await CreateTestCard(service1); var card2 = await CreateTestCard(service2); var export1 = await service1.ExportCardAsync(card1, EvidenceCardExportFormat.CanonicalJson); var export2 = await service2.ExportCardAsync(card2, EvidenceCardExportFormat.CanonicalJson); Assert.Equal(export1.ContentDigest, export2.ContentDigest); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VerifyCardAsync_ValidCard_ReturnsValid() { var service = CreateService(); var card = await CreateTestCard(service); var result = await service.VerifyCardAsync(card); Assert.True(result.Valid); Assert.True(result.SignatureValid); Assert.True(result.SbomDigestValid); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VerifyCardAsync_WithMissingReceipt_AllowedByDefault() { var service = CreateService(); var card = await CreateTestCard(service); var result = await service.VerifyCardAsync(card, new EvidenceCardVerificationOptions { AllowMissingReceipt = true }); Assert.True(result.Valid); Assert.Null(result.RekorReceiptValid); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VerifyCardAsync_WithMissingReceipt_FailsWhenRequired() { var service = CreateService(); var card = await CreateTestCard(service); var result = await service.VerifyCardAsync(card, new EvidenceCardVerificationOptions { AllowMissingReceipt = false }); Assert.False(result.Valid); Assert.Contains(result.Issues, i => i.Contains("Rekor receipt is required")); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task VerifyCardAsync_WithValidRekorReceipt_ReturnsTrue() { var service = CreateService(); var card = await CreateTestCard(service); // Add a valid-looking Rekor receipt var cardWithReceipt = card with { RekorReceipt = new RekorReceiptMetadata { Uuid = "abc123def456", LogIndex = 12345, LogId = "0x1234", LogUrl = "https://rekor.sigstore.dev", IntegratedTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), RootHash = "sha256:root123", TreeSize = 100000, InclusionProofHashes = ImmutableArray.Create("hash1", "hash2"), CheckpointNote = "rekor.sigstore.dev - 12345\n100000\nroot123\n", CheckpointSignatures = ImmutableArray.Create(new CheckpointSignature { KeyId = "key1", Signature = "c2lnbmF0dXJl" }) } }; var result = await service.VerifyCardAsync(cardWithReceipt); Assert.True(result.Valid); Assert.True(result.RekorReceiptValid); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ExportCardAsync_SetsCorrectFileName() { var service = CreateService(); var card = await CreateTestCard(service); var export = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json); Assert.Equal($"evidence-card-{card.CardId}.json", export.FileName); } private EvidenceCardService CreateService() { return new EvidenceCardService( _timeProvider, _guidProvider, NullLogger.Instance); } private async Task CreateTestCard(EvidenceCardService service) { var request = new EvidenceCardRequest { FindingId = "CVE-2024-12345", ArtifactDigest = "sha256:abc123", ComponentPurl = "pkg:npm/lodash@4.17.21", TenantId = "tenant-1" }; return await service.CreateCardAsync(request); } private sealed class FixedGuidProvider : IGuidProvider { private readonly Guid _guid; public FixedGuidProvider(Guid guid) => _guid = guid; public Guid NewGuid() => _guid; } private sealed class TestTimeProvider : TimeProvider { private readonly DateTimeOffset _fixedTime; public TestTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime; public override DateTimeOffset GetUtcNow() => _fixedTime; } }