// ----------------------------------------------------------------------------- // BundleImportServiceTests.cs // Sprint: SPRINT_20260121_036_BinaryIndex_golden_corpus_bundle_verification // Task: GCB-002 - Implement offline corpus bundle import and verification // Description: Unit tests for BundleImportService corpus bundle import and verification // ----------------------------------------------------------------------------- using System.IO.Compression; using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.BinaryIndex.GroundTruth.Reproducible.Models; using Xunit; namespace StellaOps.BinaryIndex.GroundTruth.Reproducible.Tests; public sealed class BundleImportServiceTests : IDisposable { private readonly string _tempDir; private readonly string _tempBundleDir; private readonly BundleImportService _sut; public BundleImportServiceTests() { _tempDir = Path.Combine(Path.GetTempPath(), $"import-test-{Guid.NewGuid():N}"); _tempBundleDir = Path.Combine(Path.GetTempPath(), $"bundle-test-{Guid.NewGuid():N}"); Directory.CreateDirectory(_tempDir); Directory.CreateDirectory(_tempBundleDir); var options = Options.Create(new BundleImportOptions { StagingDirectory = Path.Combine(Path.GetTempPath(), "import-staging-test") }); _sut = new BundleImportService( options, NullLogger.Instance); } public void Dispose() { if (Directory.Exists(_tempDir)) { Directory.Delete(_tempDir, recursive: true); } if (Directory.Exists(_tempBundleDir)) { Directory.Delete(_tempBundleDir, recursive: true); } } #region Validation Tests [Fact] public async Task ValidateAsync_NonexistentFile_ReturnsInvalid() { // Arrange var bundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz"); // Act var result = await _sut.ValidateAsync(bundlePath); // Assert result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("not found")); } [Fact] public async Task ValidateAsync_ValidBundle_ReturnsValid() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); // Act var result = await _sut.ValidateAsync(bundlePath); // Assert result.IsValid.Should().BeTrue(); result.Metadata.Should().NotBeNull(); result.Metadata!.BundleId.Should().NotBeNullOrEmpty(); result.Metadata.SchemaVersion.Should().Be("1.0.0"); result.Metadata.PairCount.Should().Be(1); } [Fact] public async Task ValidateAsync_MissingManifest_ReturnsInvalid() { // Arrange var bundlePath = CreateTestBundleWithoutManifest(); // Act var result = await _sut.ValidateAsync(bundlePath); // Assert result.IsValid.Should().BeFalse(); result.Errors.Should().Contain(e => e.Contains("manifest")); } #endregion #region Import Tests [Fact] public async Task ImportAsync_NonexistentFile_ReturnsFailed() { // Arrange var request = new BundleImportRequest { InputPath = Path.Combine(_tempDir, "nonexistent.tar.gz") }; // Act var result = await _sut.ImportAsync(request); // Assert result.Success.Should().BeFalse(); result.OverallStatus.Should().Be(VerificationStatus.Failed); result.Error.Should().Contain("not found"); } [Fact] public async Task ImportAsync_ValidBundle_ReturnsSuccess() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); var request = new BundleImportRequest { InputPath = bundlePath, VerifySignatures = false, VerifyTimestamps = false, VerifyDigests = true, RunMatcher = true }; // Act var result = await _sut.ImportAsync(request); // Assert result.Success.Should().BeTrue(); result.OverallStatus.Should().Be(VerificationStatus.Passed); result.Metadata.Should().NotBeNull(); result.DigestResult.Should().NotBeNull(); result.DigestResult!.Passed.Should().BeTrue(); } [Fact] public async Task ImportAsync_WithSignatureVerification_FailsForUnsignedBundle() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); var request = new BundleImportRequest { InputPath = bundlePath, VerifySignatures = true, VerifyTimestamps = false, VerifyDigests = false, RunMatcher = false }; // Act var result = await _sut.ImportAsync(request); // Assert result.SignatureResult.Should().NotBeNull(); result.SignatureResult!.Passed.Should().BeFalse(); result.OverallStatus.Should().Be(VerificationStatus.Warning); } [Fact] public async Task ImportAsync_WithPlaceholderSignature_FailsVerification() { // Arrange var bundlePath = CreateTestBundleWithPlaceholderSignature(); var request = new BundleImportRequest { InputPath = bundlePath, VerifySignatures = true, VerifyTimestamps = false, VerifyDigests = false, RunMatcher = false }; // Act var result = await _sut.ImportAsync(request); // Assert result.SignatureResult.Should().NotBeNull(); result.SignatureResult!.Passed.Should().BeFalse(); result.SignatureResult.Error.Should().Contain("placeholder"); } [Fact] public async Task ImportAsync_DigestMismatch_ReturnsFailed() { // Arrange var bundlePath = CreateTestBundleWithBadDigest(); var request = new BundleImportRequest { InputPath = bundlePath, VerifySignatures = false, VerifyTimestamps = false, VerifyDigests = true, RunMatcher = false }; // Act var result = await _sut.ImportAsync(request); // Assert result.Success.Should().BeFalse(); result.OverallStatus.Should().Be(VerificationStatus.Failed); result.DigestResult.Should().NotBeNull(); result.DigestResult!.Passed.Should().BeFalse(); result.DigestResult.Mismatches.Should().NotBeEmpty(); } [Fact] public async Task ImportAsync_WithPairVerification_VerifiesPairs() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); var request = new BundleImportRequest { InputPath = bundlePath, VerifySignatures = false, VerifyTimestamps = false, VerifyDigests = false, RunMatcher = true }; // Act var result = await _sut.ImportAsync(request); // Assert result.PairResults.Should().HaveCount(1); var pair = result.PairResults[0]; pair.Package.Should().Be("openssl"); pair.AdvisoryId.Should().Be("CVE-2024-1234"); pair.Passed.Should().BeTrue(); } [Fact] public async Task ImportAsync_WithProgress_ReportsProgress() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); var progressReports = new List(); var progress = new Progress(p => progressReports.Add(p)); var request = new BundleImportRequest { InputPath = bundlePath, VerifySignatures = false, VerifyTimestamps = false, VerifyDigests = true, RunMatcher = true }; // Act var result = await _sut.ImportAsync(request, progress); // Wait for progress reports await Task.Delay(100); // Assert result.Success.Should().BeTrue(); progressReports.Should().NotBeEmpty(); progressReports.Select(p => p.Stage).Should().Contain("Extracting bundle"); } [Fact] public async Task ImportAsync_WithCancellation_ThrowsCancelled() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); var request = new BundleImportRequest { InputPath = bundlePath }; using var cts = new CancellationTokenSource(); await cts.CancelAsync(); // Act & Assert await Assert.ThrowsAsync( () => _sut.ImportAsync(request, cancellationToken: cts.Token)); } #endregion #region Extract Tests [Fact] public async Task ExtractAsync_ValidBundle_ExtractsContents() { // Arrange var bundlePath = CreateTestBundle("openssl", "CVE-2024-1234", "debian"); var extractPath = Path.Combine(_tempDir, "extracted"); // Act var resultPath = await _sut.ExtractAsync(bundlePath, extractPath); // Assert resultPath.Should().Be(extractPath); Directory.Exists(extractPath).Should().BeTrue(); File.Exists(Path.Combine(extractPath, "manifest.json")).Should().BeTrue(); } [Fact] public async Task ExtractAsync_NonexistentFile_ThrowsException() { // Arrange var bundlePath = Path.Combine(_tempDir, "nonexistent.tar.gz"); var extractPath = Path.Combine(_tempDir, "extracted"); // Act & Assert await Assert.ThrowsAsync( () => _sut.ExtractAsync(bundlePath, extractPath)); } #endregion #region Report Generation Tests [Fact] public async Task GenerateReportAsync_MarkdownFormat_GeneratesMarkdown() { // Arrange var result = CreateTestImportResult(); var outputPath = Path.Combine(_tempDir, "report"); // Act var reportPath = await _sut.GenerateReportAsync( result, BundleReportFormat.Markdown, outputPath); // Assert reportPath.Should().EndWith(".md"); File.Exists(reportPath).Should().BeTrue(); var content = await File.ReadAllTextAsync(reportPath); content.Should().Contain("# Bundle Verification Report"); content.Should().Contain("PASSED"); } [Fact] public async Task GenerateReportAsync_JsonFormat_GeneratesJson() { // Arrange var result = CreateTestImportResult(); var outputPath = Path.Combine(_tempDir, "report"); // Act var reportPath = await _sut.GenerateReportAsync( result, BundleReportFormat.Json, outputPath); // Assert reportPath.Should().EndWith(".json"); File.Exists(reportPath).Should().BeTrue(); var content = await File.ReadAllTextAsync(reportPath); var json = JsonDocument.Parse(content); json.RootElement.GetProperty("success").GetBoolean().Should().BeTrue(); json.RootElement.GetProperty("overallStatus").GetString().Should().Be("Passed"); } [Fact] public async Task GenerateReportAsync_HtmlFormat_GeneratesHtml() { // Arrange var result = CreateTestImportResult(); var outputPath = Path.Combine(_tempDir, "report"); // Act var reportPath = await _sut.GenerateReportAsync( result, BundleReportFormat.Html, outputPath); // Assert reportPath.Should().EndWith(".html"); File.Exists(reportPath).Should().BeTrue(); var content = await File.ReadAllTextAsync(reportPath); content.Should().Contain("() }; File.WriteAllText( Path.Combine(stagingDir, "manifest.json"), JsonSerializer.Serialize(manifest)); // Create placeholder signature var signature = new { signatureType = "cosign", keyId = "test-key", placeholder = true, message = "Signing integration pending" }; File.WriteAllText( Path.Combine(stagingDir, "manifest.json.sig"), JsonSerializer.Serialize(signature)); return CreateTarball(stagingDir); } private string CreateTestBundleWithBadDigest() { var stagingDir = Path.Combine(_tempBundleDir, Guid.NewGuid().ToString("N")); Directory.CreateDirectory(stagingDir); // Create pairs directory var pairId = "openssl-CVE-2024-1234-debian"; var pairDir = Path.Combine(stagingDir, "pairs", pairId); Directory.CreateDirectory(pairDir); // Create SBOM with content that won't match the digest var sbom = new { spdxVersion = "SPDX-3.0.1", name = "openssl-sbom" }; File.WriteAllText( Path.Combine(pairDir, "sbom.spdx.json"), JsonSerializer.Serialize(sbom)); // Create manifest with wrong digest var manifest = new { bundleId = $"test-bundle-{Guid.NewGuid():N}", schemaVersion = "1.0.0", createdAt = DateTimeOffset.UtcNow, generator = "Test", pairs = new[] { new { pairId, package = "openssl", advisoryId = "CVE-2024-1234", distribution = "debian", sbomDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000", // Wrong! deltaSigDigest = (string?)null } } }; File.WriteAllText( Path.Combine(stagingDir, "manifest.json"), JsonSerializer.Serialize(manifest)); return CreateTarball(stagingDir); } private string CreateTarball(string sourceDir) { var tarPath = Path.Combine(_tempBundleDir, $"{Guid.NewGuid():N}.tar.gz"); // Create tar var tempTar = Path.GetTempFileName(); try { using (var tarStream = File.Create(tempTar)) { System.Formats.Tar.TarFile.CreateFromDirectory( sourceDir, tarStream, includeBaseDirectory: false); } // Gzip it using var inputStream = File.OpenRead(tempTar); using var outputStream = File.Create(tarPath); using var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal); inputStream.CopyTo(gzipStream); } finally { if (File.Exists(tempTar)) { File.Delete(tempTar); } // Cleanup staging Directory.Delete(sourceDir, recursive: true); } return tarPath; } private static string ComputeHash(byte[] data) { var hash = System.Security.Cryptography.SHA256.HashData(data); return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; } private static BundleImportResult CreateTestImportResult() { return new BundleImportResult { Success = true, OverallStatus = VerificationStatus.Passed, ManifestDigest = "sha256:abc123", Metadata = new BundleMetadata { BundleId = "test-bundle", SchemaVersion = "1.0.0", CreatedAt = DateTimeOffset.UtcNow, Generator = "Test", PairCount = 1, TotalSizeBytes = 1024 }, SignatureResult = new SignatureVerificationResult { Passed = true, SignatureCount = 1, SignerKeyIds = ["test-key"] }, DigestResult = new DigestVerificationResult { Passed = true, TotalBlobs = 2, MatchedBlobs = 2 }, PairResults = [ new PairVerificationResult { PairId = "openssl-CVE-2024-1234-debian", Package = "openssl", AdvisoryId = "CVE-2024-1234", Passed = true, SbomStatus = VerificationStatus.Passed, DeltaSigStatus = VerificationStatus.Passed, MatcherStatus = VerificationStatus.Passed, FunctionMatchRate = 0.95, Duration = TimeSpan.FromSeconds(1.5) } ], Duration = TimeSpan.FromSeconds(5) }; } #endregion }