// -----------------------------------------------------------------------------
// 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
}