Files
git.stella-ops.org/src/__Libraries/__Tests/StellaOps.Evidence.Pack.Tests/EvidenceCardServiceTests.cs

261 lines
8.4 KiB
C#

// <copyright file="EvidenceCardServiceTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// Sprint: SPRINT_20260112_004_LB_evidence_card_core (EVPCARD-LB-004)
// Description: Tests for EvidenceCardService
// </copyright>
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<EvidenceCardService>.Instance);
}
private async Task<EvidenceCard> 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;
}
}