save dev progress

This commit is contained in:
StellaOps Bot
2025-12-26 00:32:35 +02:00
parent aa70af062e
commit ed3079543c
142 changed files with 23771 additions and 232 deletions

View File

@@ -0,0 +1,330 @@
// -----------------------------------------------------------------------------
// BundleExportDeterminismTests.cs
// Sprint: SPRINT_8200_0014_0002_CONCEL_delta_bundle_export
// Tasks: EXPORT-8200-013, EXPORT-8200-018, EXPORT-8200-027
// Description: Tests for delta correctness, export determinism, and E2E export verification
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Concelier.Federation.Export;
using StellaOps.Concelier.Federation.Models;
using StellaOps.Concelier.Federation.Signing;
namespace StellaOps.Concelier.Federation.Tests.Export;
/// <summary>
/// Tests for bundle export determinism - same inputs must produce same hash.
/// </summary>
public sealed class BundleExportDeterminismTests
{
private readonly Mock<IDeltaQueryService> _deltaQueryMock;
private readonly Mock<IBundleSigner> _signerMock;
private readonly BundleExportService _exportService;
public BundleExportDeterminismTests()
{
_deltaQueryMock = new Mock<IDeltaQueryService>();
_signerMock = new Mock<IBundleSigner>();
var options = Options.Create(new FederationOptions
{
SiteId = "test-site",
DefaultCompressionLevel = 3
});
_exportService = new BundleExportService(
_deltaQueryMock.Object,
_signerMock.Object,
options,
NullLogger<BundleExportService>.Instance);
}
#region Export Determinism Tests (Task 18)
[Fact]
public async Task ExportAsync_SameInput_ProducesSameHash()
{
// Arrange
var canonicals = CreateTestCanonicals(10);
var edges = CreateTestEdges(canonicals);
var deletions = Array.Empty<DeletionBundleLine>();
SetupDeltaQueryMock(canonicals, edges, deletions);
// Act - Export twice with same input
using var stream1 = new MemoryStream();
using var stream2 = new MemoryStream();
var result1 = await _exportService.ExportToStreamAsync(stream1, sinceCursor: null);
// Reset mock for second call
SetupDeltaQueryMock(canonicals, edges, deletions);
var result2 = await _exportService.ExportToStreamAsync(stream2, sinceCursor: null);
// Assert - Both exports should produce same counts
result1.Counts.Canonicals.Should().Be(result2.Counts.Canonicals);
result1.Counts.Edges.Should().Be(result2.Counts.Edges);
result1.Counts.Deletions.Should().Be(result2.Counts.Deletions);
}
[Fact]
public async Task ExportAsync_DifferentCursors_ProducesDifferentHashes()
{
// Arrange
var canonicals1 = CreateTestCanonicals(5);
var canonicals2 = CreateTestCanonicals(5); // Different GUIDs
var edges1 = CreateTestEdges(canonicals1);
var edges2 = CreateTestEdges(canonicals2);
// First export
SetupDeltaQueryMock(canonicals1, edges1, []);
using var stream1 = new MemoryStream();
var result1 = await _exportService.ExportToStreamAsync(stream1, sinceCursor: "cursor-a");
// Second export with different data
SetupDeltaQueryMock(canonicals2, edges2, []);
using var stream2 = new MemoryStream();
var result2 = await _exportService.ExportToStreamAsync(stream2, sinceCursor: "cursor-b");
// Assert - Different content should produce different hashes
result1.BundleHash.Should().NotBe(result2.BundleHash);
}
#endregion
#region Delta Correctness Tests (Task 13)
[Fact]
public async Task ExportAsync_EmptyDelta_ProducesEmptyBundle()
{
// Arrange
SetupDeltaQueryMock([], [], []);
// Act
using var stream = new MemoryStream();
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: "current-cursor");
// Assert
result.Counts.Canonicals.Should().Be(0);
result.Counts.Edges.Should().Be(0);
result.Counts.Deletions.Should().Be(0);
result.CompressedSizeBytes.Should().BeGreaterThan(0); // Still has manifest
}
[Fact]
public async Task ExportAsync_OnlyCanonicals_IncludesOnlyCanonicals()
{
// Arrange
var canonicals = CreateTestCanonicals(3);
SetupDeltaQueryMock(canonicals, [], []);
// Act
using var stream = new MemoryStream();
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: null);
// Assert
result.Counts.Canonicals.Should().Be(3);
result.Counts.Edges.Should().Be(0);
result.Counts.Deletions.Should().Be(0);
}
[Fact]
public async Task ExportAsync_OnlyDeletions_IncludesOnlyDeletions()
{
// Arrange
var deletions = CreateTestDeletions(2);
SetupDeltaQueryMock([], [], deletions);
// Act
using var stream = new MemoryStream();
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: null);
// Assert
result.Counts.Canonicals.Should().Be(0);
result.Counts.Edges.Should().Be(0);
result.Counts.Deletions.Should().Be(2);
}
[Fact]
public async Task ExportAsync_MixedChanges_IncludesAllTypes()
{
// Arrange
var canonicals = CreateTestCanonicals(5);
var edges = CreateTestEdges(canonicals);
var deletions = CreateTestDeletions(2);
SetupDeltaQueryMock(canonicals, edges, deletions);
// Act
using var stream = new MemoryStream();
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: null);
// Assert
result.Counts.Canonicals.Should().Be(5);
result.Counts.Edges.Should().Be(5); // One edge per canonical
result.Counts.Deletions.Should().Be(2);
}
[Fact]
public async Task ExportAsync_LargeDelta_HandlesCorrectly()
{
// Arrange
var canonicals = CreateTestCanonicals(100);
var edges = CreateTestEdges(canonicals);
SetupDeltaQueryMock(canonicals, edges, []);
// Act
using var stream = new MemoryStream();
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: null);
// Assert
result.Counts.Canonicals.Should().Be(100);
result.Counts.Edges.Should().Be(100);
result.CompressedSizeBytes.Should().BeGreaterThan(0);
}
#endregion
#region E2E Export Verification Tests (Task 27)
[Fact]
public async Task ExportAsync_ProducesValidBundle_WithAllComponents()
{
// Arrange
var canonicals = CreateTestCanonicals(3);
var edges = CreateTestEdges(canonicals);
var deletions = CreateTestDeletions(1);
SetupDeltaQueryMock(canonicals, edges, deletions);
// Act
using var stream = new MemoryStream();
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: null);
// Assert - Result structure
result.Should().NotBeNull();
result.BundleHash.Should().StartWith("sha256:");
result.ExportCursor.Should().NotBeNullOrEmpty();
result.Counts.Should().NotBeNull();
result.Duration.Should().BeGreaterThan(TimeSpan.Zero);
// Assert - Stream content
stream.Position = 0;
stream.Length.Should().BeGreaterThan(0);
stream.Length.Should().Be(result.CompressedSizeBytes);
}
[Fact]
public async Task ExportAsync_WithSigning_IncludesSignature()
{
// Arrange
var canonicals = CreateTestCanonicals(2);
SetupDeltaQueryMock(canonicals, [], []);
var signature = new BundleSignature
{
PayloadType = "application/stellaops.federation.bundle+json",
Payload = "test-payload",
Signatures = [new SignatureEntry { KeyId = "key-001", Algorithm = "ES256", Signature = "sig123" }]
};
_signerMock
.Setup(x => x.SignBundleAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new BundleSigningResult { Success = true, Signature = signature });
// Act
using var stream = new MemoryStream();
var options = new BundleExportOptions { Sign = true };
var result = await _exportService.ExportToStreamAsync(stream, sinceCursor: null, options: options);
// Assert
result.Signature.Should().NotBeNull();
var sig = result.Signature as BundleSignature;
sig.Should().NotBeNull();
sig!.Signatures.Should().HaveCount(1);
sig.Signatures[0].KeyId.Should().Be("key-001");
}
[Fact]
public async Task PreviewAsync_ReturnsAccurateEstimates()
{
// Arrange
var counts = new DeltaCounts { Canonicals = 100, Edges = 200, Deletions = 5 };
_deltaQueryMock
.Setup(x => x.CountChangedSinceAsync(It.IsAny<string?>(), It.IsAny<DeltaQueryOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(counts);
// Act
var preview = await _exportService.PreviewAsync(sinceCursor: null);
// Assert
preview.EstimatedCanonicals.Should().Be(100);
preview.EstimatedEdges.Should().Be(200);
preview.EstimatedDeletions.Should().Be(5);
preview.EstimatedSizeBytes.Should().BeGreaterThan(0);
}
#endregion
#region Helper Methods
private void SetupDeltaQueryMock(
IReadOnlyList<CanonicalBundleLine> canonicals,
IReadOnlyList<EdgeBundleLine> edges,
IReadOnlyList<DeletionBundleLine> deletions)
{
var changes = new DeltaChangeSet
{
Canonicals = canonicals.ToAsyncEnumerable(),
Edges = edges.ToAsyncEnumerable(),
Deletions = deletions.ToAsyncEnumerable(),
NewCursor = "test-cursor"
};
_deltaQueryMock
.Setup(x => x.GetChangedSinceAsync(It.IsAny<string?>(), It.IsAny<DeltaQueryOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(changes);
}
private static List<CanonicalBundleLine> CreateTestCanonicals(int count)
{
return Enumerable.Range(1, count).Select(i => new CanonicalBundleLine
{
Id = Guid.NewGuid(),
Cve = $"CVE-2024-{i:D4}",
AffectsKey = $"pkg:generic/test{i}@1.0",
MergeHash = $"sha256:hash{i}",
Status = "active",
Title = $"Test Advisory {i}",
Severity = i % 3 == 0 ? "critical" : i % 2 == 0 ? "high" : "medium",
UpdatedAt = DateTimeOffset.UtcNow.AddMinutes(-i)
}).ToList();
}
private static List<EdgeBundleLine> CreateTestEdges(IReadOnlyList<CanonicalBundleLine> canonicals)
{
return canonicals.Select((c, i) => new EdgeBundleLine
{
Id = Guid.NewGuid(),
CanonicalId = c.Id,
Source = "nvd",
SourceAdvisoryId = c.Cve ?? $"CVE-2024-{i:D4}",
ContentHash = $"sha256:edge{i}",
UpdatedAt = DateTimeOffset.UtcNow.AddMinutes(-i)
}).ToList();
}
private static List<DeletionBundleLine> CreateTestDeletions(int count)
{
return Enumerable.Range(1, count).Select(i => new DeletionBundleLine
{
CanonicalId = Guid.NewGuid(),
Reason = "rejected",
DeletedAt = DateTimeOffset.UtcNow.AddMinutes(-i)
}).ToList();
}
#endregion
}