audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
using StellaOps.Concelier.BackportProof.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.BackportProof.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for PackageEcosystem enum.
|
||||
/// </summary>
|
||||
public sealed class PackageEcosystemTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(PackageEcosystem.Deb)]
|
||||
[InlineData(PackageEcosystem.Rpm)]
|
||||
[InlineData(PackageEcosystem.Apk)]
|
||||
[InlineData(PackageEcosystem.Unknown)]
|
||||
public void PackageEcosystem_AllValues_AreDefined(PackageEcosystem ecosystem)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(ecosystem));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackageEcosystem_AllValues_AreCounted()
|
||||
{
|
||||
var values = Enum.GetValues<PackageEcosystem>();
|
||||
Assert.Equal(4, values.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ProductContext record.
|
||||
/// </summary>
|
||||
public sealed class ProductContextTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProductContext_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var context = new ProductContext(
|
||||
Distro: "debian",
|
||||
Release: "bookworm",
|
||||
RepoScope: "main",
|
||||
Architecture: "amd64");
|
||||
|
||||
Assert.Equal("debian", context.Distro);
|
||||
Assert.Equal("bookworm", context.Release);
|
||||
Assert.Equal("main", context.RepoScope);
|
||||
Assert.Equal("amd64", context.Architecture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductContext_OptionalProperties_CanBeNull()
|
||||
{
|
||||
var context = new ProductContext(
|
||||
Distro: "alpine",
|
||||
Release: "3.19",
|
||||
RepoScope: null,
|
||||
Architecture: null);
|
||||
|
||||
Assert.Null(context.RepoScope);
|
||||
Assert.Null(context.Architecture);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProductContext_RecordEquality_WorksCorrectly()
|
||||
{
|
||||
var c1 = new ProductContext("rhel", "9", "main", "x86_64");
|
||||
var c2 = new ProductContext("rhel", "9", "main", "x86_64");
|
||||
|
||||
Assert.Equal(c1, c2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for PackageKey record.
|
||||
/// </summary>
|
||||
public sealed class PackageKeyTests
|
||||
{
|
||||
[Fact]
|
||||
public void PackageKey_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var key = new PackageKey(
|
||||
Ecosystem: PackageEcosystem.Deb,
|
||||
PackageName: "nginx",
|
||||
SourcePackageName: "nginx");
|
||||
|
||||
Assert.Equal(PackageEcosystem.Deb, key.Ecosystem);
|
||||
Assert.Equal("nginx", key.PackageName);
|
||||
Assert.Equal("nginx", key.SourcePackageName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackageKey_SourcePackage_CanBeNull()
|
||||
{
|
||||
var key = new PackageKey(
|
||||
Ecosystem: PackageEcosystem.Rpm,
|
||||
PackageName: "httpd",
|
||||
SourcePackageName: null);
|
||||
|
||||
Assert.Null(key.SourcePackageName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for EvidenceTier enum.
|
||||
/// </summary>
|
||||
public sealed class EvidenceTierTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(EvidenceTier.Unknown, 0)]
|
||||
[InlineData(EvidenceTier.NvdRange, 5)]
|
||||
[InlineData(EvidenceTier.UpstreamCommit, 4)]
|
||||
[InlineData(EvidenceTier.SourcePatch, 3)]
|
||||
[InlineData(EvidenceTier.Changelog, 2)]
|
||||
[InlineData(EvidenceTier.DistroOval, 1)]
|
||||
public void EvidenceTier_Values_HaveCorrectNumericValue(EvidenceTier tier, int expected)
|
||||
{
|
||||
Assert.Equal(expected, (int)tier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTier_DistroOval_IsHighestConfidence()
|
||||
{
|
||||
// Tier 1 is highest confidence (lowest numeric value)
|
||||
var allTiers = Enum.GetValues<EvidenceTier>().Where(t => t != EvidenceTier.Unknown);
|
||||
var highestConfidence = allTiers.OrderBy(t => (int)t).First();
|
||||
|
||||
Assert.Equal(EvidenceTier.DistroOval, highestConfidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidenceTier_NvdRange_IsLowestConfidence()
|
||||
{
|
||||
// Tier 5 is lowest confidence (highest numeric value)
|
||||
var allTiers = Enum.GetValues<EvidenceTier>().Where(t => t != EvidenceTier.Unknown);
|
||||
var lowestConfidence = allTiers.OrderByDescending(t => (int)t).First();
|
||||
|
||||
Assert.Equal(EvidenceTier.NvdRange, lowestConfidence);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for FixStatus enum.
|
||||
/// </summary>
|
||||
public sealed class FixStatusTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(FixStatus.Patched)]
|
||||
[InlineData(FixStatus.Vulnerable)]
|
||||
[InlineData(FixStatus.NotAffected)]
|
||||
[InlineData(FixStatus.WontFix)]
|
||||
[InlineData(FixStatus.UnderInvestigation)]
|
||||
[InlineData(FixStatus.Unknown)]
|
||||
public void FixStatus_AllValues_AreDefined(FixStatus status)
|
||||
{
|
||||
Assert.True(Enum.IsDefined(status));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FixStatus_AllValues_AreCounted()
|
||||
{
|
||||
var values = Enum.GetValues<FixStatus>();
|
||||
Assert.Equal(6, values.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for RulePriority enum.
|
||||
/// </summary>
|
||||
public sealed class RulePriorityTests
|
||||
{
|
||||
[Fact]
|
||||
public void RulePriority_DistroNativeOval_IsHighestPriority()
|
||||
{
|
||||
var allPriorities = Enum.GetValues<RulePriority>();
|
||||
var highest = allPriorities.Max(p => (int)p);
|
||||
|
||||
Assert.Equal((int)RulePriority.DistroNativeOval, highest);
|
||||
Assert.Equal(100, highest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RulePriority_NvdRangeHeuristic_IsLowestPriority()
|
||||
{
|
||||
var allPriorities = Enum.GetValues<RulePriority>();
|
||||
var lowest = allPriorities.Min(p => (int)p);
|
||||
|
||||
Assert.Equal((int)RulePriority.NvdRangeHeuristic, lowest);
|
||||
Assert.Equal(20, lowest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RulePriority_LegacyAliases_MatchNewValues()
|
||||
{
|
||||
Assert.Equal(RulePriority.DistroNativeOval, RulePriority.DistroNative);
|
||||
Assert.Equal(RulePriority.ChangelogExplicitCve, RulePriority.VendorCsaf);
|
||||
Assert.Equal(RulePriority.NvdRangeHeuristic, RulePriority.ThirdParty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(RulePriority.NvdRangeHeuristic, 20)]
|
||||
[InlineData(RulePriority.UpstreamCommitPartialMatch, 45)]
|
||||
[InlineData(RulePriority.UpstreamCommitExactParity, 55)]
|
||||
[InlineData(RulePriority.SourcePatchFuzzyMatch, 60)]
|
||||
[InlineData(RulePriority.SourcePatchExactMatch, 70)]
|
||||
[InlineData(RulePriority.ChangelogBugIdMapped, 75)]
|
||||
[InlineData(RulePriority.ChangelogExplicitCve, 85)]
|
||||
[InlineData(RulePriority.DerivativeOvalMedium, 90)]
|
||||
[InlineData(RulePriority.DerivativeOvalHigh, 95)]
|
||||
[InlineData(RulePriority.DistroNativeOval, 100)]
|
||||
public void RulePriority_Values_HaveCorrectNumericValue(RulePriority priority, int expected)
|
||||
{
|
||||
Assert.Equal(expected, (int)priority);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for EvidencePointer record.
|
||||
/// </summary>
|
||||
public sealed class EvidencePointerTests
|
||||
{
|
||||
[Fact]
|
||||
public void EvidencePointer_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var fetchedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var pointer = new EvidencePointer(
|
||||
SourceType: "debian-tracker",
|
||||
SourceUrl: "https://security-tracker.debian.org/tracker/CVE-2024-0001",
|
||||
SourceDigest: "sha256:abc123",
|
||||
FetchedAt: fetchedAt,
|
||||
TierSource: EvidenceTier.DistroOval);
|
||||
|
||||
Assert.Equal("debian-tracker", pointer.SourceType);
|
||||
Assert.Equal("https://security-tracker.debian.org/tracker/CVE-2024-0001", pointer.SourceUrl);
|
||||
Assert.Equal("sha256:abc123", pointer.SourceDigest);
|
||||
Assert.Equal(fetchedAt, pointer.FetchedAt);
|
||||
Assert.Equal(EvidenceTier.DistroOval, pointer.TierSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvidencePointer_TierSource_DefaultsToUnknown()
|
||||
{
|
||||
var pointer = new EvidencePointer(
|
||||
SourceType: "nvd",
|
||||
SourceUrl: "https://nvd.nist.gov/vuln/detail/CVE-2024-0001",
|
||||
SourceDigest: null,
|
||||
FetchedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.Equal(EvidenceTier.Unknown, pointer.TierSource);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for VersionRange record.
|
||||
/// </summary>
|
||||
public sealed class VersionRangeTests
|
||||
{
|
||||
[Fact]
|
||||
public void VersionRange_FullRange_ContainsAllBoundaries()
|
||||
{
|
||||
var range = new VersionRange(
|
||||
MinVersion: "1.0.0",
|
||||
MinInclusive: true,
|
||||
MaxVersion: "2.0.0",
|
||||
MaxInclusive: false);
|
||||
|
||||
Assert.Equal("1.0.0", range.MinVersion);
|
||||
Assert.True(range.MinInclusive);
|
||||
Assert.Equal("2.0.0", range.MaxVersion);
|
||||
Assert.False(range.MaxInclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VersionRange_OpenEnded_AllowsNullBoundaries()
|
||||
{
|
||||
// All versions up to 2.0.0 (exclusive)
|
||||
var range = new VersionRange(
|
||||
MinVersion: null,
|
||||
MinInclusive: false,
|
||||
MaxVersion: "2.0.0",
|
||||
MaxInclusive: false);
|
||||
|
||||
Assert.Null(range.MinVersion);
|
||||
Assert.Equal("2.0.0", range.MaxVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Concelier.BackportProof\StellaOps.Concelier.BackportProof.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
@@ -387,6 +387,7 @@ public sealed class CanonicalMergerTests
|
||||
IEnumerable<AffectedPackage>? packages = null,
|
||||
IEnumerable<CvssMetric>? metrics = null,
|
||||
IEnumerable<AdvisoryReference>? references = null,
|
||||
IEnumerable<AdvisoryCredit>? credits = null,
|
||||
IEnumerable<AdvisoryWeakness>? weaknesses = null,
|
||||
string? canonicalMetricId = null)
|
||||
{
|
||||
@@ -407,7 +408,7 @@ public sealed class CanonicalMergerTests
|
||||
severity: severity,
|
||||
exploitKnown: false,
|
||||
aliases: new[] { advisoryKey },
|
||||
credits: Array.Empty<AdvisoryCredit>(),
|
||||
credits: credits ?? Array.Empty<AdvisoryCredit>(),
|
||||
references: references ?? Array.Empty<AdvisoryReference>(),
|
||||
affectedPackages: packages ?? Array.Empty<AffectedPackage>(),
|
||||
cvssMetrics: metrics ?? Array.Empty<CvssMetric>(),
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SourceRegistryTests.cs
|
||||
// Sprint: SPRINT_20260114_SOURCES_SETUP
|
||||
// Task: Unit tests for Source Registry
|
||||
// Description: Unit tests for the SourceRegistry implementation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using StellaOps.Concelier.Core.Configuration;
|
||||
using StellaOps.Concelier.Core.Sources;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Core.Tests.Sources;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SourceRegistryTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedNow = new(2026, 1, 14, 10, 0, 0, TimeSpan.Zero);
|
||||
private readonly FakeTimeProvider _timeProvider = new(FixedNow);
|
||||
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock = new();
|
||||
|
||||
private SourceRegistry CreateRegistry(SourcesConfiguration? configuration = null)
|
||||
{
|
||||
return new SourceRegistry(
|
||||
_httpClientFactoryMock.Object,
|
||||
NullLogger<SourceRegistry>.Instance,
|
||||
_timeProvider,
|
||||
configuration);
|
||||
}
|
||||
|
||||
#region GetAllSources Tests
|
||||
|
||||
[Fact]
|
||||
public void GetAllSources_ReturnsAllDefinedSources()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var sources = registry.GetAllSources();
|
||||
|
||||
Assert.NotEmpty(sources);
|
||||
Assert.True(sources.Count >= 30, "Expected at least 30 sources defined");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllSources_ContainsExpectedSources()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var sources = registry.GetAllSources();
|
||||
|
||||
var sourceIds = sources.Select(s => s.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("nvd", sourceIds);
|
||||
Assert.Contains("ghsa", sourceIds);
|
||||
Assert.Contains("osv", sourceIds);
|
||||
Assert.Contains("epss", sourceIds);
|
||||
Assert.Contains("kev", sourceIds);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSource Tests
|
||||
|
||||
[Fact]
|
||||
public void GetSource_ReturnsSource_ForValidId()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var source = registry.GetSource("nvd");
|
||||
|
||||
Assert.NotNull(source);
|
||||
Assert.Equal("nvd", source.Id);
|
||||
Assert.Contains("NVD", source.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSource_IsCaseInsensitive()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var source1 = registry.GetSource("NVD");
|
||||
var source2 = registry.GetSource("nvd");
|
||||
var source3 = registry.GetSource("Nvd");
|
||||
|
||||
Assert.NotNull(source1);
|
||||
Assert.NotNull(source2);
|
||||
Assert.NotNull(source3);
|
||||
Assert.Equal(source1.Id, source2.Id);
|
||||
Assert.Equal(source2.Id, source3.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSource_ReturnsNull_ForUnknownSource()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var source = registry.GetSource("unknown-source-xyz");
|
||||
|
||||
Assert.Null(source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSource_ThrowsArgumentException_ForNullOrEmpty()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
// ArgumentNullException for null, ArgumentException for empty/whitespace
|
||||
Assert.ThrowsAny<ArgumentException>(() => registry.GetSource(null!));
|
||||
Assert.ThrowsAny<ArgumentException>(() => registry.GetSource(""));
|
||||
Assert.ThrowsAny<ArgumentException>(() => registry.GetSource(" "));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSourcesByCategory Tests
|
||||
|
||||
[Fact]
|
||||
public void GetSourcesByCategory_ReturnsSourcesInCategory()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var primarySources = registry.GetSourcesByCategory(SourceCategory.Primary);
|
||||
|
||||
Assert.NotEmpty(primarySources);
|
||||
Assert.All(primarySources, s => Assert.Equal(SourceCategory.Primary, s.Category));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSourcesByCategory_ReturnsEmptyList_ForEmptyCategory()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var sources = registry.GetSourcesByCategory(SourceCategory.Other);
|
||||
|
||||
// Other category may be empty or contain sources
|
||||
Assert.NotNull(sources);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region EnableSourceAsync/DisableSourceAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSourceAsync_EnablesSource_ReturnsTrue()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
await registry.DisableSourceAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await registry.EnableSourceAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.True(registry.IsEnabled("nvd"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSourceAsync_ReturnsFalse_ForUnknownSource()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.EnableSourceAsync("unknown-source-xyz", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableSourceAsync_DisablesSource_ReturnsTrue()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
await registry.EnableSourceAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await registry.DisableSourceAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.False(registry.IsEnabled("nvd"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableSourceAsync_ReturnsFalse_ForUnknownSource()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.DisableSourceAsync("unknown-source-xyz", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetEnabledSourcesAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task GetEnabledSourcesAsync_ReturnsEnabledSources()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
await registry.EnableSourceAsync("nvd", TestContext.Current.CancellationToken);
|
||||
await registry.EnableSourceAsync("ghsa", TestContext.Current.CancellationToken);
|
||||
await registry.DisableSourceAsync("osv", TestContext.Current.CancellationToken);
|
||||
|
||||
var enabled = await registry.GetEnabledSourcesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Contains("nvd", enabled, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Contains("ghsa", enabled, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsEnabled Tests
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_ReturnsTrue_ForEnabledSource()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
// By default, most sources should be enabled
|
||||
var isEnabled = registry.IsEnabled("nvd");
|
||||
|
||||
// NVD is enabled by default
|
||||
Assert.True(isEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEnabled_ReturnsFalse_ForUnknownSource()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var isEnabled = registry.IsEnabled("unknown-source-xyz");
|
||||
|
||||
Assert.False(isEnabled);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CheckConnectivityAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task CheckConnectivityAsync_ReturnsNotFound_ForUnknownSource()
|
||||
{
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckConnectivityAsync("unknown-source-xyz", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(SourceConnectivityStatus.Failed, result.Status);
|
||||
Assert.Equal("SOURCE_NOT_FOUND", result.ErrorCode);
|
||||
Assert.False(result.IsHealthy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckConnectivityAsync_ReturnsHealthy_ForSuccessfulResponse()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var httpClient = new HttpClient(handlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckConnectivityAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(SourceConnectivityStatus.Healthy, result.Status);
|
||||
Assert.True(result.IsHealthy);
|
||||
Assert.NotNull(result.Latency);
|
||||
Assert.Equal(FixedNow, result.CheckedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckConnectivityAsync_ReturnsFailed_ForHttpError()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.Unauthorized));
|
||||
|
||||
var httpClient = new HttpClient(handlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckConnectivityAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(SourceConnectivityStatus.Failed, result.Status);
|
||||
Assert.False(result.IsHealthy);
|
||||
Assert.Equal(401, result.HttpStatusCode);
|
||||
Assert.NotEmpty(result.PossibleReasons);
|
||||
Assert.NotEmpty(result.RemediationSteps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckConnectivityAsync_ReturnsFailed_ForNetworkError()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ThrowsAsync(new HttpRequestException("Connection refused"));
|
||||
|
||||
var httpClient = new HttpClient(handlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckConnectivityAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(SourceConnectivityStatus.Failed, result.Status);
|
||||
Assert.False(result.IsHealthy);
|
||||
Assert.NotNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckConnectivityAsync_UsesTimeProvider()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var httpClient = new HttpClient(handlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckConnectivityAsync("nvd", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(FixedNow, result.CheckedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckConnectivityAsync_StoresLastResult()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
var httpClient = new HttpClient(handlerMock.Object);
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
await registry.CheckConnectivityAsync("nvd", TestContext.Current.CancellationToken);
|
||||
var lastResult = registry.GetLastCheckResult("nvd");
|
||||
|
||||
Assert.NotNull(lastResult);
|
||||
Assert.Equal("nvd", lastResult.SourceId);
|
||||
Assert.Equal(SourceConnectivityStatus.Healthy, lastResult.Status);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CheckAllAndAutoConfigureAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAllAndAutoConfigureAsync_ChecksAllSources()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
// Return a new HttpClient for each CreateClient call to avoid reuse issues
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
|
||||
.Returns(() => new HttpClient(handlerMock.Object));
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckAllAndAutoConfigureAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotEmpty(result.Results);
|
||||
Assert.Equal(registry.GetAllSources().Count, result.TotalChecked);
|
||||
Assert.True(result.AllHealthy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAllAndAutoConfigureAsync_ReturnsAggregatedResult()
|
||||
{
|
||||
var callCount = 0;
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(() =>
|
||||
{
|
||||
Interlocked.Increment(ref callCount);
|
||||
// Make some requests fail
|
||||
return callCount % 5 == 0
|
||||
? new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
||||
: new HttpResponseMessage(HttpStatusCode.OK);
|
||||
});
|
||||
|
||||
// Return a new HttpClient for each CreateClient call to avoid reuse issues
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
|
||||
.Returns(() => new HttpClient(handlerMock.Object));
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckAllAndAutoConfigureAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotEmpty(result.Results);
|
||||
Assert.True(result.HealthyCount > 0);
|
||||
Assert.True(result.FailedCount > 0);
|
||||
Assert.False(result.AllHealthy);
|
||||
Assert.True(result.HasFailures);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAllAndAutoConfigureAsync_AutoEnablesHealthySources()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
// Return a new HttpClient for each CreateClient call to avoid reuse issues
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
|
||||
.Returns(() => new HttpClient(handlerMock.Object));
|
||||
|
||||
var configuration = new SourcesConfiguration { AutoEnableHealthySources = true };
|
||||
var registry = CreateRegistry(configuration);
|
||||
|
||||
// Disable all sources first
|
||||
foreach (var source in registry.GetAllSources())
|
||||
{
|
||||
await registry.DisableSourceAsync(source.Id, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
var result = await registry.CheckAllAndAutoConfigureAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
// All healthy sources should now be enabled
|
||||
var enabled = await registry.GetEnabledSourcesAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(result.HealthyCount, enabled.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAllAndAutoConfigureAsync_RecordsDuration()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
// Return a new HttpClient for each CreateClient call to avoid reuse issues
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
|
||||
.Returns(() => new HttpClient(handlerMock.Object));
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
var result = await registry.CheckAllAndAutoConfigureAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.TotalDuration >= TimeSpan.Zero);
|
||||
Assert.Equal(FixedNow, result.CheckedAt);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CheckMultipleAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task CheckMultipleAsync_ChecksSpecifiedSources()
|
||||
{
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
|
||||
|
||||
// Return a new HttpClient for each CreateClient call to avoid reuse issues
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
|
||||
.Returns(() => new HttpClient(handlerMock.Object));
|
||||
|
||||
var registry = CreateRegistry();
|
||||
var sourceIds = new[] { "nvd", "ghsa", "osv" };
|
||||
|
||||
var results = await registry.CheckMultipleAsync(sourceIds, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(3, results.Length);
|
||||
Assert.All(results, r => Assert.True(r.IsHealthy));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RetryCheckAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task RetryCheckAsync_RetriesConnectivityCheck()
|
||||
{
|
||||
var callCount = 0;
|
||||
var handlerMock = new Mock<HttpMessageHandler>();
|
||||
handlerMock.Protected()
|
||||
.Setup<Task<HttpResponseMessage>>(
|
||||
"SendAsync",
|
||||
ItExpr.IsAny<HttpRequestMessage>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(() =>
|
||||
{
|
||||
Interlocked.Increment(ref callCount);
|
||||
// Fail first time, succeed second time
|
||||
return callCount == 1
|
||||
? new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
|
||||
: new HttpResponseMessage(HttpStatusCode.OK);
|
||||
});
|
||||
|
||||
// Return a new HttpClient for each CreateClient call to avoid reuse issues
|
||||
_httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny<string>()))
|
||||
.Returns(() => new HttpClient(handlerMock.Object));
|
||||
|
||||
var registry = CreateRegistry();
|
||||
|
||||
// First check fails
|
||||
var firstResult = await registry.CheckConnectivityAsync("nvd", TestContext.Current.CancellationToken);
|
||||
Assert.False(firstResult.IsHealthy);
|
||||
|
||||
// Retry succeeds
|
||||
var retryResult = await registry.RetryCheckAsync("nvd", TestContext.Current.CancellationToken);
|
||||
Assert.True(retryResult.IsHealthy);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using StellaOps.Attestor.ProofChain.Models;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.ProofService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ProofEvidence and related models used by BackportProofService.
|
||||
/// </summary>
|
||||
public sealed class ProofEvidenceModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProofEvidence_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var dataJson = JsonSerializer.SerializeToElement(new { cve = "CVE-2026-0001", severity = "HIGH" });
|
||||
|
||||
var evidence = new ProofEvidence
|
||||
{
|
||||
EvidenceId = "evidence:distro:debian:DSA-1234",
|
||||
Type = EvidenceType.DistroAdvisory,
|
||||
Source = "debian",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:abc123def456"
|
||||
};
|
||||
|
||||
Assert.Equal("evidence:distro:debian:DSA-1234", evidence.EvidenceId);
|
||||
Assert.Equal(EvidenceType.DistroAdvisory, evidence.Type);
|
||||
Assert.Equal("debian", evidence.Source);
|
||||
Assert.Equal("sha256:abc123def456", evidence.DataHash);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(EvidenceType.DistroAdvisory)]
|
||||
[InlineData(EvidenceType.ChangelogMention)]
|
||||
[InlineData(EvidenceType.PatchHeader)]
|
||||
[InlineData(EvidenceType.BinaryFingerprint)]
|
||||
[InlineData(EvidenceType.VersionComparison)]
|
||||
[InlineData(EvidenceType.BuildCatalog)]
|
||||
public void ProofEvidence_Type_AllValues_AreValid(EvidenceType type)
|
||||
{
|
||||
var dataJson = JsonSerializer.SerializeToElement(new { test = true });
|
||||
|
||||
var evidence = new ProofEvidence
|
||||
{
|
||||
EvidenceId = $"evidence:{type.ToString().ToLowerInvariant()}:test",
|
||||
Type = type,
|
||||
Source = "test-source",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:test"
|
||||
};
|
||||
|
||||
Assert.Equal(type, evidence.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofEvidence_DataJson_ContainsStructuredData()
|
||||
{
|
||||
var advisoryData = new
|
||||
{
|
||||
distro = "ubuntu",
|
||||
advisory_id = "USN-1234-1",
|
||||
packages = new[] { "libcurl4", "curl" },
|
||||
fixed_version = "7.68.0-1ubuntu2.15"
|
||||
};
|
||||
var dataJson = JsonSerializer.SerializeToElement(advisoryData);
|
||||
|
||||
var evidence = new ProofEvidence
|
||||
{
|
||||
EvidenceId = "evidence:distro:ubuntu:USN-1234-1",
|
||||
Type = EvidenceType.DistroAdvisory,
|
||||
Source = "ubuntu",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:structured123"
|
||||
};
|
||||
|
||||
Assert.Equal(JsonValueKind.Object, evidence.Data.ValueKind);
|
||||
Assert.Equal("ubuntu", evidence.Data.GetProperty("distro").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofEvidence_RecordEquality_WorksCorrectly()
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow;
|
||||
var dataJson = JsonSerializer.SerializeToElement(new { key = "value" });
|
||||
|
||||
var evidence1 = new ProofEvidence
|
||||
{
|
||||
EvidenceId = "evidence:test:eq",
|
||||
Type = EvidenceType.ChangelogMention,
|
||||
Source = "changelog",
|
||||
Timestamp = timestamp,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:equal"
|
||||
};
|
||||
|
||||
var evidence2 = new ProofEvidence
|
||||
{
|
||||
EvidenceId = "evidence:test:eq",
|
||||
Type = EvidenceType.ChangelogMention,
|
||||
Source = "changelog",
|
||||
Timestamp = timestamp,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:equal"
|
||||
};
|
||||
|
||||
Assert.Equal(evidence1, evidence2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for ProofBlob model.
|
||||
/// </summary>
|
||||
public sealed class ProofBlobModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProofBlob_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var evidences = new List<ProofEvidence>
|
||||
{
|
||||
new()
|
||||
{
|
||||
EvidenceId = "evidence:test:001",
|
||||
Type = EvidenceType.DistroAdvisory,
|
||||
Source = "debian",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = JsonSerializer.SerializeToElement(new { }),
|
||||
DataHash = "sha256:ev1"
|
||||
}
|
||||
};
|
||||
|
||||
var proof = new ProofBlob
|
||||
{
|
||||
ProofId = "sha256:proof123",
|
||||
SubjectId = "CVE-2026-0001:pkg:deb/debian/curl@7.64.0",
|
||||
Type = ProofBlobType.BackportFixed,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = evidences,
|
||||
Method = "distro_advisory",
|
||||
Confidence = 0.95,
|
||||
ToolVersion = "1.0.0",
|
||||
SnapshotId = "snapshot:2026-01-14"
|
||||
};
|
||||
|
||||
Assert.Equal("sha256:proof123", proof.ProofId);
|
||||
Assert.Equal("CVE-2026-0001:pkg:deb/debian/curl@7.64.0", proof.SubjectId);
|
||||
Assert.Equal(ProofBlobType.BackportFixed, proof.Type);
|
||||
Assert.Equal(0.95, proof.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofBlob_WithMultipleEvidences_ContainsAll()
|
||||
{
|
||||
var dataJson = JsonSerializer.SerializeToElement(new { });
|
||||
var evidences = new List<ProofEvidence>
|
||||
{
|
||||
new()
|
||||
{
|
||||
EvidenceId = "evidence:distro:dsa",
|
||||
Type = EvidenceType.DistroAdvisory,
|
||||
Source = "debian",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:dsa"
|
||||
},
|
||||
new()
|
||||
{
|
||||
EvidenceId = "evidence:changelog:debian",
|
||||
Type = EvidenceType.ChangelogMention,
|
||||
Source = "debian-changelog",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:changelog"
|
||||
},
|
||||
new()
|
||||
{
|
||||
EvidenceId = "evidence:patch:fix",
|
||||
Type = EvidenceType.PatchHeader,
|
||||
Source = "git-patch",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Data = dataJson,
|
||||
DataHash = "sha256:patch"
|
||||
}
|
||||
};
|
||||
|
||||
var proof = new ProofBlob
|
||||
{
|
||||
ProofId = "sha256:multiproof",
|
||||
SubjectId = "CVE-2026-0002:pkg:npm/lodash@4.17.20",
|
||||
Type = ProofBlobType.BackportFixed,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = evidences,
|
||||
Method = "combined",
|
||||
Confidence = 0.92,
|
||||
ToolVersion = "1.0.0",
|
||||
SnapshotId = "snapshot:2026-01-14"
|
||||
};
|
||||
|
||||
Assert.Equal(3, proof.Evidences.Count);
|
||||
Assert.Contains(proof.Evidences, e => e.Type == EvidenceType.DistroAdvisory);
|
||||
Assert.Contains(proof.Evidences, e => e.Type == EvidenceType.ChangelogMention);
|
||||
Assert.Contains(proof.Evidences, e => e.Type == EvidenceType.PatchHeader);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ProofBlobType.BackportFixed)]
|
||||
[InlineData(ProofBlobType.NotAffected)]
|
||||
[InlineData(ProofBlobType.Vulnerable)]
|
||||
[InlineData(ProofBlobType.Unknown)]
|
||||
public void ProofBlob_Type_AllValues_AreValid(ProofBlobType type)
|
||||
{
|
||||
var proof = new ProofBlob
|
||||
{
|
||||
ProofId = $"sha256:{type.ToString().ToLowerInvariant()}",
|
||||
SubjectId = "CVE-2026-TYPE:pkg:test/pkg@1.0.0",
|
||||
Type = type,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = Array.Empty<ProofEvidence>(),
|
||||
Method = "test",
|
||||
Confidence = 0.5,
|
||||
ToolVersion = "1.0.0",
|
||||
SnapshotId = "snapshot:test"
|
||||
};
|
||||
|
||||
Assert.Equal(type, proof.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofBlob_Confidence_InValidRange()
|
||||
{
|
||||
var proof = new ProofBlob
|
||||
{
|
||||
ProofId = "sha256:conf",
|
||||
SubjectId = "CVE-2026-CONF:pkg:test/pkg@1.0.0",
|
||||
Type = ProofBlobType.BackportFixed,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = Array.Empty<ProofEvidence>(),
|
||||
Method = "test",
|
||||
Confidence = 0.87,
|
||||
ToolVersion = "1.0.0",
|
||||
SnapshotId = "snapshot:test"
|
||||
};
|
||||
|
||||
Assert.InRange(proof.Confidence, 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofBlob_ProofHash_IsOptional()
|
||||
{
|
||||
var proofWithoutHash = new ProofBlob
|
||||
{
|
||||
ProofId = "sha256:nohash",
|
||||
SubjectId = "CVE-2026-NH:pkg:test/pkg@1.0.0",
|
||||
Type = ProofBlobType.Unknown,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = Array.Empty<ProofEvidence>(),
|
||||
Method = "test",
|
||||
Confidence = 0.0,
|
||||
ToolVersion = "1.0.0",
|
||||
SnapshotId = "snapshot:test"
|
||||
};
|
||||
|
||||
Assert.Null(proofWithoutHash.ProofHash);
|
||||
|
||||
var proofWithHash = proofWithoutHash with { ProofHash = "sha256:computed" };
|
||||
Assert.Equal("sha256:computed", proofWithHash.ProofHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProofBlob_SubjectId_ContainsCveAndPurl()
|
||||
{
|
||||
var proof = new ProofBlob
|
||||
{
|
||||
ProofId = "sha256:subject",
|
||||
SubjectId = "CVE-2026-12345:pkg:pypi/django@4.2.0",
|
||||
Type = ProofBlobType.NotAffected,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Evidences = Array.Empty<ProofEvidence>(),
|
||||
Method = "vex",
|
||||
Confidence = 1.0,
|
||||
ToolVersion = "1.0.0",
|
||||
SnapshotId = "snapshot:test"
|
||||
};
|
||||
|
||||
Assert.Contains("CVE-2026-12345", proof.SubjectId);
|
||||
Assert.Contains("pkg:pypi/django@4.2.0", proof.SubjectId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Concelier.ProofService\StellaOps.Concelier.ProofService.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
Reference in New Issue
Block a user