Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AuditPackExportServiceTests.cs
|
||||
// Sprint: SPRINT_1227_0005_0003_FE_copy_audit_export
|
||||
// Task: T10 — Unit tests for AuditPackExportService
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.AuditPack.Tests;
|
||||
|
||||
using StellaOps.AuditPack.Models;
|
||||
using StellaOps.AuditPack.Services;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public class AuditPackExportServiceTests
|
||||
{
|
||||
private readonly AuditPackExportService _service;
|
||||
|
||||
public AuditPackExportServiceTests()
|
||||
{
|
||||
_service = new AuditPackExportService(
|
||||
new MockAuditBundleWriter(),
|
||||
null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_Zip_CreatesValidZipArchive()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-123",
|
||||
Format = ExportFormat.Zip,
|
||||
Segments = [ExportSegment.Sbom, ExportSegment.Match],
|
||||
IncludeAttestations = true,
|
||||
IncludeProofChain = false,
|
||||
Filename = "test-export"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExportAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ContentType.Should().Be("application/zip");
|
||||
result.Filename.Should().Be("test-export.zip");
|
||||
result.Data.Should().NotBeNull();
|
||||
result.SizeBytes.Should().BeGreaterThan(0);
|
||||
|
||||
// Verify ZIP structure
|
||||
using var memoryStream = new MemoryStream(result.Data!);
|
||||
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read);
|
||||
archive.Entries.Should().Contain(e => e.FullName == "manifest.json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_Json_CreatesSingleJsonFile()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-456",
|
||||
Format = ExportFormat.Json,
|
||||
Segments = [ExportSegment.Sbom],
|
||||
IncludeAttestations = false,
|
||||
IncludeProofChain = false,
|
||||
Filename = "test-json"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExportAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ContentType.Should().Be("application/json");
|
||||
result.Filename.Should().Be("test-json.json");
|
||||
|
||||
// Verify JSON structure
|
||||
using var jsonDoc = JsonDocument.Parse(result.Data!);
|
||||
var root = jsonDoc.RootElement;
|
||||
root.TryGetProperty("scanId", out var scanIdProp).Should().BeTrue();
|
||||
scanIdProp.GetString().Should().Be("scan-456");
|
||||
root.TryGetProperty("segments", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_Dsse_CreatesDsseEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-789",
|
||||
Format = ExportFormat.Dsse,
|
||||
Segments = [ExportSegment.Policy],
|
||||
IncludeAttestations = true,
|
||||
IncludeProofChain = true,
|
||||
Filename = "test-dsse"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExportAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.ContentType.Should().Be("application/vnd.dsse+json");
|
||||
result.Filename.Should().Be("test-dsse.dsse.json");
|
||||
|
||||
// Verify DSSE structure
|
||||
using var jsonDoc = JsonDocument.Parse(result.Data!);
|
||||
var root = jsonDoc.RootElement;
|
||||
root.TryGetProperty("payloadType", out var payloadType).Should().BeTrue();
|
||||
payloadType.GetString().Should().Be("application/vnd.stellaops.audit-pack+json");
|
||||
root.TryGetProperty("payload", out _).Should().BeTrue();
|
||||
root.TryGetProperty("signatures", out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_AllSegments_IncludesAllInZip()
|
||||
{
|
||||
// Arrange
|
||||
var allSegments = new[]
|
||||
{
|
||||
ExportSegment.Sbom,
|
||||
ExportSegment.Match,
|
||||
ExportSegment.Reachability,
|
||||
ExportSegment.Guards,
|
||||
ExportSegment.Runtime,
|
||||
ExportSegment.Policy
|
||||
};
|
||||
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-full",
|
||||
Format = ExportFormat.Zip,
|
||||
Segments = allSegments,
|
||||
IncludeAttestations = true,
|
||||
IncludeProofChain = true,
|
||||
Filename = "full-export"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExportAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
|
||||
using var memoryStream = new MemoryStream(result.Data!);
|
||||
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read);
|
||||
|
||||
// Should have manifest + 6 segments + attestations + proof chain
|
||||
archive.Entries.Count.Should().BeGreaterThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_EmptySegments_StillCreatesValidExport()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-empty",
|
||||
Format = ExportFormat.Json,
|
||||
Segments = [],
|
||||
IncludeAttestations = false,
|
||||
IncludeProofChain = false,
|
||||
Filename = "empty-export"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExportAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeTrue();
|
||||
result.Data.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_UnsupportedFormat_ReturnsFailed()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-fail",
|
||||
Format = (ExportFormat)999, // Invalid format
|
||||
Segments = [ExportSegment.Sbom],
|
||||
IncludeAttestations = false,
|
||||
IncludeProofChain = false,
|
||||
Filename = "fail-export"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExportAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Success.Should().BeFalse();
|
||||
result.Error.Should().Contain("Unsupported");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExportAsync_CancellationRequested_ThrowsOperationCanceled()
|
||||
{
|
||||
// Arrange
|
||||
var request = new ExportRequest
|
||||
{
|
||||
ScanId = "scan-cancel",
|
||||
Format = ExportFormat.Zip,
|
||||
Segments = [ExportSegment.Sbom],
|
||||
IncludeAttestations = false,
|
||||
IncludeProofChain = false,
|
||||
Filename = "cancel-export"
|
||||
};
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => _service.ExportAsync(request, cts.Token));
|
||||
}
|
||||
|
||||
// Mock implementation for testing
|
||||
private class MockAuditBundleWriter : IAuditBundleWriter
|
||||
{
|
||||
public Task<AuditBundleWriteResult> WriteAsync(
|
||||
AuditBundleWriteRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(new AuditBundleWriteResult { Success = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ReplayAttestationServiceTests.cs
|
||||
// Sprint: SPRINT_1227_0005_0004_BE_verdict_replay
|
||||
// Task: T8 — Unit tests for ReplayAttestationService
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.AuditPack.Tests;
|
||||
|
||||
using StellaOps.AuditPack.Models;
|
||||
using StellaOps.AuditPack.Services;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public class ReplayAttestationServiceTests
|
||||
{
|
||||
private readonly ReplayAttestationService _service;
|
||||
|
||||
public ReplayAttestationServiceTests()
|
||||
{
|
||||
_service = new ReplayAttestationService(null);
|
||||
}
|
||||
|
||||
private static AuditBundleManifest CreateTestManifest(string bundleId = "bundle-123")
|
||||
{
|
||||
return new AuditBundleManifest
|
||||
{
|
||||
BundleId = bundleId,
|
||||
Name = "Test Bundle",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ScanId = "scan-123",
|
||||
ImageRef = "docker.io/library/alpine:3.18",
|
||||
ImageDigest = "sha256:abc123",
|
||||
MerkleRoot = "sha256:merkle-root",
|
||||
VerdictDigest = "sha256:verdict-digest",
|
||||
Decision = "pass",
|
||||
Inputs = new InputDigests
|
||||
{
|
||||
SbomDigest = "sha256:sbom-digest",
|
||||
FeedsDigest = "sha256:feeds-digest",
|
||||
PolicyDigest = "sha256:policy-digest"
|
||||
},
|
||||
Files = ImmutableArray<BundleFileEntry>.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static ReplayExecutionResult CreateTestResult(bool match = true)
|
||||
{
|
||||
return new ReplayExecutionResult
|
||||
{
|
||||
Success = true,
|
||||
Status = match ? ReplayStatus.Match : ReplayStatus.Drift,
|
||||
VerdictMatches = match,
|
||||
DecisionMatches = match,
|
||||
OriginalVerdictDigest = "sha256:verdict-digest",
|
||||
ReplayedVerdictDigest = match ? "sha256:verdict-digest" : "sha256:different-digest",
|
||||
OriginalDecision = "pass",
|
||||
ReplayedDecision = match ? "pass" : "warn",
|
||||
Drifts = match ? [] : [new DriftItem { Type = DriftType.Decision, Field = "decision" }],
|
||||
DurationMs = 150,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_CreatesValidAttestation()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult(match: true);
|
||||
|
||||
// Act
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
|
||||
// Assert
|
||||
attestation.Should().NotBeNull();
|
||||
attestation.AttestationId.Should().NotBeNullOrEmpty();
|
||||
attestation.ManifestId.Should().Be("bundle-123");
|
||||
attestation.Match.Should().BeTrue();
|
||||
attestation.ReplayStatus.Should().Be("Match");
|
||||
attestation.Statement.Should().NotBeNull();
|
||||
attestation.StatementDigest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_IncludesInTotoStatement()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult();
|
||||
|
||||
// Act
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
|
||||
// Assert
|
||||
attestation.Statement.Type.Should().Be("https://in-toto.io/Statement/v1");
|
||||
attestation.Statement.PredicateType.Should().Be("https://stellaops.io/attestation/verdict-replay/v1");
|
||||
attestation.Statement.Subject.Should().HaveCount(1);
|
||||
attestation.Statement.Subject[0].Name.Should().StartWith("verdict:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_IncludesPredicate()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult();
|
||||
|
||||
// Act
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
var predicate = attestation.Statement.Predicate;
|
||||
|
||||
// Assert
|
||||
predicate.ManifestId.Should().Be("bundle-123");
|
||||
predicate.ScanId.Should().Be("scan-123");
|
||||
predicate.ImageRef.Should().Be("docker.io/library/alpine:3.18");
|
||||
predicate.Match.Should().BeTrue();
|
||||
predicate.Status.Should().Be("Match");
|
||||
predicate.DurationMs.Should().Be(150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_CreatesDsseEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult();
|
||||
|
||||
// Act
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
|
||||
// Assert
|
||||
attestation.Envelope.Should().NotBeNull();
|
||||
attestation.Envelope!.PayloadType.Should().Be("application/vnd.in-toto+json");
|
||||
attestation.Envelope.Payload.Should().NotBeNullOrEmpty();
|
||||
// Without signer, signatures should be empty
|
||||
attestation.Envelope.Signatures.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_DriftResult_RecordsDivergence()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult(match: false);
|
||||
|
||||
// Act
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
var predicate = attestation.Statement.Predicate;
|
||||
|
||||
// Assert
|
||||
attestation.Match.Should().BeFalse();
|
||||
attestation.ReplayStatus.Should().Be("Drift");
|
||||
predicate.Match.Should().BeFalse();
|
||||
predicate.DriftCount.Should().Be(1);
|
||||
predicate.Drifts.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidAttestation_ReturnsValid()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult();
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
|
||||
// Act
|
||||
var verificationResult = await _service.VerifyAsync(attestation);
|
||||
|
||||
// Assert
|
||||
verificationResult.IsValid.Should().BeTrue();
|
||||
verificationResult.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateBatchAsync_MultipleReplays_CreatesMultipleAttestations()
|
||||
{
|
||||
// Arrange
|
||||
var replays = new[]
|
||||
{
|
||||
(CreateTestManifest("bundle-1"), CreateTestResult()),
|
||||
(CreateTestManifest("bundle-2"), CreateTestResult(match: false)),
|
||||
(CreateTestManifest("bundle-3"), CreateTestResult())
|
||||
};
|
||||
|
||||
// Act
|
||||
var attestations = await _service.GenerateBatchAsync(replays);
|
||||
|
||||
// Assert
|
||||
attestations.Should().HaveCount(3);
|
||||
attestations[0].ManifestId.Should().Be("bundle-1");
|
||||
attestations[1].ManifestId.Should().Be("bundle-2");
|
||||
attestations[2].ManifestId.Should().Be("bundle-3");
|
||||
attestations[1].Match.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_ComputesInputsDigest()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult();
|
||||
|
||||
// Act
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
var predicate = attestation.Statement.Predicate;
|
||||
|
||||
// Assert
|
||||
predicate.InputsDigest.Should().StartWith("sha256:");
|
||||
predicate.InputsDigest.Should().HaveLength(71); // sha256: + 64 hex chars
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_ThrowsForNullManifest()
|
||||
{
|
||||
// Arrange
|
||||
var result = CreateTestResult();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(
|
||||
() => _service.GenerateAsync(null!, result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_ThrowsForNullResult()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(
|
||||
() => _service.GenerateAsync(manifest, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_TamperedPayload_ReturnsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var manifest = CreateTestManifest();
|
||||
var result = CreateTestResult();
|
||||
var attestation = await _service.GenerateAsync(manifest, result);
|
||||
|
||||
// Tamper with the envelope payload
|
||||
var tamperedAttestation = attestation with
|
||||
{
|
||||
Envelope = attestation.Envelope! with
|
||||
{
|
||||
Payload = Convert.ToBase64String(new byte[] { 1, 2, 3 })
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var verificationResult = await _service.VerifyAsync(tamperedAttestation);
|
||||
|
||||
// Assert
|
||||
verificationResult.IsValid.Should().BeFalse();
|
||||
verificationResult.Errors.Should().Contain(e => e.Contains("payload digest"));
|
||||
}
|
||||
}
|
||||
@@ -10,22 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.AuditPack\StellaOps.AuditPack.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.AuditPack\StellaOps.AuditPack.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user