stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -0,0 +1,66 @@
// <copyright file="EvidenceCardServiceTests.CreateCard.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceCardServiceTests
{
[Fact]
public async Task CreateCardAsync_WithValidRequest_ReturnsCardAsync()
{
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);
}
[Fact]
public async Task CreateCardAsync_SetsGeneratedAtFromTimeProviderAsync()
{
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);
}
[Fact]
public async Task CreateCardAsync_WithComponentPurl_ExtractsComponentInfoAsync()
{
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);
}
}

View File

@@ -0,0 +1,66 @@
// <copyright file="EvidenceCardServiceTests.Export.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Text;
using System.Text.Json;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceCardServiceTests
{
[Fact]
public async Task ExportCardAsync_Json_ReturnsValidJsonAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(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);
}
[Fact]
public async Task ExportCardAsync_CompactJson_IsSmallerThanIndentedAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(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);
}
[Fact]
public async Task ExportCardAsync_CanonicalJson_IsDeterministicAsync()
{
var service1 = CreateService();
var service2 = CreateService();
var card1 = await CreateTestCardAsync(service1);
var card2 = await CreateTestCardAsync(service2);
var export1 = await service1.ExportCardAsync(card1, EvidenceCardExportFormat.CanonicalJson);
var export2 = await service2.ExportCardAsync(card2, EvidenceCardExportFormat.CanonicalJson);
Assert.Equal(export1.ContentDigest, export2.ContentDigest);
}
[Fact]
public async Task ExportCardAsync_SetsCorrectFileNameAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(service);
var export = await service.ExportCardAsync(card, EvidenceCardExportFormat.Json);
Assert.Equal($"evidence-card-{card.CardId}.json", export.FileName);
}
}

View File

@@ -0,0 +1,44 @@
// <copyright file="EvidenceCardServiceTests.Helpers.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System;
using System.Threading.Tasks;
using StellaOps.Determinism;
using StellaOps.Evidence.Pack;
using StellaOps.Evidence.Pack.Models;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceCardServiceTests
{
private async Task<EvidenceCard> CreateTestCardAsync(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;
}
}

View File

@@ -0,0 +1,87 @@
// <copyright file="EvidenceCardServiceTests.Verify.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceCardServiceTests
{
[Fact]
public async Task VerifyCardAsync_ValidCard_ReturnsValidAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(service);
var result = await service.VerifyCardAsync(card);
Assert.True(result.Valid);
Assert.True(result.SignatureValid);
Assert.True(result.SbomDigestValid);
}
[Fact]
public async Task VerifyCardAsync_WithMissingReceipt_AllowedByDefaultAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(service);
var result = await service.VerifyCardAsync(card, new EvidenceCardVerificationOptions
{
AllowMissingReceipt = true
});
Assert.True(result.Valid);
Assert.Null(result.RekorReceiptValid);
}
[Fact]
public async Task VerifyCardAsync_WithMissingReceipt_FailsWhenRequiredAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(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"));
}
[Fact]
public async Task VerifyCardAsync_WithValidRekorReceipt_ReturnsTrueAsync()
{
var service = CreateService();
var card = await CreateTestCardAsync(service);
var cardWithReceipt = card with
{
RekorReceipt = new RekorReceiptMetadata
{
Uuid = "abc123def456",
LogIndex = 12345,
LogId = "0x1234",
LogUrl = "https://rekor.sigstore.dev",
IntegratedTime = _timeProvider.GetUtcNow().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);
}
}

View File

@@ -3,222 +3,20 @@
// 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 System;
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
[Trait("Category", TestCategories.Unit)]
public sealed partial 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(
@@ -226,35 +24,4 @@ public sealed class EvidenceCardServiceTests
_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;
}
}

View File

@@ -0,0 +1,82 @@
// <copyright file="EvidencePackServiceTests.Create.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidencePackServiceTests
{
[Fact]
public async Task CreateAsync_WithValidInput_CreatesPackAsync()
{
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"
};
var pack = await _service.CreateAsync(claims, evidence, subject, context, CancellationToken.None);
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());
var stored = await _store.GetByIdAsync("tenant-123", pack.PackId, CancellationToken.None);
stored.Should().NotBeNull();
}
[Fact]
public async Task CreateAsync_LinksToRunWhenProvidedAsync()
{
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" };
var pack = await _service.CreateAsync(claims, evidence, subject, context, CancellationToken.None);
var packsForRun = await _store.GetByRunIdAsync("run-xyz", CancellationToken.None);
packsForRun.Should().Contain(p => p.PackId == pack.PackId);
}
}

View File

@@ -0,0 +1,56 @@
// <copyright file="EvidencePackServiceTests.CreateFromRun.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidencePackServiceTests
{
[Fact]
public async Task CreateFromRunAsync_AggregatesExistingPacksAsync()
{
var subject = new EvidenceSubject { Type = EvidenceSubjectType.Finding, FindingId = "finding-123" };
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);
var aggregated = await _service.CreateFromRunAsync("run-agg", subject, CancellationToken.None);
aggregated.Claims.Should().HaveCount(2);
aggregated.Evidence.Should().HaveCount(2);
}
}

View File

@@ -0,0 +1,46 @@
// <copyright file="EvidencePackServiceTests.Export.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Text;
using System.Threading;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidencePackServiceTests
{
[Fact]
public async Task ExportAsync_AsJson_ReturnsValidJsonAsync()
{
var pack = await CreateTestPackAsync();
var export = await _service.ExportAsync(pack.PackId, EvidencePackExportFormat.Json, CancellationToken.None);
export.Format.Should().Be(EvidencePackExportFormat.Json);
export.ContentType.Should().Be("application/json");
export.FileName.Should().EndWith(".json");
var json = Encoding.UTF8.GetString(export.Content);
json.Should().Contain(pack.PackId);
}
[Fact]
public async Task ExportAsync_AsMarkdown_ReturnsReadableMarkdownAsync()
{
var pack = await CreateTestPackAsync();
var export = await _service.ExportAsync(pack.PackId, EvidencePackExportFormat.Markdown, CancellationToken.None);
export.Format.Should().Be(EvidencePackExportFormat.Markdown);
export.ContentType.Should().Be("text/markdown");
export.FileName.Should().EndWith(".md");
var markdown = Encoding.UTF8.GetString(export.Content);
markdown.Should().Contain("# Evidence Pack");
markdown.Should().Contain(pack.PackId);
markdown.Should().Contain("## Claims");
markdown.Should().Contain("## Evidence");
}
}

View File

@@ -0,0 +1,55 @@
// <copyright file="EvidencePackServiceTests.Helpers.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Evidence.Pack.Models;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidencePackServiceTests
{
private async Task<EvidencePack> CreateTestPackAsync()
{
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)
};
}
}

View File

@@ -0,0 +1,38 @@
// <copyright file="EvidencePackServiceTests.Sign.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidencePackServiceTests
{
[Fact]
public async Task SignAsync_CreatesSignedPackAsync()
{
var pack = await CreateTestPackAsync();
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);
var signedPack = await _service.SignAsync(pack, CancellationToken.None);
signedPack.Pack.Should().Be(pack);
signedPack.Envelope.Should().Be(expectedEnvelope);
signedPack.SignedAt.Should().Be(_timeProvider.GetUtcNow());
var stored = await _store.GetSignedByIdAsync(pack.TenantId, pack.PackId, CancellationToken.None);
stored.Should().NotBeNull();
}
}

View File

@@ -0,0 +1,87 @@
// <copyright file="EvidencePackServiceTests.Verify.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidencePackServiceTests
{
[Fact]
public async Task VerifyAsync_WithValidPack_ReturnsSuccessAsync()
{
var pack = await CreateTestPackAsync();
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
});
var result = await _service.VerifyAsync(signedPack, CancellationToken.None);
result.Valid.Should().BeTrue();
result.Issues.Should().BeEmpty();
}
[Fact]
public async Task VerifyAsync_WithInvalidSignature_ReturnsFailedAsync()
{
var pack = await CreateTestPackAsync();
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
});
var result = await _service.VerifyAsync(signedPack, CancellationToken.None);
result.Valid.Should().BeFalse();
result.Issues.Should().Contain(i => i.Contains("Signature verification failed"));
}
}

View File

@@ -1,14 +1,12 @@
// <copyright file="EvidencePackServiceTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using StellaOps.Evidence.Pack;
using StellaOps.Evidence.Pack.Storage;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
@@ -16,12 +14,12 @@ namespace StellaOps.Evidence.Pack.Tests;
/// Tests for <see cref="EvidencePackService"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class EvidencePackServiceTests
public sealed partial class EvidencePackServiceTests
{
private readonly InMemoryEvidencePackStore _store;
private readonly Mock<IEvidenceResolver> _resolverMock;
private readonly Mock<IEvidencePackSigner> _signerMock;
private readonly FakeTimeProvider _timeProvider;
private readonly FixedTimeProvider _timeProvider;
private readonly EvidencePackService _service;
public EvidencePackServiceTests()
@@ -29,7 +27,7 @@ public sealed class EvidencePackServiceTests
_store = new InMemoryEvidencePackStore();
_resolverMock = new Mock<IEvidenceResolver>();
_signerMock = new Mock<IEvidencePackSigner>();
_timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-01-10T10:00:00Z"));
_timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 10, 10, 0, 0, TimeSpan.Zero));
_service = new EvidencePackService(
_store,
@@ -38,320 +36,4 @@ public sealed class EvidencePackServiceTests
_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,38 @@
// <copyright file="EvidenceResolverTests.Resolve.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System;
using System.Threading;
using FluentAssertions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceResolverTests
{
[Fact]
public async Task ResolveAndSnapshotAsync_WithSupportedType_ReturnsSnapshotAsync()
{
var expectedSnapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100);
_sbomResolverMock.Setup(r => r.ResolveAsync("sbom", "scan-123", It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedSnapshot);
var result = await _resolver.ResolveAndSnapshotAsync("sbom", "scan-123", CancellationToken.None);
result.Snapshot.Should().Be(expectedSnapshot);
result.Digest.Should().StartWith("sha256:");
result.ResolvedAt.Should().NotBe(default(DateTimeOffset));
result.ResolvedAt.Offset.Should().Be(TimeSpan.Zero);
}
[Fact]
public async Task ResolveAndSnapshotAsync_WithUnsupportedType_ThrowsExceptionAsync()
{
var act = () => _resolver.ResolveAndSnapshotAsync("unknown", "path", CancellationToken.None);
await act.Should().ThrowAsync<UnsupportedEvidenceTypeException>()
.Where(e => e.Type == "unknown");
}
}

View File

@@ -0,0 +1,30 @@
// <copyright file="EvidenceResolverTests.Supports.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using FluentAssertions;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceResolverTests
{
[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();
}
}

View File

@@ -0,0 +1,106 @@
// <copyright file="EvidenceResolverTests.Verify.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class EvidenceResolverTests
{
[Fact]
public async Task VerifyEvidenceAsync_WithMatchingDigest_ReturnsSuccessAsync()
{
var snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100);
_sbomResolverMock.Setup(r => r.ResolveAsync("sbom", "scan-123", It.IsAny<CancellationToken>()))
.ReturnsAsync(snapshot);
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 = FixedCollectedAt,
Snapshot = snapshot
};
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
result.Resolved.Should().BeTrue();
result.DigestMatches.Should().BeTrue();
result.Error.Should().BeNull();
}
[Fact]
public async Task VerifyEvidenceAsync_WithMismatchedDigest_ReturnsFailedAsync()
{
var originalSnapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100);
var changedSnapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 150);
_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 = FixedCollectedAt,
Snapshot = originalSnapshot
};
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
result.Resolved.Should().BeTrue();
result.DigestMatches.Should().BeFalse();
result.Error.Should().Contain("mismatch");
}
[Fact]
public async Task VerifyEvidenceAsync_WithInvalidUri_ReturnsFailedAsync()
{
var evidence = new EvidenceItem
{
EvidenceId = "ev-001",
Type = EvidenceType.Sbom,
Uri = "https://invalid-scheme.com/path",
Digest = "sha256:abc123",
CollectedAt = FixedCollectedAt,
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
};
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
result.Resolved.Should().BeFalse();
result.Error.Should().Contain("Invalid evidence URI");
}
[Fact]
public async Task VerifyEvidenceAsync_WithNotFoundEvidence_ReturnsFailedAsync()
{
_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 = FixedCollectedAt,
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
};
var result = await _resolver.VerifyEvidenceAsync(evidence, CancellationToken.None);
result.Resolved.Should().BeFalse();
result.Error.Should().Contain("not found");
}
}

View File

@@ -1,14 +1,11 @@
// <copyright file="EvidenceResolverTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Evidence.Pack.Models;
using StellaOps.Evidence.Pack.Resolvers;
using StellaOps.Evidence.Pack;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
@@ -16,8 +13,9 @@ namespace StellaOps.Evidence.Pack.Tests;
/// Tests for <see cref="EvidenceResolver"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class EvidenceResolverTests
public sealed partial class EvidenceResolverTests
{
private static readonly DateTimeOffset FixedCollectedAt = new(2026, 1, 10, 10, 0, 0, TimeSpan.Zero);
private readonly Mock<ITypeResolver> _sbomResolverMock;
private readonly Mock<ITypeResolver> _reachResolverMock;
private readonly EvidenceResolver _resolver;
@@ -34,190 +32,4 @@ public sealed class EvidenceResolverTests
[_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,22 @@
// <copyright file="FixedTimeProvider.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System;
namespace StellaOps.Evidence.Pack.Tests;
internal sealed class FixedTimeProvider : TimeProvider
{
private DateTimeOffset _utcNow;
public FixedTimeProvider(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,25 @@
// <copyright file="InMemoryEvidencePackStoreTests.Clear.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class InMemoryEvidencePackStoreTests
{
[Fact]
public async Task Clear_RemovesAllDataAsync()
{
await _store.SaveAsync(CreateTestPack("pack-001", "tenant-1"), CancellationToken.None);
await _store.LinkToRunAsync("pack-001", "run-1", CancellationToken.None);
_store.Clear();
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();
}
}

View File

@@ -0,0 +1,47 @@
// <copyright file="InMemoryEvidencePackStoreTests.GetById.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class InMemoryEvidencePackStoreTests
{
[Fact]
public async Task SaveAsync_ThenGetByIdAsync_ReturnsPackAsync()
{
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
var retrieved = await _store.GetByIdAsync("tenant-1", "pack-001", CancellationToken.None);
retrieved.Should().NotBeNull();
retrieved!.PackId.Should().Be("pack-001");
retrieved.TenantId.Should().Be("tenant-1");
}
[Fact]
public async Task GetByIdAsync_WithWrongTenant_ReturnsNullAsync()
{
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
var retrieved = await _store.GetByIdAsync("tenant-2", "pack-001", CancellationToken.None);
retrieved.Should().BeNull();
}
[Fact]
public async Task GetByIdAsync_WithWildcardTenant_FindsAnyTenantAsync()
{
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
var retrieved = await _store.GetByIdAsync("*", "pack-001", CancellationToken.None);
retrieved.Should().NotBeNull();
retrieved!.PackId.Should().Be("pack-001");
}
}

View File

@@ -0,0 +1,49 @@
// <copyright file="InMemoryEvidencePackStoreTests.Helpers.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using StellaOps.Evidence.Pack.Models;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class InMemoryEvidencePackStoreTests
{
private static EvidencePack CreateTestPack(string packId, string tenantId, string? cveId = null)
{
return new EvidencePack
{
PackId = packId,
Version = "1.0",
TenantId = tenantId,
CreatedAt = FixedTimestamp,
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 = FixedTimestamp,
Snapshot = EvidenceSnapshot.Sbom("cyclonedx", "1.4", 100)
}
]
};
}
}

View File

@@ -0,0 +1,68 @@
// <copyright file="InMemoryEvidencePackStoreTests.List.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class InMemoryEvidencePackStoreTests
{
[Fact]
public async Task ListAsync_FiltersByTenantAsync()
{
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);
var results = await _store.ListAsync("tenant-1", null, CancellationToken.None);
results.Should().HaveCount(2);
results.Should().AllSatisfy(p => p.TenantId.Should().Be("tenant-1"));
}
[Fact]
public async Task ListAsync_FiltersByCveIdAsync()
{
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);
var query = new EvidencePackQuery { CveId = "CVE-2023-1234" };
var results = await _store.ListAsync("tenant-1", query, CancellationToken.None);
results.Should().HaveCount(1);
results[0].Subject.CveId.Should().Be("CVE-2023-1234");
}
[Fact]
public async Task ListAsync_FiltersByRunIdAsync()
{
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);
var query = new EvidencePackQuery { RunId = "run-abc" };
var results = await _store.ListAsync("tenant-1", query, CancellationToken.None);
results.Should().HaveCount(1);
results[0].PackId.Should().Be("pack-001");
}
[Fact]
public async Task ListAsync_AppliesLimitAsync()
{
for (var i = 0; i < 10; i++)
{
await _store.SaveAsync(CreateTestPack($"pack-{i:D3}", "tenant-1"), CancellationToken.None);
}
var query = new EvidencePackQuery { Limit = 5 };
var results = await _store.ListAsync("tenant-1", query, CancellationToken.None);
results.Should().HaveCount(5);
}
}

View File

@@ -0,0 +1,45 @@
// <copyright file="InMemoryEvidencePackStoreTests.RunLinks.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class InMemoryEvidencePackStoreTests
{
[Fact]
public async Task LinkToRunAsync_CreatesAssociationAsync()
{
var pack = CreateTestPack("pack-001", "tenant-1");
await _store.SaveAsync(pack, CancellationToken.None);
await _store.LinkToRunAsync("pack-001", "run-xyz", CancellationToken.None);
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_AllowsMultiplePacksPerRunAsync()
{
await _store.SaveAsync(CreateTestPack("pack-001", "tenant-1"), CancellationToken.None);
await _store.SaveAsync(CreateTestPack("pack-002", "tenant-1"), CancellationToken.None);
await _store.LinkToRunAsync("pack-001", "run-xyz", CancellationToken.None);
await _store.LinkToRunAsync("pack-002", "run-xyz", CancellationToken.None);
var packsForRun = await _store.GetByRunIdAsync("run-xyz", CancellationToken.None);
packsForRun.Should().HaveCount(2);
}
[Fact]
public async Task GetByRunIdAsync_WithNoLinks_ReturnsEmptyAsync()
{
var results = await _store.GetByRunIdAsync("nonexistent-run", CancellationToken.None);
results.Should().BeEmpty();
}
}

View File

@@ -0,0 +1,37 @@
// <copyright file="InMemoryEvidencePackStoreTests.Signed.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Threading;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
public sealed partial class InMemoryEvidencePackStoreTests
{
[Fact]
public async Task SaveSignedAsync_ThenGetSignedByIdAsync_ReturnsSignedPackAsync()
{
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 = FixedTimestamp
};
await _store.SaveSignedAsync(signedPack, CancellationToken.None);
var retrieved = await _store.GetSignedByIdAsync("tenant-1", "pack-001", CancellationToken.None);
retrieved.Should().NotBeNull();
retrieved!.Pack.PackId.Should().Be("pack-001");
retrieved.Envelope.Signatures.Should().HaveCount(1);
}
}

View File

@@ -1,11 +1,9 @@
// <copyright file="InMemoryEvidencePackStoreTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using FluentAssertions;
using StellaOps.Evidence.Pack.Models;
using System;
using StellaOps.Evidence.Pack.Storage;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
@@ -13,246 +11,13 @@ namespace StellaOps.Evidence.Pack.Tests;
/// Tests for <see cref="InMemoryEvidencePackStore"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class InMemoryEvidencePackStoreTests
public sealed partial class InMemoryEvidencePackStoreTests
{
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 10, 10, 0, 0, TimeSpan.Zero);
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,39 @@
// <copyright file="PassthroughTypeResolverTests.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System;
using System.Threading;
using FluentAssertions;
using StellaOps.Evidence.Pack.Resolvers;
using Xunit;
namespace StellaOps.Evidence.Pack.Tests;
/// <summary>
/// Tests for <see cref="PassthroughTypeResolver"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class PassthroughTypeResolverTests
{
[Fact]
public async Task ResolveAsync_ReturnsSnapshotWithPathInfoAsync()
{
var timeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 1, 10, 10, 0, 0, TimeSpan.Zero));
var resolver = new PassthroughTypeResolver(["test"], timeProvider);
var snapshot = await resolver.ResolveAsync("test", "my-path", CancellationToken.None);
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

@@ -4,5 +4,5 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/__Libraries/__Tests/StellaOps.Evidence.Pack.Tests/StellaOps.Evidence.Pack.Tests.md. |
| REMED-05 | DONE | Remediation complete; async naming, deterministic fixtures, file split <= 100 lines; ConfigureAwait(false) skipped in tests per xUnit1030; dotnet test passed 2026-02-03 (42 tests). |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |