// ----------------------------------------------------------------------------- // ProvenanceScopeLifecycleTests.cs // Sprint: SPRINT_8200_0015_0001_CONCEL_backport_integration // Task: BACKPORT-8200-017 // Description: Tests for provenance scope lifecycle management // ----------------------------------------------------------------------------- using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Concelier.Merge.Backport; using StellaOps.TestKit; namespace StellaOps.Concelier.Merge.Tests; /// /// Tests for ProvenanceScopeService lifecycle operations. /// Covers Task 17 (BACKPORT-8200-017) from SPRINT_8200_0015_0001. /// public sealed class ProvenanceScopeLifecycleTests { private readonly Mock _storeMock; private readonly Mock _resolverMock; private readonly ProvenanceScopeService _service; public ProvenanceScopeLifecycleTests() { _storeMock = new Mock(); _resolverMock = new Mock(); _service = new ProvenanceScopeService( _storeMock.Object, NullLogger.Instance, _resolverMock.Object); } #region CreateOrUpdateAsync Tests [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateOrUpdateAsync_NewScope_CreatesProvenanceScope() { // Arrange var canonicalId = Guid.NewGuid(); var request = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = "CVE-2024-1234", PackagePurl = "pkg:deb/debian/curl@7.64.0-4+deb11u1", Source = "debian", FixedVersion = "7.64.0-4+deb11u2", PatchLineage = "abc123def456" }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny(), It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Guid.NewGuid()); // Act var result = await _service.CreateOrUpdateAsync(request); // Assert result.Success.Should().BeTrue(); result.WasCreated.Should().BeTrue(); result.ProvenanceScopeId.Should().NotBeNull(); _storeMock.Verify(x => x.UpsertAsync( It.Is(s => s.CanonicalId == canonicalId && s.DistroRelease.Contains("debian") && s.BackportSemver == "7.64.0-4+deb11u2"), It.IsAny()), Times.Once); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateOrUpdateAsync_ExistingScope_UpdatesProvenanceScope() { // Arrange var canonicalId = Guid.NewGuid(); var existingScopeId = Guid.NewGuid(); var request = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = "CVE-2024-5678", PackagePurl = "pkg:rpm/redhat/nginx@1.20.1-14.el9", Source = "redhat", FixedVersion = "1.20.1-14.el9_2.1" }; var existingScope = new ProvenanceScope { Id = existingScopeId, CanonicalId = canonicalId, DistroRelease = "redhat:9", BackportSemver = "1.20.1-14.el9", Confidence = 0.5, CreatedAt = DateTimeOffset.UtcNow.AddHours(-1), UpdatedAt = DateTimeOffset.UtcNow.AddHours(-1) }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny(), It.IsAny())) .ReturnsAsync(existingScope); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(existingScopeId); // Act var result = await _service.CreateOrUpdateAsync(request); // Assert result.Success.Should().BeTrue(); result.WasCreated.Should().BeFalse(); result.ProvenanceScopeId.Should().Be(existingScopeId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateOrUpdateAsync_WithEvidenceResolver_ResolvesEvidence() { // Arrange var canonicalId = Guid.NewGuid(); var request = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = "CVE-2024-1234", PackagePurl = "pkg:deb/debian/openssl@1.1.1n-0+deb11u5", Source = "debian", ResolveEvidence = true }; var evidence = new BackportEvidence { CveId = "CVE-2024-1234", PackagePurl = request.PackagePurl, DistroRelease = "debian:bullseye", Tier = BackportEvidenceTier.DistroAdvisory, Confidence = 0.95, PatchId = "abc123def456abc123def456abc123def456abc123", BackportVersion = "1.1.1n-0+deb11u6", PatchOrigin = PatchOrigin.Upstream, EvidenceDate = DateTimeOffset.UtcNow }; _resolverMock .Setup(x => x.ResolveAsync(request.CveId, request.PackagePurl, It.IsAny())) .ReturnsAsync(evidence); _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny(), It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Guid.NewGuid()); // Act var result = await _service.CreateOrUpdateAsync(request); // Assert result.Success.Should().BeTrue(); _storeMock.Verify(x => x.UpsertAsync( It.Is(s => s.Confidence == 0.95 && s.BackportSemver == "1.1.1n-0+deb11u6" && s.PatchId == "abc123def456abc123def456abc123def456abc123"), It.IsAny()), Times.Once); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task CreateOrUpdateAsync_NonDistroSource_StillCreatesScope() { // Arrange var canonicalId = Guid.NewGuid(); var request = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = "CVE-2024-VENDOR", PackagePurl = "pkg:generic/product@1.0.0", Source = "nvd", // Non-distro source ResolveEvidence = false }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny(), It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Guid.NewGuid()); // Act var result = await _service.CreateOrUpdateAsync(request); // Assert result.Success.Should().BeTrue(); } #endregion #region UpdateFromEvidenceAsync Tests [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpdateFromEvidenceAsync_NewEvidence_CreatesScope() { // Arrange var canonicalId = Guid.NewGuid(); var evidence = new BackportEvidence { CveId = "CVE-2024-1234", PackagePurl = "pkg:deb/debian/bash@5.1", DistroRelease = "debian:bookworm", Tier = BackportEvidenceTier.PatchHeader, Confidence = 0.85, PatchId = "patchheader-commit-sha", BackportVersion = "5.1-7+deb12u1", PatchOrigin = PatchOrigin.Upstream, EvidenceDate = DateTimeOffset.UtcNow }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, "debian:bookworm", It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Guid.NewGuid()); // Act var result = await _service.UpdateFromEvidenceAsync(canonicalId, evidence); // Assert result.Success.Should().BeTrue(); result.WasCreated.Should().BeTrue(); _storeMock.Verify(x => x.UpsertAsync( It.Is(s => s.DistroRelease == "debian:bookworm" && s.Confidence == 0.85 && s.PatchId == "patchheader-commit-sha"), It.IsAny()), Times.Once); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpdateFromEvidenceAsync_BetterEvidence_UpdatesScope() { // Arrange var canonicalId = Guid.NewGuid(); var existingScopeId = Guid.NewGuid(); var existingScope = new ProvenanceScope { Id = existingScopeId, CanonicalId = canonicalId, DistroRelease = "debian:bookworm", Confidence = 0.5, PatchId = null, CreatedAt = DateTimeOffset.UtcNow.AddDays(-1), UpdatedAt = DateTimeOffset.UtcNow.AddDays(-1) }; var betterEvidence = new BackportEvidence { CveId = "CVE-2024-1234", PackagePurl = "pkg:deb/debian/test@1.0", DistroRelease = "debian:bookworm", Tier = BackportEvidenceTier.DistroAdvisory, Confidence = 0.95, // Higher confidence PatchId = "abc123", BackportVersion = "1.0-fixed", PatchOrigin = PatchOrigin.Distro, EvidenceDate = DateTimeOffset.UtcNow }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, "debian:bookworm", It.IsAny())) .ReturnsAsync(existingScope); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(existingScopeId); // Act var result = await _service.UpdateFromEvidenceAsync(canonicalId, betterEvidence); // Assert result.Success.Should().BeTrue(); result.WasCreated.Should().BeFalse(); _storeMock.Verify(x => x.UpsertAsync( It.Is(s => s.Confidence == 0.95 && s.PatchId == "abc123"), It.IsAny()), Times.Once); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task UpdateFromEvidenceAsync_LowerConfidenceEvidence_SkipsUpdate() { // Arrange var canonicalId = Guid.NewGuid(); var existingScopeId = Guid.NewGuid(); var existingScope = new ProvenanceScope { Id = existingScopeId, CanonicalId = canonicalId, DistroRelease = "redhat:9", Confidence = 0.9, // High confidence PatchId = "existing-patch-id", CreatedAt = DateTimeOffset.UtcNow.AddDays(-1), UpdatedAt = DateTimeOffset.UtcNow.AddDays(-1) }; var lowerEvidence = new BackportEvidence { CveId = "CVE-2024-1234", PackagePurl = "pkg:rpm/redhat/test@1.0", DistroRelease = "redhat:9", Tier = BackportEvidenceTier.BinaryFingerprint, Confidence = 0.6, // Lower confidence PatchId = "new-patch-id", PatchOrigin = PatchOrigin.Upstream, EvidenceDate = DateTimeOffset.UtcNow }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, "redhat:9", It.IsAny())) .ReturnsAsync(existingScope); // Act var result = await _service.UpdateFromEvidenceAsync(canonicalId, lowerEvidence); // Assert result.Success.Should().BeTrue(); result.ProvenanceScopeId.Should().Be(existingScopeId); // Should not call upsert since confidence is lower _storeMock.Verify(x => x.UpsertAsync( It.IsAny(), It.IsAny()), Times.Never); } #endregion #region LinkEvidenceRefAsync Tests [Trait("Category", TestCategories.Unit)] [Fact] public async Task LinkEvidenceRefAsync_LinksEvidenceToScope() { // Arrange var scopeId = Guid.NewGuid(); var evidenceRef = Guid.NewGuid(); _storeMock .Setup(x => x.LinkEvidenceRefAsync(scopeId, evidenceRef, It.IsAny())) .Returns(Task.CompletedTask); // Act await _service.LinkEvidenceRefAsync(scopeId, evidenceRef); // Assert _storeMock.Verify(x => x.LinkEvidenceRefAsync(scopeId, evidenceRef, It.IsAny()), Times.Once); } #endregion #region GetByCanonicalIdAsync Tests [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetByCanonicalIdAsync_ReturnsAllScopes() { // Arrange var canonicalId = Guid.NewGuid(); var scopes = new List { new() { Id = Guid.NewGuid(), CanonicalId = canonicalId, DistroRelease = "debian:bookworm", Confidence = 0.9, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow }, new() { Id = Guid.NewGuid(), CanonicalId = canonicalId, DistroRelease = "ubuntu:22.04", Confidence = 0.85, CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow } }; _storeMock .Setup(x => x.GetByCanonicalIdAsync(canonicalId, It.IsAny())) .ReturnsAsync(scopes); // Act var result = await _service.GetByCanonicalIdAsync(canonicalId); // Assert result.Should().HaveCount(2); result.Should().Contain(s => s.DistroRelease == "debian:bookworm"); result.Should().Contain(s => s.DistroRelease == "ubuntu:22.04"); } #endregion #region DeleteByCanonicalIdAsync Tests [Trait("Category", TestCategories.Unit)] [Fact] public async Task DeleteByCanonicalIdAsync_DeletesAllScopes() { // Arrange var canonicalId = Guid.NewGuid(); _storeMock .Setup(x => x.DeleteByCanonicalIdAsync(canonicalId, It.IsAny())) .Returns(Task.CompletedTask); // Act await _service.DeleteByCanonicalIdAsync(canonicalId); // Assert _storeMock.Verify(x => x.DeleteByCanonicalIdAsync(canonicalId, It.IsAny()), Times.Once); } #endregion #region Distro Release Extraction Tests [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("pkg:deb/debian/curl@7.64.0-4+deb11u1", "debian", "debian:bullseye")] [InlineData("pkg:deb/debian/openssl@3.0.11-1~deb12u2", "debian", "debian:bookworm")] [InlineData("pkg:rpm/redhat/nginx@1.20.1-14.el9", "redhat", "redhat:9")] [InlineData("pkg:rpm/redhat/kernel@5.14.0-284.el8", "redhat", "redhat:8")] [InlineData("pkg:deb/ubuntu/curl@7.81.0-1ubuntu1.14~22.04", "ubuntu", "ubuntu:22.04")] public async Task CreateOrUpdateAsync_ExtractsCorrectDistroRelease( string purl, string source, string expectedDistro) { // Arrange var canonicalId = Guid.NewGuid(); var request = new ProvenanceScopeRequest { CanonicalId = canonicalId, CveId = "CVE-2024-TEST", PackagePurl = purl, Source = source, ResolveEvidence = false }; _storeMock .Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, expectedDistro, It.IsAny())) .ReturnsAsync((ProvenanceScope?)null); _storeMock .Setup(x => x.UpsertAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(Guid.NewGuid()); // Act await _service.CreateOrUpdateAsync(request); // Assert _storeMock.Verify(x => x.UpsertAsync( It.Is(s => s.DistroRelease == expectedDistro), It.IsAny()), Times.Once); } #endregion }