494 lines
16 KiB
C#
494 lines
16 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// Tests for ProvenanceScopeService lifecycle operations.
|
|
/// Covers Task 17 (BACKPORT-8200-017) from SPRINT_8200_0015_0001.
|
|
/// </summary>
|
|
public sealed class ProvenanceScopeLifecycleTests
|
|
{
|
|
private readonly Mock<IProvenanceScopeStore> _storeMock;
|
|
private readonly Mock<IBackportEvidenceResolver> _resolverMock;
|
|
private readonly ProvenanceScopeService _service;
|
|
|
|
public ProvenanceScopeLifecycleTests()
|
|
{
|
|
_storeMock = new Mock<IProvenanceScopeStore>();
|
|
_resolverMock = new Mock<IBackportEvidenceResolver>();
|
|
_service = new ProvenanceScopeService(
|
|
_storeMock.Object,
|
|
NullLogger<ProvenanceScopeService>.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<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((ProvenanceScope?)null);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.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<ProvenanceScope>(s =>
|
|
s.CanonicalId == canonicalId &&
|
|
s.DistroRelease.Contains("debian") &&
|
|
s.BackportSemver == "7.64.0-4+deb11u2"),
|
|
It.IsAny<CancellationToken>()),
|
|
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<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(existingScope);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.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<CancellationToken>()))
|
|
.ReturnsAsync(evidence);
|
|
|
|
_storeMock
|
|
.Setup(x => x.GetByCanonicalAndDistroAsync(canonicalId, It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((ProvenanceScope?)null);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(Guid.NewGuid());
|
|
|
|
// Act
|
|
var result = await _service.CreateOrUpdateAsync(request);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
|
|
_storeMock.Verify(x => x.UpsertAsync(
|
|
It.Is<ProvenanceScope>(s =>
|
|
s.Confidence == 0.95 &&
|
|
s.BackportSemver == "1.1.1n-0+deb11u6" &&
|
|
s.PatchId == "abc123def456abc123def456abc123def456abc123"),
|
|
It.IsAny<CancellationToken>()),
|
|
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<string>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((ProvenanceScope?)null);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.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<CancellationToken>()))
|
|
.ReturnsAsync((ProvenanceScope?)null);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.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<ProvenanceScope>(s =>
|
|
s.DistroRelease == "debian:bookworm" &&
|
|
s.Confidence == 0.85 &&
|
|
s.PatchId == "patchheader-commit-sha"),
|
|
It.IsAny<CancellationToken>()),
|
|
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<CancellationToken>()))
|
|
.ReturnsAsync(existingScope);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.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<ProvenanceScope>(s =>
|
|
s.Confidence == 0.95 &&
|
|
s.PatchId == "abc123"),
|
|
It.IsAny<CancellationToken>()),
|
|
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<CancellationToken>()))
|
|
.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<ProvenanceScope>(),
|
|
It.IsAny<CancellationToken>()),
|
|
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<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Act
|
|
await _service.LinkEvidenceRefAsync(scopeId, evidenceRef);
|
|
|
|
// Assert
|
|
_storeMock.Verify(x => x.LinkEvidenceRefAsync(scopeId, evidenceRef, It.IsAny<CancellationToken>()), 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<ProvenanceScope>
|
|
{
|
|
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<CancellationToken>()))
|
|
.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<CancellationToken>()))
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Act
|
|
await _service.DeleteByCanonicalIdAsync(canonicalId);
|
|
|
|
// Assert
|
|
_storeMock.Verify(x => x.DeleteByCanonicalIdAsync(canonicalId, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
|
|
.ReturnsAsync((ProvenanceScope?)null);
|
|
|
|
_storeMock
|
|
.Setup(x => x.UpsertAsync(It.IsAny<ProvenanceScope>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(Guid.NewGuid());
|
|
|
|
// Act
|
|
await _service.CreateOrUpdateAsync(request);
|
|
|
|
// Assert
|
|
_storeMock.Verify(x => x.UpsertAsync(
|
|
It.Is<ProvenanceScope>(s => s.DistroRelease == expectedDistro),
|
|
It.IsAny<CancellationToken>()),
|
|
Times.Once);
|
|
}
|
|
|
|
#endregion
|
|
}
|