stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user