sprints work

This commit is contained in:
master
2026-01-10 20:32:13 +02:00
parent 0d5eda86fc
commit 17d0631b8e
189 changed files with 40667 additions and 497 deletions

View File

@@ -0,0 +1,357 @@
// <copyright file="EvidencePackServiceTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using StellaOps.Evidence.Pack.Storage;
namespace StellaOps.Evidence.Pack.Tests;
/// <summary>
/// Tests for <see cref="EvidencePackService"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class EvidencePackServiceTests
{
private readonly InMemoryEvidencePackStore _store;
private readonly Mock<IEvidenceResolver> _resolverMock;
private readonly Mock<IEvidencePackSigner> _signerMock;
private readonly FakeTimeProvider _timeProvider;
private readonly EvidencePackService _service;
public EvidencePackServiceTests()
{
_store = new InMemoryEvidencePackStore();
_resolverMock = new Mock<IEvidenceResolver>();
_signerMock = new Mock<IEvidencePackSigner>();
_timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-01-10T10:00:00Z"));
_service = new EvidencePackService(
_store,
_resolverMock.Object,
_signerMock.Object,
_timeProvider,
NullLogger<EvidencePackService>.Instance);
}
[Fact]
public async Task CreateAsync_WithValidInput_CreatesPack()
{
// Arrange
var claims = new[]
{
new EvidenceClaim
{
ClaimId = "claim-001",
Text = "Component is affected by CVE-2023-44487",
Type = ClaimType.VulnerabilityStatus,
Status = "affected",
Confidence = 0.92,
EvidenceIds = ["ev-001"],
Source = "ai"
}
};
var evidence = new[]
{
new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "stella://sbom/scan-123",
Digest = "sha256:abc123",
CollectedAt = _timeProvider.GetUtcNow(),
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
}
};
var subject = new EvidenceSubject
{
Type = EvidenceSubjectType.Cve,
CveId = "CVE-2023-44487"
};
var context = new EvidencePackContext
{
TenantId = "tenant-123",
RunId = "run-abc"
};
// Act
var pack = await _service.CreateAsync(claims, evidence, subject, context, CancellationToken.None);
// Assert
pack.Should().NotBeNull();
pack.PackId.Should().StartWith("pack-");
pack.TenantId.Should().Be("tenant-123");
pack.Claims.Should().HaveCount(1);
pack.Evidence.Should().HaveCount(1);
pack.Subject.CveId.Should().Be("CVE-2023-44487");
pack.CreatedAt.Should().Be(_timeProvider.GetUtcNow());
// Verify stored
var stored = await _store.GetByIdAsync("tenant-123", pack.PackId, CancellationToken.None);
stored.Should().NotBeNull();
}
[Fact]
public async Task CreateAsync_LinksToRunWhenProvided()
{
// Arrange
var claims = CreateMinimalClaims();
var evidence = CreateMinimalEvidence();
var subject = new EvidenceSubject { Type = EvidenceSubjectType.Finding, FindingId = "finding-123" };
var context = new EvidencePackContext { TenantId = "tenant-1", RunId = "run-xyz" };
// Act
var pack = await _service.CreateAsync(claims, evidence, subject, context, CancellationToken.None);
// Assert
var packsForRun = await _store.GetByRunIdAsync("run-xyz", CancellationToken.None);
packsForRun.Should().Contain(p => p.PackId == pack.PackId);
}
[Fact]
public async Task CreateFromRunAsync_AggregatesExistingPacks()
{
// Arrange
var subject = new EvidenceSubject { Type = EvidenceSubjectType.Finding, FindingId = "finding-123" };
// Create two packs for the same run
var context1 = new EvidencePackContext { TenantId = "tenant-1", RunId = "run-agg" };
var context2 = new EvidencePackContext { TenantId = "tenant-1", RunId = "run-agg" };
await _service.CreateAsync(
[new EvidenceClaim { ClaimId = "c1", Text = "Claim 1", Type = ClaimType.VulnerabilityStatus, Status = "affected", Confidence = 0.8, EvidenceIds = ["e1"] }],
[CreateEvidence("e1")],
subject, context1, CancellationToken.None);
await _service.CreateAsync(
[new EvidenceClaim { ClaimId = "c2", Text = "Claim 2", Type = ClaimType.Reachability, Status = "reachable", Confidence = 0.9, EvidenceIds = ["e2"] }],
[CreateEvidence("e2")],
subject, context2, CancellationToken.None);
// Act
var aggregated = await _service.CreateFromRunAsync("run-agg", subject, CancellationToken.None);
// Assert
aggregated.Claims.Should().HaveCount(2);
aggregated.Evidence.Should().HaveCount(2);
}
[Fact]
public async Task SignAsync_CreatesSignedPack()
{
// Arrange
var pack = await CreateTestPack();
var expectedEnvelope = new DsseEnvelope
{
PayloadType = "application/vnd.stellaops.evidence-pack+json",
Payload = "base64-payload",
PayloadDigest = pack.ComputeContentDigest(),
Signatures = [new DsseSignature { KeyId = "key-123", Sig = "sig-abc" }]
};
_signerMock.Setup(s => s.SignAsync(pack, It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedEnvelope);
// Act
var signedPack = await _service.SignAsync(pack, CancellationToken.None);
// Assert
signedPack.Pack.Should().Be(pack);
signedPack.Envelope.Should().Be(expectedEnvelope);
signedPack.SignedAt.Should().Be(_timeProvider.GetUtcNow());
// Verify stored
var stored = await _store.GetSignedByIdAsync(pack.TenantId, pack.PackId, CancellationToken.None);
stored.Should().NotBeNull();
}
[Fact]
public async Task VerifyAsync_WithValidPack_ReturnsSuccess()
{
// Arrange
var pack = await CreateTestPack();
var envelope = new DsseEnvelope
{
PayloadType = "application/vnd.stellaops.evidence-pack+json",
Payload = "base64-payload",
PayloadDigest = pack.ComputeContentDigest(),
Signatures = [new DsseSignature { KeyId = "key-123", Sig = "sig-abc" }]
};
var signedPack = new SignedEvidencePack
{
Pack = pack,
Envelope = envelope,
SignedAt = _timeProvider.GetUtcNow()
};
_signerMock.Setup(s => s.VerifyAsync(envelope, It.IsAny<CancellationToken>()))
.ReturnsAsync(SignatureVerificationResult.Success("key-123", _timeProvider.GetUtcNow()));
_resolverMock.Setup(r => r.VerifyEvidenceAsync(It.IsAny<EvidenceItem>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((EvidenceItem e, CancellationToken _) => new EvidenceResolutionResult
{
EvidenceId = e.EvidenceId,
Uri = e.Uri,
Resolved = true,
DigestMatches = true
});
// Act
var result = await _service.VerifyAsync(signedPack, CancellationToken.None);
// Assert
result.Valid.Should().BeTrue();
result.Issues.Should().BeEmpty();
}
[Fact]
public async Task VerifyAsync_WithInvalidSignature_ReturnsFailed()
{
// Arrange
var pack = await CreateTestPack();
var envelope = new DsseEnvelope
{
PayloadType = "application/vnd.stellaops.evidence-pack+json",
Payload = "base64-payload",
PayloadDigest = pack.ComputeContentDigest(),
Signatures = [new DsseSignature { KeyId = "key-123", Sig = "invalid-sig" }]
};
var signedPack = new SignedEvidencePack
{
Pack = pack,
Envelope = envelope,
SignedAt = _timeProvider.GetUtcNow()
};
_signerMock.Setup(s => s.VerifyAsync(envelope, It.IsAny<CancellationToken>()))
.ReturnsAsync(SignatureVerificationResult.Failure("Invalid signature", _timeProvider.GetUtcNow()));
_resolverMock.Setup(r => r.VerifyEvidenceAsync(It.IsAny<EvidenceItem>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((EvidenceItem e, CancellationToken _) => new EvidenceResolutionResult
{
EvidenceId = e.EvidenceId,
Uri = e.Uri,
Resolved = true,
DigestMatches = true
});
// Act
var result = await _service.VerifyAsync(signedPack, CancellationToken.None);
// Assert
result.Valid.Should().BeFalse();
result.Issues.Should().Contain(i => i.Contains("Signature verification failed"));
}
[Fact]
public async Task ExportAsync_AsJson_ReturnsValidJson()
{
// Arrange
var pack = await CreateTestPack();
// Act
var export = await _service.ExportAsync(pack.PackId, EvidencePackExportFormat.Json, CancellationToken.None);
// Assert
export.Format.Should().Be(EvidencePackExportFormat.Json);
export.ContentType.Should().Be("application/json");
export.FileName.Should().EndWith(".json");
var json = System.Text.Encoding.UTF8.GetString(export.Content);
json.Should().Contain(pack.PackId);
}
[Fact]
public async Task ExportAsync_AsMarkdown_ReturnsReadableMarkdown()
{
// Arrange
var pack = await CreateTestPack();
// Act
var export = await _service.ExportAsync(pack.PackId, EvidencePackExportFormat.Markdown, CancellationToken.None);
// Assert
export.Format.Should().Be(EvidencePackExportFormat.Markdown);
export.ContentType.Should().Be("text/markdown");
export.FileName.Should().EndWith(".md");
var markdown = System.Text.Encoding.UTF8.GetString(export.Content);
markdown.Should().Contain("# Evidence Pack");
markdown.Should().Contain(pack.PackId);
markdown.Should().Contain("## Claims");
markdown.Should().Contain("## Evidence");
}
private async Task<EvidencePack> CreateTestPack()
{
var claims = CreateMinimalClaims();
var evidence = CreateMinimalEvidence();
var subject = new EvidenceSubject { Type = EvidenceSubjectType.Cve, CveId = "CVE-2023-44487" };
var context = new EvidencePackContext { TenantId = "tenant-test" };
return await _service.CreateAsync(claims, evidence, subject, context, CancellationToken.None);
}
private static EvidenceClaim[] CreateMinimalClaims()
{
return
[
new EvidenceClaim
{
ClaimId = "claim-001",
Text = "Test claim",
Type = ClaimType.VulnerabilityStatus,
Status = "affected",
Confidence = 0.9,
EvidenceIds = ["ev-001"]
}
];
}
private EvidenceItem[] CreateMinimalEvidence()
{
return [CreateEvidence("ev-001")];
}
private EvidenceItem CreateEvidence(string id)
{
return new EvidenceItem
{
EvidenceId = id,
Type = EvidenceType.Sbom,
Uri = $"stella://sbom/{id}",
Digest = $"sha256:{id}",
CollectedAt = _timeProvider.GetUtcNow(),
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 50)
};
}
}
/// <summary>
/// A fake TimeProvider for testing.
/// </summary>
internal sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _utcNow;
public FakeTimeProvider(DateTimeOffset initialTime)
{
_utcNow = initialTime;
}
public override DateTimeOffset GetUtcNow() => _utcNow;
public void Advance(TimeSpan duration) => _utcNow = _utcNow.Add(duration);
public void SetTime(DateTimeOffset time) => _utcNow = time;
}

View File

@@ -0,0 +1,223 @@
// <copyright file="EvidenceResolverTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using StellaOps.Evidence.Pack.Resolvers;
namespace StellaOps.Evidence.Pack.Tests;
/// <summary>
/// Tests for <see cref="EvidenceResolver"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class EvidenceResolverTests
{
private readonly Mock<ITypeResolver> _sbomResolverMock;
private readonly Mock<ITypeResolver> _reachResolverMock;
private readonly EvidenceResolver _resolver;
public EvidenceResolverTests()
{
_sbomResolverMock = new Mock<ITypeResolver>();
_sbomResolverMock.Setup(r => r.SupportedTypes).Returns(["sbom"]);
_reachResolverMock = new Mock<ITypeResolver>();
_reachResolverMock.Setup(r => r.SupportedTypes).Returns(["reach"]);
_resolver = new EvidenceResolver(
[_sbomResolverMock.Object, _reachResolverMock.Object],
NullLogger<EvidenceResolver>.Instance);
}
[Fact]
public async Task ResolveAndSnapshotAsync_WithSupportedType_ReturnsSnapshot()
{
// Arrange
var expectedSnapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100);
_sbomResolverMock.Setup(r => r.ResolveAsync("sbom", "scan-123", It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedSnapshot);
// Act
var result = await _resolver.ResolveAndSnapshotAsync("sbom", "scan-123", CancellationToken.None);
// Assert
result.Snapshot.Should().Be(expectedSnapshot);
result.Digest.Should().StartWith("sha256:");
result.ResolvedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public async Task ResolveAndSnapshotAsync_WithUnsupportedType_ThrowsException()
{
// Act
var act = () => _resolver.ResolveAndSnapshotAsync("unknown", "path", CancellationToken.None);
// Assert
await act.Should().ThrowAsync<UnsupportedEvidenceTypeException>()
.Where(e => e.Type == "unknown");
}
[Fact]
public async Task VerifyEvidenceAsync_WithMatchingDigest_ReturnsSuccess()
{
// Arrange
var snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100);
_sbomResolverMock.Setup(r => r.ResolveAsync("sbom", "scan-123", It.IsAny<CancellationToken>()))
.ReturnsAsync(snapshot);
// First resolve to get the digest
var resolved = await _resolver.ResolveAndSnapshotAsync("sbom", "scan-123", CancellationToken.None);
var evidence = new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "stella://sbom/scan-123",
Digest = resolved.Digest,
CollectedAt = DateTimeOffset.UtcNow,
Snapshot = snapshot
};
// Act
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
// Assert
result.Resolved.Should().BeTrue();
result.DigestMatches.Should().BeTrue();
result.Error.Should().BeNull();
}
[Fact]
public async Task VerifyEvidenceAsync_WithMismatchedDigest_ReturnsFailed()
{
// Arrange
var originalSnapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100);
var changedSnapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 150); // Different count
_sbomResolverMock.Setup(r => r.ResolveAsync("sbom", "scan-123", It.IsAny<CancellationToken>()))
.ReturnsAsync(changedSnapshot);
var evidence = new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "stella://sbom/scan-123",
Digest = "sha256:original-digest",
CollectedAt = DateTimeOffset.UtcNow,
Snapshot = originalSnapshot
};
// Act
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
// Assert
result.Resolved.Should().BeTrue();
result.DigestMatches.Should().BeFalse();
result.Error.Should().Contain("mismatch");
}
[Fact]
public async Task VerifyEvidenceAsync_WithInvalidUri_ReturnsFailed()
{
// Arrange
var evidence = new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "https://invalid-scheme.com/path",
Digest = "sha256:abc123",
CollectedAt = DateTimeOffset.UtcNow,
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
};
// Act
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
// Assert
result.Resolved.Should().BeFalse();
result.Error.Should().Contain("Invalid evidence URI");
}
[Fact]
public async Task VerifyEvidenceAsync_WithNotFoundEvidence_ReturnsFailed()
{
// Arrange
_sbomResolverMock.Setup(r => r.ResolveAsync("sbom", "missing", It.IsAny<CancellationToken>()))
.ThrowsAsync(new EvidenceNotFoundException("sbom", "missing"));
var evidence = new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "stella://sbom/missing",
Digest = "sha256:abc123",
CollectedAt = DateTimeOffset.UtcNow,
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
};
// Act
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
// Assert
result.Resolved.Should().BeFalse();
result.Error.Should().Contain("not found");
}
[Fact]
public void SupportsType_WithRegisteredType_ReturnsTrue()
{
_resolver.SupportsType("sbom").Should().BeTrue();
_resolver.SupportsType("reach").Should().BeTrue();
}
[Fact]
public void SupportsType_WithUnregisteredType_ReturnsFalse()
{
_resolver.SupportsType("unknown").Should().BeFalse();
}
[Fact]
public void SupportsType_IsCaseInsensitive()
{
_resolver.SupportsType("SBOM").Should().BeTrue();
_resolver.SupportsType("Sbom").Should().BeTrue();
}
}
/// <summary>
/// Tests for <see cref="PassthroughTypeResolver"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class PassthroughTypeResolverTests
{
[Fact]
public async Task ResolveAsync_ReturnsSnapshotWithPathInfo()
{
// Arrange
var timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-01-10T10:00:00Z"));
var resolver = new PassthroughTypeResolver(["test"], timeProvider);
// Act
var snapshot = await resolver.ResolveAsync("test", "my-path", CancellationToken.None);
// Assert
snapshot.Type.Should().Be("test");
snapshot.Data.Should().ContainKey("path");
snapshot.Data["path"].Should().Be("my-path");
snapshot.Data["source"].Should().Be("passthrough");
}
[Fact]
public void SupportedTypes_ReturnsConfiguredTypes()
{
var resolver = new PassthroughTypeResolver(["a", "b", "c"], TimeProvider.System);
resolver.SupportedTypes.Should().BeEquivalentTo(["a", "b", "c"]);
}
}

View File

@@ -0,0 +1,258 @@
// <copyright file="InMemoryEvidencePackStoreTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using StellaOps.Evidence.Pack.Storage;
namespace StellaOps.Evidence.Pack.Tests;
/// <summary>
/// Tests for <see cref="InMemoryEvidencePackStore"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class InMemoryEvidencePackStoreTests
{
private readonly InMemoryEvidencePackStore _store;
public InMemoryEvidencePackStoreTests()
{
_store = new InMemoryEvidencePackStore();
}
[Fact]
public async Task SaveAsync_ThenGetByIdAsync_ReturnsPack()
{
// Arrange
var pack = CreateTestPack("pack-001", "tenant-1");
// Act
await _store.SaveAsync(pack, CancellationToken.None);
var retrieved = await _store.GetByIdAsync("tenant-1", "pack-001", CancellationToken.None);
// Assert
retrieved.Should().NotBeNull();
retrieved!.PackId.Should().Be("pack-001");
retrieved.TenantId.Should().Be("tenant-1");
}
[Fact]
public async Task GetByIdAsync_WithWrongTenant_ReturnsNull()
{
// Arrange
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
// Act
var retrieved = await _store.GetByIdAsync("tenant-2", "pack-001", CancellationToken.None);
// Assert
retrieved.Should().BeNull();
}
[Fact]
public async Task GetByIdAsync_WithWildcardTenant_FindsAnyTenant()
{
// Arrange
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
// Act
var retrieved = await _store.GetByIdAsync("*", "pack-001", CancellationToken.None);
// Assert
retrieved.Should().NotBeNull();
retrieved!.PackId.Should().Be("pack-001");
}
[Fact]
public async Task ListAsync_FiltersByTenant()
{
// Arrange
await _store.SaveAsync(CreateTestPack("pack-001", "tenant-1"), CancellationToken.None);
await _store.SaveAsync(CreateTestPack("pack-002", "tenant-1"), CancellationToken.None);
await _store.SaveAsync(CreateTestPack("pack-003", "tenant-2"), CancellationToken.None);
// Act
var results = await _store.ListAsync("tenant-1", null, CancellationToken.None);
// Assert
results.Should().HaveCount(2);
results.Should().AllSatisfy(p => p.TenantId.Should().Be("tenant-1"));
}
[Fact]
public async Task ListAsync_FiltersByCveId()
{
// Arrange
await _store.SaveAsync(CreateTestPack("pack-001", "tenant-1", cveId: "CVE-2023-1234"), CancellationToken.None);
await _store.SaveAsync(CreateTestPack("pack-002", "tenant-1", cveId: "CVE-2023-5678"), CancellationToken.None);
// Act
var query = new EvidencePackQuery { CveId = "CVE-2023-1234" };
var results = await _store.ListAsync("tenant-1", query, CancellationToken.None);
// Assert
results.Should().HaveCount(1);
results[0].Subject.CveId.Should().Be("CVE-2023-1234");
}
[Fact]
public async Task ListAsync_FiltersByRunId()
{
// Arrange
var pack1 = CreateTestPack("pack-001", "tenant-1");
var pack2 = CreateTestPack("pack-002", "tenant-1");
await _store.SaveAsync(pack1, CancellationToken.None);
await _store.SaveAsync(pack2, CancellationToken.None);
await _store.LinkToRunAsync("pack-001", "run-abc", CancellationToken.None);
// Act
var query = new EvidencePackQuery { RunId = "run-abc" };
var results = await _store.ListAsync("tenant-1", query, CancellationToken.None);
// Assert
results.Should().HaveCount(1);
results[0].PackId.Should().Be("pack-001");
}
[Fact]
public async Task ListAsync_AppliesLimit()
{
// Arrange
for (var i = 0; i < 10; i++)
{
await _store.SaveAsync(CreateTestPack($"pack-{i:D3}", "tenant-1"), CancellationToken.None);
}
// Act
var query = new EvidencePackQuery { Limit = 5 };
var results = await _store.ListAsync("tenant-1", query, CancellationToken.None);
// Assert
results.Should().HaveCount(5);
}
[Fact]
public async Task LinkToRunAsync_CreatesAssociation()
{
// Arrange
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
// Act
await _store.LinkToRunAsync("pack-001", "run-xyz", CancellationToken.None);
// Assert
var packsForRun = await _store.GetByRunIdAsync("run-xyz", CancellationToken.None);
packsForRun.Should().HaveCount(1);
packsForRun[0].PackId.Should().Be("pack-001");
}
[Fact]
public async Task LinkToRunAsync_AllowsMultiplePacksPerRun()
{
// Arrange
await _store.SaveAsync(CreateTestPack("pack-001", "tenant-1"), CancellationToken.None);
await _store.SaveAsync(CreateTestPack("pack-002", "tenant-1"), CancellationToken.None);
// Act
await _store.LinkToRunAsync("pack-001", "run-xyz", CancellationToken.None);
await _store.LinkToRunAsync("pack-002", "run-xyz", CancellationToken.None);
// Assert
var packsForRun = await _store.GetByRunIdAsync("run-xyz", CancellationToken.None);
packsForRun.Should().HaveCount(2);
}
[Fact]
public async Task GetByRunIdAsync_WithNoLinks_ReturnsEmpty()
{
// Act
var results = await _store.GetByRunIdAsync("nonexistent-run", CancellationToken.None);
// Assert
results.Should().BeEmpty();
}
[Fact]
public async Task SaveSignedAsync_ThenGetSignedByIdAsync_ReturnsSignedPack()
{
// Arrange
var pack = CreateTestPack("pack-001", "tenant-1");
var signedPack = new SignedEvidencePack
{
Pack = pack,
Envelope = new DsseEnvelope
{
PayloadType = "application/vnd.stellaops.evidence-pack+json",
Payload = "base64-content",
PayloadDigest = "sha256:abc123",
Signatures = [new DsseSignature { KeyId = "key-1", Sig = "sig-1" }]
},
SignedAt = DateTimeOffset.UtcNow
};
// Act
await _store.SaveSignedAsync(signedPack, CancellationToken.None);
var retrieved = await _store.GetSignedByIdAsync("tenant-1", "pack-001", CancellationToken.None);
// Assert
retrieved.Should().NotBeNull();
retrieved!.Pack.PackId.Should().Be("pack-001");
retrieved.Envelope.Signatures.Should().HaveCount(1);
}
[Fact]
public async Task Clear_RemovesAllData()
{
// Arrange
await _store.SaveAsync(CreateTestPack("pack-001", "tenant-1"), CancellationToken.None);
await _store.LinkToRunAsync("pack-001", "run-1", CancellationToken.None);
// Act
_store.Clear();
// Assert
var pack = await _store.GetByIdAsync("tenant-1", "pack-001", CancellationToken.None);
pack.Should().BeNull();
var packs = await _store.GetByRunIdAsync("run-1", CancellationToken.None);
packs.Should().BeEmpty();
}
private static EvidencePack CreateTestPack(string packId, string tenantId, string? cveId = null)
{
return new EvidencePack
{
PackId = packId,
Version = "1.0",
TenantId = tenantId,
CreatedAt = DateTimeOffset.UtcNow,
Subject = new EvidenceSubject
{
Type = EvidenceSubjectType.Cve,
CveId = cveId ?? "CVE-2023-44487"
},
Claims = [new EvidenceClaim
{
ClaimId = "claim-001",
Text = "Test claim",
Type = ClaimType.VulnerabilityStatus,
Status = "affected",
Confidence = 0.9,
EvidenceIds = ["ev-001"]
}],
Evidence = [new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "stella://sbom/test",
Digest = "sha256:test",
CollectedAt = DateTimeOffset.UtcNow,
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
}]
};
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>StellaOps.Evidence.Pack.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Evidence.Pack\StellaOps.Evidence.Pack.csproj" />
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>