Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -8,11 +8,13 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Cache.Valkey.Tests;
|
||||
|
||||
public class AdvisoryCacheKeysTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Advisory_WithDefaultPrefix_GeneratesCorrectKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -25,7 +27,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:advisory:abc123def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Advisory_WithCustomPrefix_GeneratesCorrectKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -39,7 +42,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("custom:advisory:abc123def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void HotSet_WithDefaultPrefix_GeneratesCorrectKey()
|
||||
{
|
||||
// Act
|
||||
@@ -49,7 +53,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:rank:hot");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ByPurl_NormalizesPurl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -62,7 +67,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:by:purl:pkg:npm/@angular/core@12.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ByPurl_NormalizesToLowercase()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +81,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:by:purl:pkg:npm/@angular/core@12.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ByCve_NormalizesToUppercase()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +95,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:by:cve:CVE-2024-1234");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void StatsHits_GeneratesCorrectKey()
|
||||
{
|
||||
// Act
|
||||
@@ -98,7 +106,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:cache:stats:hits");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void StatsMisses_GeneratesCorrectKey()
|
||||
{
|
||||
// Act
|
||||
@@ -108,7 +117,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:cache:stats:misses");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void WarmupLast_GeneratesCorrectKey()
|
||||
{
|
||||
// Act
|
||||
@@ -118,7 +128,8 @@ public class AdvisoryCacheKeysTests
|
||||
key.Should().Be("concelier:cache:warmup:last");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizePurl_HandlesEmptyString()
|
||||
{
|
||||
// Act
|
||||
@@ -128,7 +139,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizePurl_HandlesNull()
|
||||
{
|
||||
// Act
|
||||
@@ -138,7 +150,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizePurl_ReplacesSpecialCharacters()
|
||||
{
|
||||
// Arrange - PURL with unusual characters
|
||||
@@ -152,7 +165,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().Be("pkg:npm/test_query_value_fragment");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizePurl_TruncatesLongPurls()
|
||||
{
|
||||
// Arrange - Very long PURL
|
||||
@@ -165,7 +179,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Length.Should().BeLessThanOrEqualTo(500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractMergeHash_ReturnsHashFromAdvisoryKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +193,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().Be("abc123def456");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractMergeHash_ReturnsNullForInvalidKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -191,7 +207,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractPurl_ReturnsPurlFromIndexKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -204,7 +221,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().Be("pkg:npm/test@1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExtractCve_ReturnsCveFromMappingKey()
|
||||
{
|
||||
// Arrange
|
||||
@@ -217,7 +235,8 @@ public class AdvisoryCacheKeysTests
|
||||
result.Should().Be("CVE-2024-1234");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AdvisoryPattern_GeneratesCorrectPattern()
|
||||
{
|
||||
// Act
|
||||
@@ -227,7 +246,8 @@ public class AdvisoryCacheKeysTests
|
||||
pattern.Should().Be("concelier:advisory:*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void PurlIndexPattern_GeneratesCorrectPattern()
|
||||
{
|
||||
// Act
|
||||
@@ -237,7 +257,8 @@ public class AdvisoryCacheKeysTests
|
||||
pattern.Should().Be("concelier:by:purl:*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CveMappingPattern_GeneratesCorrectPattern()
|
||||
{
|
||||
// Act
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Cache.Valkey.Tests;
|
||||
|
||||
public class CacheTtlPolicyTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithHighScore_ReturnsHighScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -25,7 +27,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithScoreAtHighThreshold_ReturnsHighScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -38,7 +41,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithMediumScore_ReturnsMediumScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -51,7 +55,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithScoreAtMediumThreshold_ReturnsMediumScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -64,7 +69,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithLowScore_ReturnsLowScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -77,7 +83,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithZeroScore_ReturnsLowScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +97,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithNullScore_ReturnsLowScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -103,7 +111,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithCustomThresholds_UsesCustomValues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +131,8 @@ public class CacheTtlPolicyTests
|
||||
policy.GetTtl(0.3).Should().Be(TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DefaultValues_AreCorrect()
|
||||
{
|
||||
// Arrange
|
||||
@@ -138,7 +148,8 @@ public class CacheTtlPolicyTests
|
||||
policy.CveMappingTtl.Should().Be(TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithScoreBelowMediumThreshold_ReturnsLowScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -151,7 +162,8 @@ public class CacheTtlPolicyTests
|
||||
ttl.Should().Be(TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetTtl_WithScoreBelowHighThreshold_ReturnsMediumScoreTtl()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -31,7 +31,8 @@ public sealed class CccsConnectorTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
@@ -65,10 +66,12 @@ public sealed class CccsConnectorTests
|
||||
pendingMappings!.AsDocumentArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Fetch_PersistsRawDocumentWithMetadata()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
using StellaOps.TestKit;
|
||||
SeedFeedResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CccsConnector>();
|
||||
|
||||
@@ -32,7 +32,8 @@ public sealed class CertBundConnectorTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
@@ -75,10 +76,12 @@ public sealed class CertBundConnectorTests
|
||||
pendingMappings!.AsDocumentArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Fetch_PersistsDocumentWithMetadata()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
using StellaOps.TestKit;
|
||||
SeedResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<CertBundConnector>();
|
||||
|
||||
@@ -25,11 +25,13 @@ public sealed class AlpineConnectorTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_StoresAdvisoriesAndUpdatesCursor()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
harness.Handler.AddJsonResponse(SecDbUri, BuildMinimalSecDb());
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AlpineConnector>();
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineDependencyInjectionRoutineTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_ConfiguresOptionsAndScheduler()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -41,6 +42,7 @@ public sealed class AlpineDependencyInjectionRoutineTests
|
||||
|
||||
using var provider = services.BuildServiceProvider(validateScopes: true);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var options = provider.GetRequiredService<IOptions<AlpineOptions>>().Value;
|
||||
Assert.Equal(new Uri("https://secdb.alpinelinux.org/"), options.BaseUri);
|
||||
Assert.Equal(new[] { "v3.20" }, options.Releases);
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_BuildsApkAdvisoriesWithRanges()
|
||||
{
|
||||
var dto = new AlpineSecDbDto(
|
||||
|
||||
@@ -2,11 +2,13 @@ using System.Linq;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Dto;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineSecDbParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_SecDbFixture_ExtractsPackagesAndMetadata()
|
||||
{
|
||||
var dto = AlpineFixtureReader.LoadDto("v3.20-main.json");
|
||||
|
||||
@@ -10,11 +10,13 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineSnapshotTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("v3.18-main.json", "alpine-v3.18-main.snapshot.json", "2025-12-22T00:00:00Z")]
|
||||
[InlineData("v3.19-main.json", "alpine-v3.19-main.snapshot.json", "2025-12-22T00:10:00Z")]
|
||||
[InlineData("v3.20-main.json", "alpine-v3.20-main.snapshot.json", "2025-12-22T00:20:00Z")]
|
||||
|
||||
@@ -65,11 +65,13 @@ public sealed class DebianConnectorTests : IAsyncLifetime
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_PopulatesRangePrimitivesAndResumesWithNotModified()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
SeedInitialResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<DebianConnector>();
|
||||
|
||||
@@ -5,11 +5,13 @@ using StellaOps.Concelier.Connector.Distro.Debian;
|
||||
using StellaOps.Concelier.Connector.Distro.Debian.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
|
||||
|
||||
public sealed class DebianMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_BuildsRangePrimitives_ForResolvedPackage()
|
||||
{
|
||||
var dto = new DebianAdvisoryDto(
|
||||
|
||||
@@ -35,11 +35,13 @@ public sealed class SuseConnectorTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProcessesResolvedAndOpenNotices()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
SeedInitialResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<SuseConnector>();
|
||||
|
||||
@@ -5,11 +5,13 @@ using System.Text.Json;
|
||||
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
public sealed class SuseCsafParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_ProducesRecommendedAndAffectedPackages()
|
||||
{
|
||||
var json = ReadFixture("Source/Distro/Suse/Fixtures/suse-su-2025_0001-1.json");
|
||||
@@ -25,7 +27,8 @@ public sealed class SuseCsafParserTests
|
||||
Assert.Equal("openssl-1.1.1w-150500.17.25.1.x86_64", package.CanonicalNevra);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_HandlesOpenInvestigation()
|
||||
{
|
||||
var json = ReadFixture("Source/Distro/Suse/Fixtures/suse-su-2025_0002-1.json");
|
||||
|
||||
@@ -9,11 +9,13 @@ using StellaOps.Concelier.Connector.Distro.Suse.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
|
||||
|
||||
public sealed class SuseMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_BuildsNevraRangePrimitives()
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "Suse", "Fixtures", "suse-su-2025_0001-1.json"));
|
||||
|
||||
@@ -34,11 +34,13 @@ public sealed class UbuntuConnectorTests
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_GeneratesEvrRangePrimitives()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
SeedInitialResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<UbuntuConnector>();
|
||||
|
||||
@@ -23,7 +23,8 @@ namespace StellaOps.Concelier.Connector.Epss.Tests;
|
||||
|
||||
public sealed class EpssConnectorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_StoresDocument_OnSuccess()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -61,7 +62,8 @@ public sealed class EpssConnectorTests
|
||||
Assert.Contains(record.Id, cursor.PendingDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_ReturnsNotModified_OnEtagMatch()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -112,7 +114,8 @@ public sealed class EpssConnectorTests
|
||||
Assert.Empty(cursor.PendingDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_CreatesDto_AndUpdatesStatus()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -163,7 +166,8 @@ public sealed class EpssConnectorTests
|
||||
Assert.Equal(DocumentStatuses.PendingMap, updated!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MapAsync_MarksDocumentMapped()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
@@ -228,7 +232,8 @@ public sealed class EpssConnectorTests
|
||||
Assert.Equal(DocumentStatuses.Mapped, updated!.Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(0.75, EpssBand.Critical)]
|
||||
[InlineData(0.55, EpssBand.High)]
|
||||
[InlineData(0.25, EpssBand.Medium)]
|
||||
@@ -242,7 +247,8 @@ public sealed class EpssConnectorTests
|
||||
Assert.Equal(expected, observation.Band);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EpssCursor_Empty_UsesMinValue()
|
||||
{
|
||||
var cursor = EpssCursor.Empty;
|
||||
@@ -331,6 +337,7 @@ public sealed class EpssConnectorTests
|
||||
foreach (var line in lines)
|
||||
{
|
||||
writer.WriteLine(line);
|
||||
using StellaOps.TestKit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ public sealed class IcsCisaConnectorTests
|
||||
_fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_EndToEnd_ProducesCanonicalAdvisories()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
using StellaOps.TestKit;
|
||||
RegisterResponses(harness.Handler);
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<IcsCisaConnector>();
|
||||
|
||||
@@ -49,7 +49,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
_output = output;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -104,7 +105,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
pendingMappings!.AsDocumentArray.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ExclusiveUpperBound_ProducesExclusiveNormalizedRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -136,7 +138,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be(">= 3.2.0 < 4.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ExclusiveLowerBound_ProducesExclusiveNormalizedRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -169,7 +172,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be("> 1.2.0 <= 2.4.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_SingleBound_ProducesMinimumOnlyConstraint()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -208,7 +212,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be(">= 5.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_UpperBoundOnlyExclusive_ProducesLessThanRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -241,7 +246,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be("< 3.5.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_UpperBoundOnlyInclusive_ProducesLessThanOrEqualRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -273,7 +279,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be("<= 4.2.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_LowerBoundOnlyExclusive_ProducesGreaterThanRule()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -306,7 +313,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be("> 1.9.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_InvalidSegment_ProducesFallbackRange()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -332,7 +340,8 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
.WhoseValue.Should().Be("지원 버전: 최신 업데이트 적용");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Telemetry_RecordsMetrics()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -340,6 +349,7 @@ public sealed class KisaConnectorTests : IAsyncLifetime
|
||||
|
||||
using var metrics = new KisaMetricCollector();
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var connector = provider.GetRequiredService<KisaConnector>();
|
||||
await connector.FetchAsync(provider, CancellationToken.None);
|
||||
await connector.ParseAsync(provider, CancellationToken.None);
|
||||
|
||||
@@ -8,6 +8,7 @@ using StellaOps.Concelier.Connector.Common.Html;
|
||||
using StellaOps.Concelier.Connector.Kisa.Internal;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Kisa.Tests;
|
||||
|
||||
public sealed class KisaDetailParserTests
|
||||
@@ -15,7 +16,8 @@ public sealed class KisaDetailParserTests
|
||||
private static readonly Uri DetailApiUri = new("https://test.local/rssDetailData.do?IDX=5868");
|
||||
private static readonly Uri DetailPageUri = new("https://test.local/detailDos.do?IDX=5868");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseHtmlPayload_ProducesExpectedModels()
|
||||
{
|
||||
var parser = new KisaDetailParser(new HtmlContentSanitizer());
|
||||
|
||||
@@ -44,7 +44,8 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesDeterministicSnapshots()
|
||||
{
|
||||
var harness = await EnsureHarnessAsync();
|
||||
@@ -261,6 +262,7 @@ public sealed class RuBduConnectorSnapshotTests : IAsyncLifetime
|
||||
entry.LastWriteTime = new DateTimeOffset(2025, 10, 14, 9, 0, 0, TimeSpan.Zero);
|
||||
using var entryStream = entry.Open();
|
||||
using var writer = new StreamWriter(entryStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
using StellaOps.TestKit;
|
||||
writer.Write(xml);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
|
||||
|
||||
public sealed class RuBduMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_ConstructsCanonicalAdvisory()
|
||||
{
|
||||
var dto = new RuBduVulnerabilityDto(
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Xml.Linq;
|
||||
using StellaOps.Concelier.Connector.Ru.Bdu.Internal;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Ru.Bdu.Tests;
|
||||
|
||||
public sealed class RuBduXmlParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_ValidElement_ReturnsDto()
|
||||
{
|
||||
const string xml = """
|
||||
@@ -75,7 +77,8 @@ public sealed class RuBduXmlParserTests
|
||||
Assert.Contains(dto.Identifiers, identifier => identifier.Type == "GHSA" && identifier.Link == "https://github.com/advisories/GHSA-xxxx-yyyy-zzzz");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_SampleArchiveEntries_ReturnDtos()
|
||||
{
|
||||
var path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Fixtures", "export-sample.xml"));
|
||||
|
||||
@@ -49,7 +49,8 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesExpectedSnapshot()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
@@ -80,10 +81,12 @@ public sealed class RuNkckiConnectorTests : IAsyncLifetime
|
||||
Assert.True(IsEmptyArray(state.Cursor, "pendingMappings"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Fetch_ReusesCachedBulletinWhenListingFails()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
using StellaOps.TestKit;
|
||||
SeedListingAndBulletin();
|
||||
|
||||
var connector = provider.GetRequiredService<RuNkckiConnector>();
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
|
||||
|
||||
public sealed class RuNkckiJsonParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Parse_WellFormedEntry_ReturnsDto()
|
||||
{
|
||||
const string json = """
|
||||
@@ -40,6 +41,7 @@ public sealed class RuNkckiJsonParserTests
|
||||
""";
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
using StellaOps.TestKit;
|
||||
var dto = RuNkckiJsonParser.Parse(document.RootElement);
|
||||
|
||||
Assert.Equal("BDU:2025-00001", dto.FstecId);
|
||||
|
||||
@@ -7,11 +7,13 @@ using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
using System.Reflection;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Ru.Nkcki.Tests;
|
||||
|
||||
public sealed class RuNkckiMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_ConstructsCanonicalAdvisory()
|
||||
{
|
||||
var softwareEntries = ImmutableArray.Create(
|
||||
|
||||
@@ -3,11 +3,13 @@ using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
|
||||
|
||||
public sealed class MirrorAdvisoryMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_ProducesCanonicalAdvisoryWithMirrorProvenance()
|
||||
{
|
||||
var bundle = SampleData.CreateBundle();
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
|
||||
|
||||
public sealed class MirrorSignatureVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidSignaturePasses()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
@@ -29,7 +30,8 @@ public sealed class MirrorSignatureVerifierTests
|
||||
await verifier.VerifyAsync(payload, signature, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_InvalidSignatureThrows()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
@@ -48,7 +50,8 @@ public sealed class MirrorSignatureVerifierTests
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(payload, tampered, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_KeyMismatchThrows()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
@@ -71,7 +74,8 @@ public sealed class MirrorSignatureVerifierTests
|
||||
cancellationToken: CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ThrowsWhenProviderMissingKey()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
@@ -96,7 +100,8 @@ public sealed class MirrorSignatureVerifierTests
|
||||
cancellationToken: CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyAsync_UsesCachedPublicKeyWhenFileRemoved()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
@@ -138,6 +143,7 @@ public sealed class MirrorSignatureVerifierTests
|
||||
private static string WritePublicKeyPem(CryptoSigningKey signingKey)
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(signingKey.PublicParameters);
|
||||
using StellaOps.TestKit;
|
||||
var info = ecdsa.ExportSubjectPublicKeyInfo();
|
||||
var pem = PemEncoding.Write("PUBLIC KEY", info);
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-mirror-{Guid.NewGuid():N}.pem");
|
||||
|
||||
@@ -43,7 +43,8 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_PersistsMirrorArtifacts()
|
||||
{
|
||||
var manifestContent = "{\"domain\":\"primary\",\"files\":[]}";
|
||||
@@ -105,7 +106,8 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
Assert.Empty(pendingMappingsArray);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_TamperedSignatureThrows()
|
||||
{
|
||||
var manifestContent = "{\"domain\":\"primary\"}";
|
||||
@@ -142,7 +144,8 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
Assert.False(state.Cursor.TryGetValue("bundleDigest", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_SignatureKeyMismatchThrows()
|
||||
{
|
||||
var manifestContent = "{\"domain\":\"primary\"}";
|
||||
@@ -175,7 +178,8 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_VerifiesSignatureUsingFallbackPublicKey()
|
||||
{
|
||||
var manifestContent = "{\"domain\":\"primary\"}";
|
||||
@@ -218,7 +222,8 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchAsync_DigestMismatchMarksFailure()
|
||||
{
|
||||
var manifestExpected = "{\"domain\":\"primary\"}";
|
||||
@@ -245,7 +250,8 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
Assert.False(cursor.Contains("bundleDigest"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseAndMap_PersistAdvisoriesFromBundle()
|
||||
{
|
||||
var bundleDocument = SampleData.CreateBundle();
|
||||
@@ -419,6 +425,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
ArgumentNullException.ThrowIfNull(signingKey);
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-mirror-{Guid.NewGuid():N}.pem");
|
||||
using var ecdsa = ECDsa.Create(signingKey.PublicParameters);
|
||||
using StellaOps.TestKit;
|
||||
var publicKeyInfo = ecdsa.ExportSubjectPublicKeyInfo();
|
||||
var pem = PemEncoding.Write("PUBLIC KEY", publicKeyInfo);
|
||||
File.WriteAllText(path, pem);
|
||||
|
||||
@@ -6,11 +6,13 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests;
|
||||
|
||||
public sealed class CiscoDtoFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAsync_MergesRawAndCsafProducts()
|
||||
{
|
||||
const string CsafPayload = @"
|
||||
|
||||
@@ -11,11 +11,13 @@ using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests;
|
||||
|
||||
public sealed class CiscoMapperTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Map_ProducesCanonicalAdvisory()
|
||||
{
|
||||
var published = new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
@@ -43,10 +43,12 @@ public sealed class MsrcConnectorTests : IAsyncLifetime
|
||||
_handler = new CannedHttpMessageHandler();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FetchParseMap_ProducesCanonicalAdvisory()
|
||||
{
|
||||
await using var provider = await BuildServiceProviderAsync();
|
||||
using StellaOps.TestKit;
|
||||
SeedResponses();
|
||||
|
||||
var connector = provider.GetRequiredService<MsrcConnector>();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Core.Tests;
|
||||
|
||||
public sealed class CanonicalMergerTests
|
||||
{
|
||||
private static readonly DateTimeOffset BaseTimestamp = new(2025, 10, 10, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_PrefersGhsaTitleAndSummaryByPrecedence()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(6)));
|
||||
@@ -42,7 +44,8 @@ public sealed class CanonicalMergerTests
|
||||
string.Equals(provenance.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_FreshnessOverrideUsesOsvSummaryWhenNewerByThreshold()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(10)));
|
||||
@@ -77,7 +80,8 @@ public sealed class CanonicalMergerTests
|
||||
string.Equals(provenance.DecisionReason, "freshness_override", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AffectedPackagesPreferOsvPrecedence()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(4)));
|
||||
@@ -163,7 +167,8 @@ public sealed class CanonicalMergerTests
|
||||
string.Equals(decision.DecisionReason, "precedence", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_CvssMetricsOrderedByPrecedence()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(5)));
|
||||
@@ -184,7 +189,8 @@ public sealed class CanonicalMergerTests
|
||||
Assert.Equal("3.1|CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", result.Advisory.CanonicalMetricId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_ReferencesNormalizedAndFreshnessOverrides()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(80)));
|
||||
@@ -234,7 +240,8 @@ public sealed class CanonicalMergerTests
|
||||
Assert.Contains("https://example.com/path/resource?a=1&b=2", itemDecision.Field, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_DescriptionFreshnessOverride()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(12)));
|
||||
@@ -272,7 +279,8 @@ public sealed class CanonicalMergerTests
|
||||
string.Equals(decision.DecisionReason, "freshness_override", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_CwesPreferNvdPrecedence()
|
||||
{
|
||||
var merger = new CanonicalMerger(new FixedTimeProvider(BaseTimestamp.AddHours(6)));
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace StellaOps.Concelier.Core.Tests;
|
||||
|
||||
public sealed class JobCoordinatorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerAsync_RunCompletesSuccessfully()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -56,7 +57,8 @@ public sealed class JobCoordinatorTests
|
||||
Assert.Equal("bar", completed.Parameters["foo"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerAsync_MarksRunFailed_WhenLeaseReleaseFails()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -106,7 +108,8 @@ public sealed class JobCoordinatorTests
|
||||
Assert.True(leaseStore.ReleaseAttempts > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerAsync_MarksRunFailed_WhenLeaseHeartbeatFails()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -156,7 +159,8 @@ public sealed class JobCoordinatorTests
|
||||
Assert.True(leaseStore.HeartbeatCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerAsync_ReturnsAlreadyRunning_WhenLeaseUnavailable()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -195,7 +199,8 @@ public sealed class JobCoordinatorTests
|
||||
Assert.False(jobStore.CreatedRuns.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerAsync_ReturnsInvalidParameters_ForUnsupportedPayload()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -237,7 +242,8 @@ public sealed class JobCoordinatorTests
|
||||
Assert.False(jobStore.CreatedRuns.Any());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task TriggerAsync_CancelsJobOnTimeout()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -262,6 +268,7 @@ public sealed class JobCoordinatorTests
|
||||
jobOptions.Definitions.Add(definition.Kind, definition);
|
||||
|
||||
using var diagnostics = new JobDiagnostics();
|
||||
using StellaOps.TestKit;
|
||||
var coordinator = new JobCoordinator(
|
||||
Options.Create(jobOptions),
|
||||
jobStore,
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Concelier.Core.Tests;
|
||||
|
||||
public sealed class JobPluginRegistrationExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RegisterJobPluginRoutines_LoadsPluginsAndRegistersDefinitions()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -48,6 +49,7 @@ public sealed class JobPluginRegistrationExtensionsTests
|
||||
descriptor => descriptor.ServiceType.FullName == typeof(PluginRoutineExecuted).FullName);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
|
||||
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(PluginJob.JobKind, out var definition));
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace StellaOps.Concelier.Core.Tests;
|
||||
|
||||
public sealed class JobSchedulerBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddJob_RegistersDefinitionWithExplicitMetadata()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -32,7 +33,8 @@ public sealed class JobSchedulerBuilderTests
|
||||
Assert.False(definition.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddJob_UsesDefaults_WhenOptionalMetadataExcluded()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -45,6 +47,7 @@ public sealed class JobSchedulerBuilderTests
|
||||
builder.AddJob<DefaultedJob>(kind: "jobs:defaults");
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var options = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
|
||||
|
||||
Assert.True(options.Definitions.TryGetValue("jobs:defaults", out var definition));
|
||||
|
||||
@@ -11,6 +11,7 @@ using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Exporter.Json.Tests;
|
||||
|
||||
public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
@@ -22,7 +23,8 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
_root = Directory.CreateTempSubdirectory("concelier-json-export-tests").FullName;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WritesDeterministicTree()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
@@ -62,7 +64,8 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
Assert.Equal(SnapshotSerializer.ToSnapshot(advisories[0]), actualJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProducesIdenticalBytesAcrossRuns()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
@@ -82,7 +85,8 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
Assert.Equal(Convert.ToHexString(firstDigest), Convert.ToHexString(secondDigest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_NormalizesInputOrdering()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
@@ -98,7 +102,8 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
Assert.Equal(expectedOrder, result.FilePaths.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_DifferentInputOrderProducesSameDigest()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
@@ -124,7 +129,8 @@ public sealed class JsonExportSnapshotBuilderTests : IDisposable
|
||||
Convert.ToHexString(ComputeDigest(second)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_EnumeratesStreamOnlyOnce()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace StellaOps.Concelier.Exporter.Json.Tests;
|
||||
|
||||
public sealed class JsonExporterDependencyInjectionRoutineTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Register_AddsJobDefinitionAndServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -41,6 +42,7 @@ public sealed class JsonExporterDependencyInjectionRoutineTests
|
||||
routine.Register(services, configuration);
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
using StellaOps.TestKit;
|
||||
var optionsAccessor = provider.GetRequiredService<IOptions<JobSchedulerOptions>>();
|
||||
var options = optionsAccessor.Value;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Exporter.Json.Tests;
|
||||
|
||||
public sealed class JsonExporterParitySmokeTests : IDisposable
|
||||
@@ -19,7 +20,8 @@ public sealed class JsonExporterParitySmokeTests : IDisposable
|
||||
_root = Directory.CreateTempSubdirectory("concelier-json-parity-tests").FullName;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportProducesVulnListCompatiblePaths()
|
||||
{
|
||||
var options = new JsonExportOptions { OutputRoot = _root };
|
||||
|
||||
@@ -33,7 +33,8 @@ public sealed class JsonFeedExporterTests : IDisposable
|
||||
_root = Directory.CreateTempSubdirectory("concelier-json-exporter-tests").FullName;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_SkipsWhenDigestUnchanged()
|
||||
{
|
||||
var advisory = new Advisory(
|
||||
@@ -94,7 +95,8 @@ public sealed class JsonFeedExporterTests : IDisposable
|
||||
Assert.False(Directory.Exists(secondExportPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_WritesManifestMetadata()
|
||||
{
|
||||
var exportedAt = DateTimeOffset.Parse("2024-08-10T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
@@ -246,7 +248,8 @@ public sealed class JsonFeedExporterTests : IDisposable
|
||||
Assert.Equal(ExporterVersion.GetVersion(typeof(JsonFeedExporter)), exporterVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_WritesMirrorBundlesWithSignatures()
|
||||
{
|
||||
var exportedAt = DateTimeOffset.Parse("2025-01-05T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
@@ -428,6 +431,7 @@ public sealed class JsonFeedExporterTests : IDisposable
|
||||
private static string WriteSigningKey(string directory)
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
using StellaOps.TestKit;
|
||||
var pkcs8 = ecdsa.ExportPkcs8PrivateKey();
|
||||
var pem = BuildPem("PRIVATE KEY", pkcs8);
|
||||
var path = Path.Combine(directory, $"mirror-key-{Guid.NewGuid():N}.pem");
|
||||
|
||||
@@ -4,13 +4,15 @@ using System.IO;
|
||||
using StellaOps.Concelier.Exporter.Json;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Exporter.Json.Tests;
|
||||
|
||||
public sealed class VulnListJsonExportPathResolverTests
|
||||
{
|
||||
private static readonly DateTimeOffset DefaultPublished = DateTimeOffset.Parse("2024-01-01T00:00:00Z", CultureInfo.InvariantCulture);
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesCvePath()
|
||||
{
|
||||
var advisory = CreateAdvisory("CVE-2024-1234");
|
||||
@@ -21,7 +23,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("nvd", "2024", "CVE-2024-1234.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesGhsaWithPackage()
|
||||
{
|
||||
var package = new AffectedPackage(
|
||||
@@ -43,7 +46,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("ghsa", "go", "github.com%2Facme%2Fwidget", "GHSA-AAAA-BBBB-CCCC.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesUbuntuUsn()
|
||||
{
|
||||
var advisory = CreateAdvisory("USN-6620-1");
|
||||
@@ -53,7 +57,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("ubuntu", "USN-6620-1.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesDebianDla()
|
||||
{
|
||||
var advisory = CreateAdvisory("DLA-1234-1");
|
||||
@@ -63,7 +68,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("debian", "DLA-1234-1.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesRedHatRhsa()
|
||||
{
|
||||
var advisory = CreateAdvisory("RHSA-2024:0252");
|
||||
@@ -73,7 +79,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("redhat", "oval", "RHSA-2024_0252.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesAmazonAlas()
|
||||
{
|
||||
var advisory = CreateAdvisory("ALAS2-2024-1234");
|
||||
@@ -83,7 +90,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("amazon", "2", "ALAS2-2024-1234.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesOracleElsa()
|
||||
{
|
||||
var advisory = CreateAdvisory("ELSA-2024-12345");
|
||||
@@ -93,7 +101,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("oracle", "linux", "ELSA-2024-12345.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesRockyRlsa()
|
||||
{
|
||||
var advisory = CreateAdvisory("RLSA-2024:0417");
|
||||
@@ -103,7 +112,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("rocky", "RLSA-2024_0417.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesByProvenanceFallback()
|
||||
{
|
||||
var provenance = new[] { new AdvisoryProvenance("wolfi", "map", "", DefaultPublished) };
|
||||
@@ -114,7 +124,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("wolfi", "WOLFI-2024-0001.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolvesAcscByProvenance()
|
||||
{
|
||||
var provenance = new[] { new AdvisoryProvenance("acsc", "mapping", "acsc-2025-010", DefaultPublished) };
|
||||
@@ -125,7 +136,8 @@ public sealed class VulnListJsonExportPathResolverTests
|
||||
Assert.Equal(Path.Combine("cert", "au", "acsc-2025-010.json"), path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DefaultsToMiscWhenUnmapped()
|
||||
{
|
||||
var advisory = CreateAdvisory("CUSTOM-2024-99");
|
||||
|
||||
@@ -2,11 +2,13 @@ using System;
|
||||
using StellaOps.Concelier.Exporter.TrivyDb;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
|
||||
|
||||
public sealed class TrivyDbExportPlannerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreatePlan_ReturnsFullWhenStateMissing()
|
||||
{
|
||||
var planner = new TrivyDbExportPlanner();
|
||||
@@ -21,7 +23,8 @@ public sealed class TrivyDbExportPlannerTests
|
||||
Assert.Equal(manifest, plan.Manifest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreatePlan_ReturnsSkipWhenCursorMatches()
|
||||
{
|
||||
var planner = new TrivyDbExportPlanner();
|
||||
@@ -49,7 +52,8 @@ public sealed class TrivyDbExportPlannerTests
|
||||
Assert.Empty(plan.RemovedPaths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CreatePlan_ReturnsFullWhenCursorDiffers()
|
||||
{
|
||||
var planner = new TrivyDbExportPlanner();
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
_jsonRoot = Path.Combine(_root, "tree");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_SortsAdvisoriesByKeyDeterministically()
|
||||
{
|
||||
var advisoryB = CreateSampleAdvisory("CVE-2024-1002", "Second advisory");
|
||||
@@ -98,7 +99,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Single(recordingBuilder.ManifestDigests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_SmallDatasetProducesDeterministicOciLayout()
|
||||
{
|
||||
var advisories = new[]
|
||||
@@ -134,7 +136,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Equal(TrivyDbMediaTypes.TrivyLayer, layer.GetProperty("mediaType").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ExportOptions_GetExportRoot_NormalizesRelativeRoot()
|
||||
{
|
||||
var options = new TrivyDbExportOptions
|
||||
@@ -149,7 +152,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.EndsWith(Path.Combine("exports", "trivy-test", exportId), path, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_PersistsStateAndSkipsWhenDigestUnchanged()
|
||||
{
|
||||
var advisory = CreateSampleAdvisory();
|
||||
@@ -222,7 +226,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Empty(orasPusher.Pushes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_CreatesOfflineBundle()
|
||||
{
|
||||
var advisory = CreateSampleAdvisory();
|
||||
@@ -282,7 +287,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Empty(orasPusher.Pushes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_WritesMirrorBundlesWhenConfigured()
|
||||
{
|
||||
var advisoryOne = CreateSampleAdvisory("CVE-2025-1001", "Mirror Advisory One");
|
||||
@@ -431,7 +437,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Empty(orasPusher.Pushes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_SkipsOrasPushWhenDeltaPublishingDisabled()
|
||||
{
|
||||
var initial = CreateSampleAdvisory("CVE-2024-7100", "Publish toggles");
|
||||
@@ -492,7 +499,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Empty(orasPusher.Pushes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_SkipsOfflineBundleForDeltaWhenDisabled()
|
||||
{
|
||||
var initial = CreateSampleAdvisory("CVE-2024-7200", "Offline delta toggles");
|
||||
@@ -562,7 +570,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.False(File.Exists(deltaBundlePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_ResetsBaselineWhenDeltaChainExists()
|
||||
{
|
||||
var advisory = CreateSampleAdvisory("CVE-2024-5000", "Baseline reset");
|
||||
@@ -635,7 +644,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.NotEmpty(updated.Files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_DeltaSequencePromotesBaselineReset()
|
||||
{
|
||||
var baseline = CreateSampleAdvisory("CVE-2024-8100", "Baseline advisory");
|
||||
@@ -725,7 +735,8 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
Assert.Equal(finalExportId, state.BaseExportId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExportAsync_DeltaReusesBaseLayerOnDisk()
|
||||
{
|
||||
var baseline = CreateSampleAdvisory("CVE-2024-8300", "Layer reuse baseline");
|
||||
@@ -1185,6 +1196,7 @@ public sealed class TrivyDbFeedExporterTests : IDisposable
|
||||
var archivePath = Path.Combine(workingDirectory, "db.tar.gz");
|
||||
File.WriteAllBytes(archivePath, _payload);
|
||||
using var sha256 = SHA256.Create();
|
||||
using StellaOps.TestKit;
|
||||
var digest = "sha256:" + Convert.ToHexString(sha256.ComputeHash(_payload)).ToLowerInvariant();
|
||||
|
||||
return Task.FromResult(new TrivyDbBuilderResult(
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Concelier.Storage.Exporting;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
|
||||
|
||||
public sealed class TrivyDbOciWriterTests : IDisposable
|
||||
@@ -21,7 +22,8 @@ public sealed class TrivyDbOciWriterTests : IDisposable
|
||||
_root = Directory.CreateTempSubdirectory("trivy-writer-tests").FullName;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task WriteAsync_ReusesBlobsFromBaseLayout_WhenDigestMatches()
|
||||
{
|
||||
var baseLayout = Path.Combine(_root, "base");
|
||||
|
||||
@@ -6,11 +6,13 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Concelier.Exporter.TrivyDb;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Exporter.TrivyDb.Tests;
|
||||
|
||||
public sealed class TrivyDbPackageBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildsOciManifestWithExpectedMediaTypes()
|
||||
{
|
||||
var metadata = Encoding.UTF8.GetBytes("{\"generatedAt\":\"2024-07-15T12:00:00Z\"}");
|
||||
@@ -56,7 +58,8 @@ public sealed class TrivyDbPackageBuilderTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ThrowsWhenMetadataMissing()
|
||||
{
|
||||
var builder = new TrivyDbPackageBuilder();
|
||||
|
||||
@@ -9,6 +9,7 @@ using FluentAssertions;
|
||||
using StellaOps.Concelier.Interest.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Interest.Tests;
|
||||
|
||||
public class InterestScoreCalculatorTests
|
||||
@@ -21,7 +22,8 @@ public class InterestScoreCalculatorTests
|
||||
_calculator = new InterestScoreCalculator(_defaultWeights);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithNoSignals_ReturnsBaseScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -43,7 +45,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithSbomMatch_AddsInSbomFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -72,7 +75,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithReachableSbomMatch_AddsReachableFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -102,7 +106,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithDeployedSbomMatch_AddsDeployedFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -132,7 +137,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithFullSbomMatch_AddsAllSbomFactors()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +169,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithVexNotAffected_ExcludesVexFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -203,7 +210,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().NotContain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithRecentLastSeen_AddsRecentFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -233,7 +241,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().Contain("recent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithOldLastSeen_DecaysRecentFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -263,7 +272,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().NotContain("recent"); // decayFactor < 0.5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_WithVeryOldLastSeen_NoRecentFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +294,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().NotContain("recent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_MaxScore_IsCappedAt1()
|
||||
{
|
||||
// Arrange - use custom weights that exceed 1.0
|
||||
@@ -322,7 +333,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Score.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_SetsComputedAtToNow()
|
||||
{
|
||||
// Arrange
|
||||
@@ -338,7 +350,8 @@ public class InterestScoreCalculatorTests
|
||||
result.ComputedAt.Should().BeOnOrBefore(after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Calculate_PreservesCanonicalId()
|
||||
{
|
||||
// Arrange
|
||||
@@ -352,7 +365,8 @@ public class InterestScoreCalculatorTests
|
||||
result.CanonicalId.Should().Be(canonicalId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(VexStatus.Affected)]
|
||||
[InlineData(VexStatus.Fixed)]
|
||||
[InlineData(VexStatus.UnderInvestigation)]
|
||||
@@ -379,7 +393,8 @@ public class InterestScoreCalculatorTests
|
||||
result.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestTier_HighScore_ReturnsHigh()
|
||||
{
|
||||
// Arrange
|
||||
@@ -395,7 +410,8 @@ public class InterestScoreCalculatorTests
|
||||
score.Tier.Should().Be(InterestTier.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestTier_MediumScore_ReturnsMedium()
|
||||
{
|
||||
// Arrange
|
||||
@@ -411,7 +427,8 @@ public class InterestScoreCalculatorTests
|
||||
score.Tier.Should().Be(InterestTier.Medium);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestTier_LowScore_ReturnsLow()
|
||||
{
|
||||
// Arrange
|
||||
@@ -427,7 +444,8 @@ public class InterestScoreCalculatorTests
|
||||
score.Tier.Should().Be(InterestTier.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestTier_NoneScore_ReturnsNone()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -13,6 +13,7 @@ using Moq;
|
||||
using StellaOps.Concelier.Interest.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Interest.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,7 +58,8 @@ public class InterestScoringServiceTests
|
||||
|
||||
#region Task 18: Integration Tests - Score Persistence
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_PersistsToRepository()
|
||||
{
|
||||
// Arrange
|
||||
@@ -72,7 +74,8 @@ public class InterestScoringServiceTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetScoreAsync_RetrievesFromRepository()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +95,8 @@ public class InterestScoringServiceTests
|
||||
result.Score.Should().Be(0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetScoreAsync_ReturnsNull_WhenNotFound()
|
||||
{
|
||||
// Arrange
|
||||
@@ -107,7 +111,8 @@ public class InterestScoringServiceTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BatchUpdateAsync_UpdatesMultipleScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +127,8 @@ public class InterestScoringServiceTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BatchUpdateAsync_HandlesEmptyInput()
|
||||
{
|
||||
// Act
|
||||
@@ -138,7 +144,8 @@ public class InterestScoringServiceTests
|
||||
|
||||
#region Task 23: Job Execution and Score Consistency
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RecalculateAllAsync_ReturnsZero_WhenNoAdvisoryStore()
|
||||
{
|
||||
// The service is created without an ICanonicalAdvisoryStore,
|
||||
@@ -152,7 +159,8 @@ public class InterestScoringServiceTests
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_ProducesDeterministicResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -167,7 +175,8 @@ public class InterestScoringServiceTests
|
||||
result1.Reasons.Should().BeEquivalentTo(result2.Reasons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_ReturnsValidScoreRange()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +191,8 @@ public class InterestScoringServiceTests
|
||||
result.ComputedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_PreservesScoreConsistency()
|
||||
{
|
||||
// Arrange
|
||||
@@ -206,7 +216,8 @@ public class InterestScoringServiceTests
|
||||
savedScore.Reasons.Should().BeEquivalentTo(["in_sbom", "reachable"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BatchUpdateAsync_MaintainsScoreOrdering()
|
||||
{
|
||||
// Arrange
|
||||
@@ -232,7 +243,8 @@ public class InterestScoringServiceTests
|
||||
|
||||
#region Task 28: Degradation/Restoration Cycle
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DegradeToStubsAsync_ReturnsZero_WhenNoAdvisoryStore()
|
||||
{
|
||||
// The service is created without an ICanonicalAdvisoryStore,
|
||||
@@ -246,7 +258,8 @@ public class InterestScoringServiceTests
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RestoreFromStubsAsync_ReturnsZero_WhenNoAdvisoryStore()
|
||||
{
|
||||
// The service is created without an ICanonicalAdvisoryStore,
|
||||
@@ -259,7 +272,8 @@ public class InterestScoringServiceTests
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DegradeRestoreCycle_MaintainsDataIntegrity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -293,7 +307,8 @@ public class InterestScoringServiceTests
|
||||
stored.Reasons.Should().Contain("in_sbom");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DegradeToStubsAsync_ReturnsZero_WhenNoLowScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -312,7 +327,8 @@ public class InterestScoringServiceTests
|
||||
result.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RestoreFromStubsAsync_ReturnsZero_WhenNoHighScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -334,7 +350,8 @@ public class InterestScoringServiceTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_HandlesBoundaryScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -350,7 +367,8 @@ public class InterestScoringServiceTests
|
||||
Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_HandlesNullInputGracefully()
|
||||
{
|
||||
// Act
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace StellaOps.Concelier.Merge.Analyzers.Tests;
|
||||
|
||||
public sealed class MergeUsageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReportsDiagnostic_ForAdvisoryMergeServiceInstantiation()
|
||||
{
|
||||
const string source = """
|
||||
@@ -33,7 +34,8 @@ public sealed class MergeUsageAnalyzerTests
|
||||
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId && d.GetMessage().Contains("AdvisoryMergeService", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReportsDiagnostic_ForAddMergeModuleInvocation()
|
||||
{
|
||||
const string source = """
|
||||
@@ -56,7 +58,8 @@ public sealed class MergeUsageAnalyzerTests
|
||||
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId && d.GetMessage().Contains("AddMergeModule", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReportsDiagnostic_ForFieldDeclaration()
|
||||
{
|
||||
const string source = """
|
||||
@@ -74,7 +77,8 @@ public sealed class MergeUsageAnalyzerTests
|
||||
Assert.Contains(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DoesNotReportDiagnostic_InsideMergeAssembly()
|
||||
{
|
||||
const string source = """
|
||||
@@ -92,13 +96,15 @@ public sealed class MergeUsageAnalyzerTests
|
||||
Assert.DoesNotContain(diagnostics, d => d.Id == MergeUsageAnalyzer.DiagnosticId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReportsDiagnostic_ForTypeOfUsage()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace Sample.TypeOf;
|
||||
|
||||
public static class Demo
|
||||
|
||||
@@ -4,13 +4,15 @@ using StellaOps.Concelier.Merge.Identity;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryIdentityResolverTests
|
||||
{
|
||||
private readonly AdvisoryIdentityResolver _resolver = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_GroupsBySharedCveAlias()
|
||||
{
|
||||
var nvd = CreateAdvisory("CVE-2025-1234", aliases: new[] { "CVE-2025-1234" }, source: "nvd");
|
||||
@@ -24,7 +26,8 @@ public sealed class AdvisoryIdentityResolverTests
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "CVE-2025-1234"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_PrefersPsirtAliasWhenNoCve()
|
||||
{
|
||||
var vendor = CreateAdvisory("VMSA-2025-0001", aliases: new[] { "VMSA-2025-0001" }, source: "vmware");
|
||||
@@ -38,7 +41,8 @@ public sealed class AdvisoryIdentityResolverTests
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "VMSA-2025-0001"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_FallsBackToGhsaWhenOnlyGhsaPresent()
|
||||
{
|
||||
var ghsa = CreateAdvisory("GHSA-aaaa-bbbb-cccc", aliases: new[] { "GHSA-aaaa-bbbb-cccc" }, source: "ghsa");
|
||||
@@ -52,7 +56,8 @@ public sealed class AdvisoryIdentityResolverTests
|
||||
Assert.True(cluster.Aliases.Any(alias => alias.Value == "GHSA-aaaa-bbbb-cccc"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Resolve_GroupsByKeyWhenNoAliases()
|
||||
{
|
||||
var first = CreateAdvisory("custom-1", aliases: Array.Empty<string>(), source: "source-a");
|
||||
|
||||
@@ -15,11 +15,13 @@ using StellaOps.Concelier.Storage.Aliases;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
using StellaOps.Provenance;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryMergeServiceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MergeAsync_AppliesCanonicalRulesAndPersistsDecisions()
|
||||
{
|
||||
var aliasStore = new FakeAliasStore();
|
||||
@@ -167,7 +169,8 @@ public sealed class AdvisoryMergeServiceTests
|
||||
provenance: new[] { provenance });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MergeAsync_PersistsConflictSummariesWithHashes()
|
||||
{
|
||||
var aliasStore = new FakeAliasStore();
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AdvisoryPrecedenceMergerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_PrefersVendorPrecedenceOverNvd()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -59,7 +60,8 @@ public sealed class AdvisoryPrecedenceMergerTests
|
||||
Assert.Contains(severityConflict.Tags, tag => string.Equals(tag.Key, "type", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "severity", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_KevOnlyTogglesExploitKnown()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -128,7 +130,8 @@ public sealed class AdvisoryPrecedenceMergerTests
|
||||
Assert.Contains(merged.Provenance, provenance => provenance.Source == "merge");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_UnionsCreditsFromSources()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
@@ -231,7 +234,8 @@ public sealed class AdvisoryPrecedenceMergerTests
|
||||
Assert.Contains(merged.Credits, credit => credit.Provenance.Source == "osv");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_AcscActsAsEnrichmentSource()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 12, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -333,7 +337,8 @@ public sealed class AdvisoryPrecedenceMergerTests
|
||||
Assert.Contains(merged.Provenance, provenance => provenance.Source == "merge" && (provenance.Value?.Contains("acsc", StringComparison.OrdinalIgnoreCase) ?? false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_RecordsNormalizedRuleMetrics()
|
||||
{
|
||||
var now = new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
@@ -474,7 +479,8 @@ public sealed class AdvisoryPrecedenceMergerTests
|
||||
Assert.Contains(missingMeasurement.Tags, tag => string.Equals(tag.Key, "package_type", StringComparison.Ordinal) && string.Equals(tag.Value?.ToString(), "semver", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_RespectsConfiguredPrecedenceOverrides()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 3, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
@@ -490,6 +496,7 @@ public sealed class AdvisoryPrecedenceMergerTests
|
||||
var logger = new TestLogger<AdvisoryPrecedenceMerger>();
|
||||
using var metrics = new MetricCollector("StellaOps.Concelier.Merge");
|
||||
|
||||
using StellaOps.TestKit;
|
||||
var merger = new AdvisoryPrecedenceMerger(
|
||||
new AffectedPackagePrecedenceResolver(),
|
||||
options,
|
||||
|
||||
@@ -4,11 +4,13 @@ using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AffectedPackagePrecedenceResolverTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_PrefersRedHatOverNvdForSameCpe()
|
||||
{
|
||||
var redHat = new AffectedPackage(
|
||||
@@ -64,7 +66,8 @@ public sealed class AffectedPackagePrecedenceResolverTests
|
||||
Assert.Equal(1, rangeOverride.SuppressedRangeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Merge_KeepsNvdWhenNoHigherPrecedence()
|
||||
{
|
||||
var nvd = new AffectedPackage(
|
||||
|
||||
@@ -6,11 +6,13 @@ using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Storage.Aliases;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class AliasGraphResolverTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ReturnsCollisions_WhenAliasesOverlap()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
@@ -39,7 +41,8 @@ public sealed class AliasGraphResolverTests
|
||||
Assert.Contains("ADV-2", collision.AdvisoryKeys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_TracesConnectedAdvisories()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
@@ -73,7 +76,8 @@ public sealed class AliasGraphResolverTests
|
||||
Assert.Contains(component.AliasMap["ADV-B"], record => record.Scheme == "OSV" && record.Value == "OSV-2025-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BuildComponentAsync_LinksOsvAndGhsaAliases()
|
||||
{
|
||||
var aliasStore = new AliasStore();
|
||||
|
||||
@@ -4,18 +4,21 @@ using StellaOps.Concelier.Normalization.Distro;
|
||||
using StellaOps.VersionComparison;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class ApkVersionComparerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComparatorType_Returns_Apk()
|
||||
{
|
||||
Assert.Equal(ComparatorType.Apk, ApkVersionComparer.Instance.ComparatorType);
|
||||
}
|
||||
public static TheoryData<string, string, int, string> ComparisonCases => BuildComparisonCases();
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(ComparisonCases))]
|
||||
public void Compare_ApkVersions_ReturnsExpectedOrder(string left, string right, int expected, string note)
|
||||
{
|
||||
@@ -23,7 +26,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.True(expected == actual, $"[{note}] '{left}' vs '{right}': expected {expected}, got {actual}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_ParsesApkVersionComponents()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.Compare(
|
||||
@@ -84,7 +88,8 @@ public sealed class ApkVersionComparerTests
|
||||
|
||||
#region CompareWithProof Tests (SPRINT_4000_0002_0001)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_BothNull_ReturnsEqual()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof(null, null);
|
||||
@@ -94,7 +99,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.Contains("null", result.ProofLines[0].ToLower());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_LeftNull_ReturnsLess()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof(null, "1.0-r0");
|
||||
@@ -103,7 +109,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.Contains("null", result.ProofLines[0].ToLower());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_RightNull_ReturnsGreater()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.0-r0", null);
|
||||
@@ -112,7 +119,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.Contains("null", result.ProofLines[0].ToLower());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EqualVersions_ReturnsEqualWithProof()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.2.3-r1", "1.2.3-r1");
|
||||
@@ -122,7 +130,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.Contains(result.ProofLines, line => line.Contains("equal"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_VersionDifference_ReturnsProofLines()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.2.3-r0", "1.2.4-r0");
|
||||
@@ -133,7 +142,8 @@ public sealed class ApkVersionComparerTests
|
||||
line.Contains("Version") || line.Contains("older") || line.Contains("<"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_PkgRelDifference_ReturnsProofWithPkgRel()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.2.3-r1", "1.2.3-r2");
|
||||
@@ -142,7 +152,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.Contains(result.ProofLines, line => line.Contains("release") || line.Contains("-r"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_ImplicitVsExplicitPkgRel_ReturnsProofExplaining()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.2.3", "1.2.3-r0");
|
||||
@@ -151,7 +162,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.Contains(result.ProofLines, line => line.Contains("implicit") || line.Contains("explicit"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_NewerVersion_ReturnsGreaterThanOrEqual()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.2.4-r0", "1.2.3-r0");
|
||||
@@ -160,7 +172,8 @@ public sealed class ApkVersionComparerTests
|
||||
Assert.True(result.IsGreaterThanOrEqual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_InvalidVersions_FallsBackToStringComparison()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("", "");
|
||||
@@ -172,7 +185,8 @@ public sealed class ApkVersionComparerTests
|
||||
line.Contains("equal", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_ReturnsCorrectComparatorType()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.CompareWithProof("1.0-r0", "1.0-r1");
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Concelier.Merge.Backport;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -35,7 +36,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Tier 1: DistroAdvisory Evidence
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier1DistroAdvisory_ExtractsEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -60,7 +62,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
evidence.DistroRelease.Should().Contain("debian");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier1LowConfidence_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -83,7 +86,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Tier 2: ChangelogMention Evidence
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier2ChangelogMention_ExtractsEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -108,7 +112,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
evidence.DistroRelease.Should().Contain("redhat");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier2WithUpstreamCommit_ExtractsPatchLineage()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +149,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Tier 3: PatchHeader Evidence
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier3PatchHeader_ExtractsEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -168,7 +174,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
evidence.PatchOrigin.Should().Be(PatchOrigin.Upstream);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier3DistroPatch_DetectsDistroOrigin()
|
||||
{
|
||||
// Arrange
|
||||
@@ -204,7 +211,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Tier 4: BinaryFingerprint Evidence
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_Tier4BinaryFingerprint_ExtractsEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -230,7 +238,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Tier Priority
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_MultipleTiers_SelectsHighestTier()
|
||||
{
|
||||
// Arrange: BinaryFingerprint (Tier 4) should be selected as highest
|
||||
@@ -256,7 +265,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
evidence!.Tier.Should().Be(BackportEvidenceTier.BinaryFingerprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_PatchHeaderVsChangelog_PrefersPatchHeader()
|
||||
{
|
||||
// Arrange: PatchHeader (Tier 3) > ChangelogMention (Tier 2)
|
||||
@@ -286,7 +296,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Distro Release Extraction
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("pkg:deb/debian/curl@7.64.0-4+deb11u1", "debian:bullseye")]
|
||||
[InlineData("pkg:deb/debian/openssl@3.0.11-1~deb12u2", "debian:bookworm")]
|
||||
[InlineData("pkg:rpm/redhat/nginx@1.20.1-14.el9", "redhat:9")]
|
||||
@@ -314,7 +325,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Batch Resolution
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveBatchAsync_ResolvesMultiplePackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -350,7 +362,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_NullProof_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -365,7 +378,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
evidence.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_VeryLowConfidence_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -383,7 +397,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
evidence.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HasEvidenceAsync_ReturnsTrueWhenEvidenceExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -401,7 +416,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
hasEvidence.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task HasEvidenceAsync_ReturnsFalseWhenNoEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -416,7 +432,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
hasEvidence.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ThrowsOnNullCveId()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -424,7 +441,8 @@ public sealed class BackportEvidenceResolverTests
|
||||
() => _resolver.ResolveAsync(null!, "pkg:deb/debian/test@1.0"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ResolveAsync_ThrowsOnNullPurl()
|
||||
{
|
||||
// Act & Assert
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +66,8 @@ public sealed class BackportProvenanceE2ETests
|
||||
|
||||
#region E2E: Debian Backport Advisory Flow
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_IngestDebianAdvisoryWithBackport_CreatesProvenanceScope()
|
||||
{
|
||||
// Arrange: Simulate Debian security advisory for CVE-2024-1234
|
||||
@@ -129,7 +131,8 @@ public sealed class BackportProvenanceE2ETests
|
||||
capturedScope.PatchId.Should().Be(patchCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_IngestRhelAdvisoryWithBackport_CreatesProvenanceScopeWithDistroOrigin()
|
||||
{
|
||||
// Arrange: Simulate RHEL security advisory with distro-specific patch
|
||||
@@ -186,7 +189,8 @@ public sealed class BackportProvenanceE2ETests
|
||||
|
||||
#region E2E: Multiple Distro Backports for Same CVE
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_SameCveMultipleDistros_CreatesSeparateProvenanceScopes()
|
||||
{
|
||||
// Arrange: Same CVE with Debian and Ubuntu backports
|
||||
@@ -239,7 +243,8 @@ public sealed class BackportProvenanceE2ETests
|
||||
|
||||
#region E2E: Merge Event with Backport Evidence
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_MergeWithBackportEvidence_RecordsInAuditLog()
|
||||
{
|
||||
// Arrange
|
||||
@@ -296,7 +301,8 @@ public sealed class BackportProvenanceE2ETests
|
||||
|
||||
#region E2E: Evidence Tier Upgrade
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_EvidenceUpgrade_UpdatesProvenanceScope()
|
||||
{
|
||||
// Arrange: Start with low-tier evidence, then upgrade
|
||||
@@ -355,7 +361,8 @@ public sealed class BackportProvenanceE2ETests
|
||||
|
||||
#region E2E: Provenance Retrieval
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task E2E_RetrieveProvenanceForCanonical_ReturnsAllDistroScopes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class CanonicalHashCalculatorTests
|
||||
@@ -41,7 +42,8 @@ public sealed class CanonicalHashCalculatorTests
|
||||
},
|
||||
provenance: new[] { AdvisoryProvenance.Empty });
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_ReturnsDeterministicValue()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
@@ -51,7 +53,8 @@ public sealed class CanonicalHashCalculatorTests
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_IgnoresOrderingDifferences()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
@@ -77,7 +80,8 @@ public sealed class CanonicalHashCalculatorTests
|
||||
Assert.Equal(originalHash, reorderedHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeHash_NullReturnsEmpty()
|
||||
{
|
||||
var calculator = new CanonicalHashCalculator();
|
||||
|
||||
@@ -3,11 +3,13 @@ using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class DebianEvrComparerTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1:1.2.3-1", 1, "1.2.3", "1")]
|
||||
[InlineData("1.2.3-1", 0, "1.2.3", "1")]
|
||||
[InlineData("2:4.5", 2, "4.5", "")]
|
||||
@@ -24,7 +26,8 @@ public sealed class DebianEvrComparerTests
|
||||
Assert.Equal(input, evr.Original);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(":1.0-1")]
|
||||
[InlineData("1:")]
|
||||
@@ -36,7 +39,8 @@ public sealed class DebianEvrComparerTests
|
||||
Assert.Null(evr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_PrefersHigherEpoch()
|
||||
{
|
||||
var lower = "0:2.0-1";
|
||||
@@ -45,7 +49,8 @@ public sealed class DebianEvrComparerTests
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_UsesVersionOrdering()
|
||||
{
|
||||
var lower = "0:1.2.3-1";
|
||||
@@ -54,7 +59,8 @@ public sealed class DebianEvrComparerTests
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_TildeRanksEarlier()
|
||||
{
|
||||
var prerelease = "0:1.0~beta1-1";
|
||||
@@ -63,7 +69,8 @@ public sealed class DebianEvrComparerTests
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(prerelease, stable) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_RevisionBreaksTies()
|
||||
{
|
||||
var first = "0:1.0-1";
|
||||
@@ -72,7 +79,8 @@ public sealed class DebianEvrComparerTests
|
||||
Assert.True(DebianEvrComparer.Instance.Compare(second, first) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_FallsBackToOrdinalForInvalid()
|
||||
{
|
||||
var left = "not-an-evr";
|
||||
@@ -86,7 +94,8 @@ public sealed class DebianEvrComparerTests
|
||||
|
||||
public static TheoryData<string, string, int, string> ComparisonCases => BuildComparisonCases();
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(ComparisonCases))]
|
||||
public void Compare_DebianEvr_ReturnsExpectedOrder(string left, string right, int expected, string note)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text;
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class GoldenVersionComparisonTests
|
||||
@@ -16,7 +17,8 @@ public sealed class GoldenVersionComparisonTests
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("rpm_version_comparison.golden.ndjson", "rpm")]
|
||||
[InlineData("deb_version_comparison.golden.ndjson", "deb")]
|
||||
[InlineData("apk_version_comparison.golden.ndjson", "apk")]
|
||||
|
||||
@@ -4,11 +4,13 @@ using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class MergeEventWriterTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AppendAsync_WritesRecordWithComputedHashes()
|
||||
{
|
||||
var store = new InMemoryMergeEventStore();
|
||||
@@ -31,7 +33,8 @@ public sealed class MergeEventWriterTests
|
||||
Assert.Same(store.LastRecord, record);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AppendAsync_NullBeforeUsesEmptyHash()
|
||||
{
|
||||
var store = new InMemoryMergeEventStore();
|
||||
|
||||
@@ -9,6 +9,7 @@ using FluentAssertions;
|
||||
using StellaOps.Concelier.Merge.Identity;
|
||||
using StellaOps.Concelier.Merge.Identity.Normalizers;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -26,7 +27,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
|
||||
#region Same Patch Lineage = Same Hash
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_SamePatchLineage_ProducesSameHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -56,7 +58,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
hash1.Should().Be(hash2, "same patch lineage should produce same hash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_NoPatchLineage_ProducesSameHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -90,7 +93,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
|
||||
#region Different Patch Lineage = Different Hash
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_DifferentPatchLineage_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange - Upstream fix vs distro-specific backport
|
||||
@@ -121,7 +125,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
"different patch lineage should produce different hash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_WithVsWithoutPatchLineage_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -152,7 +157,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
"advisory with patch lineage should differ from one without");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_DebianVsRhelBackport_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange - Same CVE, different distro backports
|
||||
@@ -187,7 +193,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
|
||||
#region Patch Lineage Normalization
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData(
|
||||
"abc123def456abc123def456abc123def456abcd",
|
||||
"ABC123DEF456ABC123DEF456ABC123DEF456ABCD",
|
||||
@@ -230,7 +237,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
hash1.Should().Be(hash2, reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_AbbreviatedSha_DiffersFromFullSha()
|
||||
{
|
||||
// Abbreviated SHA is treated as different from a full different SHA
|
||||
@@ -265,7 +273,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
|
||||
#region Real-World Scenarios
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_GoldenCorpus_DebianBackportVsNvd()
|
||||
{
|
||||
// Golden corpus test case: CVE-2024-1234 with Debian backport
|
||||
@@ -300,7 +309,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
"NVD and Debian entries should produce different hashes due to package and version differences");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_GoldenCorpus_DistroSpecificFix()
|
||||
{
|
||||
// Golden corpus test case: Distro-specific fix different from upstream
|
||||
@@ -331,7 +341,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
"distro-specific fix should produce different hash from upstream");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_SameUpstreamBackported_ProducesSameHash()
|
||||
{
|
||||
// When two distros backport the SAME upstream patch, they should merge
|
||||
@@ -367,7 +378,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_EmptyPatchLineage_TreatedAsNull()
|
||||
{
|
||||
var emptyLineage = new MergeHashInput
|
||||
@@ -397,7 +409,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
"empty and null patch lineage should produce same hash");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_WhitespacePatchLineage_TreatedAsNull()
|
||||
{
|
||||
var whitespaceLineage = new MergeHashInput
|
||||
@@ -427,7 +440,8 @@ public sealed class MergeHashBackportDifferentiationTests
|
||||
"whitespace-only patch lineage should be treated as null");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ComputeMergeHash_IsDeterministic()
|
||||
{
|
||||
// Verify determinism across multiple calls
|
||||
|
||||
@@ -7,6 +7,7 @@ using StellaOps.Concelier.Merge.Services;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage.MergeEvents;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
@@ -16,7 +17,8 @@ public sealed class MergePrecedenceIntegrationTests : IAsyncLifetime
|
||||
private AdvisoryPrecedenceMerger? _merger;
|
||||
private FakeTimeProvider? _timeProvider;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MergePipeline_PsirtOverridesNvd_AndKevOnlyTogglesExploitKnown()
|
||||
{
|
||||
await EnsureInitializedAsync();
|
||||
|
||||
@@ -3,11 +3,13 @@ using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class NevraComparerTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("kernel-1:4.18.0-348.7.1.el8_5.x86_64", "kernel", 1, "4.18.0", "348.7.1.el8_5", "x86_64")]
|
||||
[InlineData("bash-5.1.8-2.fc35.x86_64", "bash", 0, "5.1.8", "2.fc35", "x86_64")]
|
||||
[InlineData("openssl-libs-1:1.1.1k-7.el8", "openssl-libs", 1, "1.1.1k", "7.el8", null)]
|
||||
@@ -28,7 +30,8 @@ public sealed class NevraComparerTests
|
||||
Assert.Equal(input, nevra.Original);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("kernel4.18.0-80.el8")]
|
||||
[InlineData("kernel-4.18.0")]
|
||||
@@ -40,7 +43,8 @@ public sealed class NevraComparerTests
|
||||
Assert.Null(nevra);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_TrimsWhitespace()
|
||||
{
|
||||
var success = Nevra.TryParse(" kernel-0:4.18.0-80.el8.x86_64 ", out var nevra);
|
||||
@@ -51,7 +55,8 @@ public sealed class NevraComparerTests
|
||||
Assert.Equal("4.18.0", nevra.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_PrefersHigherEpoch()
|
||||
{
|
||||
var older = "kernel-0:4.18.0-348.7.1.el8_5.x86_64";
|
||||
@@ -61,7 +66,8 @@ public sealed class NevraComparerTests
|
||||
Assert.True(NevraComparer.Instance.Compare(older, newer) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_UsesRpmVersionOrdering()
|
||||
{
|
||||
var lower = "kernel-0:4.18.0-80.el8.x86_64";
|
||||
@@ -70,7 +76,8 @@ public sealed class NevraComparerTests
|
||||
Assert.True(NevraComparer.Instance.Compare(higher, lower) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_UsesReleaseOrdering()
|
||||
{
|
||||
var el8 = "bash-0:5.1.0-1.el8.x86_64";
|
||||
@@ -79,7 +86,8 @@ public sealed class NevraComparerTests
|
||||
Assert.True(NevraComparer.Instance.Compare(el9, el8) > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_TildeRanksEarlier()
|
||||
{
|
||||
var prerelease = "bash-0:5.1.0~beta-1.fc34.x86_64";
|
||||
@@ -88,7 +96,8 @@ public sealed class NevraComparerTests
|
||||
Assert.True(NevraComparer.Instance.Compare(prerelease, stable) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_ConsidersArchitecture()
|
||||
{
|
||||
var noarch = "pkg-0:1.0-1.noarch";
|
||||
@@ -97,7 +106,8 @@ public sealed class NevraComparerTests
|
||||
Assert.True(NevraComparer.Instance.Compare(noarch, arch) < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_FallsBackToOrdinalForInvalid()
|
||||
{
|
||||
var left = "not-a-nevra";
|
||||
@@ -110,7 +120,8 @@ public sealed class NevraComparerTests
|
||||
|
||||
public static TheoryData<string, string, int, string> ComparisonCases => BuildComparisonCases();
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(ComparisonCases))]
|
||||
public void Compare_NevraVersions_ReturnsExpectedOrder(string left, string right, int expected, string note)
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Concelier.Merge.Backport;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -34,7 +35,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
|
||||
#region CreateOrUpdateAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_NewScope_CreatesProvenanceScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -74,7 +76,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_ExistingScope_UpdatesProvenanceScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -117,7 +120,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
result.ProvenanceScopeId.Should().Be(existingScopeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_WithEvidenceResolver_ResolvesEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -171,7 +175,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateOrUpdateAsync_NonDistroSource_StillCreatesScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -204,7 +209,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
|
||||
#region UpdateFromEvidenceAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateFromEvidenceAsync_NewEvidence_CreatesScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -246,7 +252,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateFromEvidenceAsync_BetterEvidence_UpdatesScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -300,7 +307,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateFromEvidenceAsync_LowerConfidenceEvidence_SkipsUpdate()
|
||||
{
|
||||
// Arrange
|
||||
@@ -352,7 +360,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
|
||||
#region LinkEvidenceRefAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LinkEvidenceRefAsync_LinksEvidenceToScope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -374,7 +383,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
|
||||
#region GetByCanonicalIdAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCanonicalIdAsync_ReturnsAllScopes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -418,7 +428,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
|
||||
#region DeleteByCanonicalIdAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteByCanonicalIdAsync_DeletesAllScopes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -439,7 +450,8 @@ public sealed class ProvenanceScopeLifecycleTests
|
||||
|
||||
#region Distro Release Extraction Tests
|
||||
|
||||
[Theory]
|
||||
[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")]
|
||||
|
||||
@@ -2,11 +2,13 @@ using FluentAssertions;
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class SemanticVersionRangeResolverTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1.2.3", true)]
|
||||
[InlineData("1.2.3-beta.1", true)]
|
||||
[InlineData("invalid", false)]
|
||||
@@ -19,14 +21,16 @@ public sealed class SemanticVersionRangeResolverTests
|
||||
Assert.Equal(expected, version is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_ParsesSemanticVersions()
|
||||
{
|
||||
Assert.True(SemanticVersionRangeResolver.Compare("1.2.3", "1.2.2") > 0);
|
||||
Assert.True(SemanticVersionRangeResolver.Compare("1.2.3-beta", "1.2.3") < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_UsesOrdinalFallbackForInvalid()
|
||||
{
|
||||
var left = "zzz";
|
||||
@@ -37,7 +41,8 @@ public sealed class SemanticVersionRangeResolverTests
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveWindows_WithFixedVersion_ComputesExclusiveUpper()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows("1.0.0", "1.2.0", null);
|
||||
@@ -47,7 +52,8 @@ public sealed class SemanticVersionRangeResolverTests
|
||||
Assert.Null(inclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveWindows_WithLastAffectedOnly_ComputesInclusiveAndExclusive()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows("1.0.0", null, "1.1.5");
|
||||
@@ -57,7 +63,8 @@ public sealed class SemanticVersionRangeResolverTests
|
||||
Assert.Equal(SemanticVersionRangeResolver.Parse("1.1.5"), inclusive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ResolveWindows_WithNeither_ReturnsNullBounds()
|
||||
{
|
||||
var (introduced, exclusive, inclusive) = SemanticVersionRangeResolver.ResolveWindows(null, null, null);
|
||||
|
||||
@@ -2,11 +2,13 @@ using System;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class AdvisoryProvenanceTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void FieldMask_NormalizesAndDeduplicates()
|
||||
{
|
||||
var timestamp = DateTimeOffset.Parse("2025-01-01T00:00:00Z");
|
||||
@@ -25,14 +27,16 @@ public sealed class AdvisoryProvenanceTests
|
||||
Assert.Null(provenance.DecisionReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EmptyProvenance_ExposesEmptyFieldMask()
|
||||
{
|
||||
Assert.True(AdvisoryProvenance.Empty.FieldMask.IsEmpty);
|
||||
Assert.Null(AdvisoryProvenance.Empty.DecisionReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void DecisionReason_IsTrimmed()
|
||||
{
|
||||
var timestamp = DateTimeOffset.Parse("2025-03-01T00:00:00Z");
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class AdvisoryTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalizesAliasesAndReferences()
|
||||
{
|
||||
var advisory = new Advisory(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class AffectedPackageStatusTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Known_Affected", AffectedPackageStatusCatalog.KnownAffected)]
|
||||
[InlineData("KNOWN-NOT-AFFECTED", AffectedPackageStatusCatalog.KnownNotAffected)]
|
||||
[InlineData("Under Investigation", AffectedPackageStatusCatalog.UnderInvestigation)]
|
||||
@@ -28,14 +30,16 @@ public sealed class AffectedPackageStatusTests
|
||||
Assert.Equal(provenance, status.Provenance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Constructor_ThrowsForUnknownStatus()
|
||||
{
|
||||
var provenance = new AdvisoryProvenance("test", "status", "value", DateTimeOffset.UtcNow);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new AffectedPackageStatus("unsupported", provenance));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("Not Impacted", AffectedPackageStatusCatalog.NotAffected)]
|
||||
[InlineData("Resolved", AffectedPackageStatusCatalog.Fixed)]
|
||||
[InlineData("Mitigation provided", AffectedPackageStatusCatalog.Mitigated)]
|
||||
@@ -46,13 +50,15 @@ public sealed class AffectedPackageStatusTests
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalize_ReturnsFalseForUnknown()
|
||||
{
|
||||
Assert.False(AffectedPackageStatusCatalog.TryNormalize("unsupported", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Allowed_ReturnsCanonicalStatuses()
|
||||
{
|
||||
var expected = new[]
|
||||
|
||||
@@ -2,11 +2,13 @@ using System;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class AffectedVersionRangeExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_UsesNevraPrimitivesWhenAvailable()
|
||||
{
|
||||
var range = new AffectedVersionRange(
|
||||
@@ -33,7 +35,8 @@ public sealed class AffectedVersionRangeExtensionsTests
|
||||
Assert.Equal("pkg-1.2.0-2.x86_64", rule.Max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_FallsBackForEvrWhenPrimitivesMissing()
|
||||
{
|
||||
var range = new AffectedVersionRange(
|
||||
@@ -55,7 +58,8 @@ public sealed class AffectedVersionRangeExtensionsTests
|
||||
Assert.Equal("fallback", rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_ReturnsNullForUnknownKind()
|
||||
{
|
||||
var range = new AffectedVersionRange(
|
||||
@@ -72,7 +76,8 @@ public sealed class AffectedVersionRangeExtensionsTests
|
||||
Assert.Null(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_FallsBackForApkRange()
|
||||
{
|
||||
var range = new AffectedVersionRange(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class AliasSchemeRegistryTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("cve-2024-1234", AliasSchemes.Cve, "CVE-2024-1234")]
|
||||
[InlineData("GHSA-xxxx-yyyy-zzzz", AliasSchemes.Ghsa, "GHSA-xxxx-yyyy-zzzz")]
|
||||
[InlineData("osv-2023-15", AliasSchemes.OsV, "OSV-2023-15")]
|
||||
@@ -21,7 +23,8 @@ public sealed class AliasSchemeRegistryTests
|
||||
Assert.Equal(expectedAlias, normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalize_ReturnsFalseForUnknownAlias()
|
||||
{
|
||||
var success = AliasSchemeRegistry.TryNormalize("custom-identifier", out var normalized, out var scheme);
|
||||
@@ -31,7 +34,8 @@ public sealed class AliasSchemeRegistryTests
|
||||
Assert.Equal(string.Empty, scheme);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validation_NormalizesAliasWhenRecognized()
|
||||
{
|
||||
var result = Validation.TryNormalizeAlias(" rhsa-2024:0252 ", out var normalized);
|
||||
@@ -41,7 +45,8 @@ public sealed class AliasSchemeRegistryTests
|
||||
Assert.Equal("RHSA-2024:0252", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validation_RejectsEmptyAlias()
|
||||
{
|
||||
var result = Validation.TryNormalizeAlias(" ", out var normalized);
|
||||
|
||||
@@ -8,7 +8,8 @@ namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class CanonicalJsonSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializesPropertiesInDeterministicOrder()
|
||||
{
|
||||
var advisory = new Advisory(
|
||||
@@ -34,7 +35,8 @@ public sealed class CanonicalJsonSerializerTests
|
||||
Assert.Equal(sorted, propertyNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SnapshotSerializerProducesStableOutput()
|
||||
{
|
||||
var advisory = new Advisory(
|
||||
@@ -64,7 +66,8 @@ public sealed class CanonicalJsonSerializerTests
|
||||
Assert.Equal(normalized1, normalized2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SerializesRangePrimitivesPayload()
|
||||
{
|
||||
var recordedAt = new DateTimeOffset(2025, 2, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
@@ -125,6 +128,7 @@ public sealed class CanonicalJsonSerializerTests
|
||||
|
||||
var json = CanonicalJsonSerializer.Serialize(advisory);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
using StellaOps.TestKit;
|
||||
var rangeElement = document.RootElement
|
||||
.GetProperty("affectedPackages")[0]
|
||||
.GetProperty("versionRanges")[0];
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class EvrPrimitiveExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_ProducesRangeForIntroducedAndFixed()
|
||||
{
|
||||
var primitive = new EvrPrimitive(
|
||||
@@ -25,7 +27,8 @@ public sealed class EvrPrimitiveExtensionsTests
|
||||
Assert.Equal("ubuntu:focal", rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_GreaterThanOrEqualWhenOnlyIntroduced()
|
||||
{
|
||||
var primitive = new EvrPrimitive(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class NevraPrimitiveExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_ProducesRangeWhenBoundsAvailable()
|
||||
{
|
||||
var primitive = new NevraPrimitive(
|
||||
@@ -25,7 +27,8 @@ public sealed class NevraPrimitiveExtensionsTests
|
||||
Assert.Equal("rhel-8", rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_UsesLastAffectedAsInclusiveUpperBound()
|
||||
{
|
||||
var primitive = new NevraPrimitive(
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Linq;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class NormalizedVersionRuleTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizedVersions_AreDeduplicatedAndOrdered()
|
||||
{
|
||||
var recordedAt = DateTimeOffset.Parse("2025-01-05T00:00:00Z");
|
||||
@@ -59,7 +61,8 @@ public sealed class NormalizedVersionRuleTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void NormalizedVersionRule_NormalizesTypeSeparators()
|
||||
{
|
||||
var rule = new NormalizedVersionRule("semver", "tie_breaker", value: "1.2.3");
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class OsvGhsaParityDiagnosticsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordReport_EmitsTotalAndIssues()
|
||||
{
|
||||
var issues = ImmutableArray.Create(
|
||||
@@ -45,13 +46,15 @@ public sealed class OsvGhsaParityDiagnosticsTests
|
||||
Assert.Equal("none", severity.Tags["fieldMask"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordReport_NoIssues_OnlyEmitsTotal()
|
||||
{
|
||||
var report = new OsvGhsaParityReport(0, ImmutableArray<OsvGhsaParityIssue>.Empty);
|
||||
var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
|
||||
using var listener = CreateListener(measurements);
|
||||
|
||||
using StellaOps.TestKit;
|
||||
OsvGhsaParityDiagnostics.RecordReport(report, "");
|
||||
|
||||
listener.Dispose();
|
||||
|
||||
@@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class OsvGhsaParityInspectorTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_ReturnsNoIssues_WhenDatasetsMatch()
|
||||
{
|
||||
var ghsaId = "GHSA-1111";
|
||||
@@ -21,7 +23,8 @@ public sealed class OsvGhsaParityInspectorTests
|
||||
Assert.Empty(report.Issues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_FlagsMissingOsvEntry()
|
||||
{
|
||||
var ghsaId = "GHSA-2222";
|
||||
@@ -35,7 +38,8 @@ public sealed class OsvGhsaParityInspectorTests
|
||||
Assert.Contains(ProvenanceFieldMasks.AffectedPackages, issue.FieldMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_FlagsMissingGhsaEntry()
|
||||
{
|
||||
var ghsaId = "GHSA-2424";
|
||||
@@ -49,7 +53,8 @@ public sealed class OsvGhsaParityInspectorTests
|
||||
Assert.Contains(ProvenanceFieldMasks.AffectedPackages, issue.FieldMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_FlagsSeverityMismatch()
|
||||
{
|
||||
var ghsaId = "GHSA-3333";
|
||||
@@ -63,7 +68,8 @@ public sealed class OsvGhsaParityInspectorTests
|
||||
Assert.Contains(ProvenanceFieldMasks.Advisory, issue.FieldMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_FlagsRangeMismatch()
|
||||
{
|
||||
var ghsaId = "GHSA-4444";
|
||||
|
||||
@@ -11,7 +11,8 @@ namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class ProvenanceDiagnosticsTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordMissing_AddsExpectedTagsAndDeduplicates()
|
||||
{
|
||||
ResetState();
|
||||
@@ -44,7 +45,8 @@ public sealed class ProvenanceDiagnosticsTests
|
||||
Assert.Equal(ProvenanceFieldMasks.References, second.Tags["fieldMask"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReportResumeWindow_ClearsTrackedEntries_WhenWindowBackfills()
|
||||
{
|
||||
ResetState();
|
||||
@@ -68,7 +70,8 @@ public sealed class ProvenanceDiagnosticsTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ReportResumeWindow_RetainsEntries_WhenWindowTooRecent()
|
||||
{
|
||||
ResetState();
|
||||
@@ -86,7 +89,8 @@ public sealed class ProvenanceDiagnosticsTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RecordRangePrimitive_EmitsCoverageMetric()
|
||||
{
|
||||
var range = new AffectedVersionRange(
|
||||
@@ -108,6 +112,7 @@ public sealed class ProvenanceDiagnosticsTests
|
||||
var measurements = new List<(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags)>();
|
||||
using var listener = CreateListener(measurements, "concelier.range.primitives");
|
||||
|
||||
using StellaOps.TestKit;
|
||||
ProvenanceDiagnostics.RecordRangePrimitive("source-D", range);
|
||||
|
||||
listener.Dispose();
|
||||
|
||||
@@ -2,11 +2,13 @@ using System.Collections.Generic;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class RangePrimitivesTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetCoverageTag_ReturnsSpecificKinds()
|
||||
{
|
||||
var primitives = new RangePrimitives(
|
||||
@@ -18,7 +20,8 @@ public sealed class RangePrimitivesTests
|
||||
Assert.Equal("nevra+semver", primitives.GetCoverageTag());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetCoverageTag_ReturnsVendorWhenOnlyExtensions()
|
||||
{
|
||||
var primitives = new RangePrimitives(
|
||||
@@ -31,7 +34,8 @@ public sealed class RangePrimitivesTests
|
||||
Assert.Equal("vendor", primitives.GetCoverageTag());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void GetCoverageTag_ReturnsNoneWhenEmpty()
|
||||
{
|
||||
var primitives = new RangePrimitives(null, null, null, null);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class SemVerPrimitiveTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1.0.0", true, "2.0.0", false, null, false, null, null, SemVerPrimitiveStyles.Range)]
|
||||
[InlineData("1.0.0", true, null, false, null, false, null, null, SemVerPrimitiveStyles.GreaterThanOrEqual)]
|
||||
[InlineData("1.0.0", false, null, false, null, false, null, null, SemVerPrimitiveStyles.GreaterThan)]
|
||||
@@ -38,7 +40,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Equal(expectedStyle, primitive.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void EqualityIncludesExactValue()
|
||||
{
|
||||
var baseline = new SemVerPrimitive(
|
||||
@@ -57,7 +60,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Equal(SemVerPrimitiveStyles.Range, baseline.Style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_MapsRangeBounds()
|
||||
{
|
||||
var primitive = new SemVerPrimitive(
|
||||
@@ -82,7 +86,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Equal(">=1.0.0 <2.0.0", rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_ExactUsesExactValue()
|
||||
{
|
||||
var primitive = new SemVerPrimitive(
|
||||
@@ -106,7 +111,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Equal("from-ghsa", rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_GreaterThanMapsMinimum()
|
||||
{
|
||||
var primitive = new SemVerPrimitive(
|
||||
@@ -130,7 +136,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Null(rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_UsesConstraintExpressionAsFallbackNotes()
|
||||
{
|
||||
var primitive = new SemVerPrimitive(
|
||||
@@ -148,7 +155,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Equal("> 1.4.0", rule!.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_ExactCarriesConstraintExpressionWhenNotesMissing()
|
||||
{
|
||||
var primitive = new SemVerPrimitive(
|
||||
@@ -169,7 +177,8 @@ public sealed class SemVerPrimitiveTests
|
||||
Assert.Equal("= 3.2.1", rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_ExplicitNotesOverrideConstraintExpression()
|
||||
{
|
||||
var primitive = new SemVerPrimitive(
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using StellaOps.Concelier.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class SerializationDeterminismTests
|
||||
@@ -18,7 +19,8 @@ public sealed class SerializationDeterminismTests
|
||||
"ar-SA"
|
||||
};
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanonicalSerializer_ProducesStableJsonAcrossCultures()
|
||||
{
|
||||
var examples = CanonicalExampleFactory.GetExamples().ToArray();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Models.Tests;
|
||||
|
||||
public sealed class SeverityNormalizationTests
|
||||
{
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("CRITICAL", "critical")]
|
||||
[InlineData("Important", "high")]
|
||||
[InlineData("moderate", "medium")]
|
||||
@@ -27,7 +29,8 @@ public sealed class SeverityNormalizationTests
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_ReturnsNullWhenInputNullOrWhitespace()
|
||||
{
|
||||
Assert.Null(SeverityNormalization.Normalize(null));
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class ApkVersionParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_RoundTripsExplicitPkgRel()
|
||||
{
|
||||
var parsed = ApkVersion.Parse(" 3.1.4-r0 ");
|
||||
@@ -13,7 +15,8 @@ public sealed class ApkVersionParserTests
|
||||
Assert.Equal("3.1.4-r0", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_SuppressesImplicitPkgRel()
|
||||
{
|
||||
var parsed = ApkVersion.Parse("1.2.3_alpha");
|
||||
@@ -21,7 +24,8 @@ public sealed class ApkVersionParserTests
|
||||
Assert.Equal("1.2.3_alpha", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_TracksExplicitRelease()
|
||||
{
|
||||
var success = ApkVersion.TryParse("2.0.1-r5", out var parsed);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class CpeNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizeCpe_Preserves2Dot3Format()
|
||||
{
|
||||
var input = "cpe:2.3:A:Example:Product:1.0:*:*:*:*:*:*:*";
|
||||
@@ -15,7 +17,8 @@ public sealed class CpeNormalizerTests
|
||||
Assert.Equal("cpe:2.3:a:example:product:1.0:*:*:*:*:*:*:*", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizeCpe_UpgradesUriBinding()
|
||||
{
|
||||
var input = "cpe:/o:RedHat:Enterprise_Linux:8";
|
||||
@@ -26,7 +29,8 @@ public sealed class CpeNormalizerTests
|
||||
Assert.Equal("cpe:2.3:o:redhat:enterprise_linux:8:*:*:*:*:*:*:*", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizeCpe_InvalidInputReturnsFalse()
|
||||
{
|
||||
var success = IdentifierNormalizer.TryNormalizeCpe("not-a-cpe", out var normalized);
|
||||
@@ -35,7 +39,8 @@ public sealed class CpeNormalizerTests
|
||||
Assert.Null(normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizeCpe_DecodesPercentEncodingAndEscapes()
|
||||
{
|
||||
var input = "cpe:/a:Example%20Corp:Widget%2fSuite:1.0:update:%7e:%2a";
|
||||
@@ -46,7 +51,8 @@ public sealed class CpeNormalizerTests
|
||||
Assert.Equal(@"cpe:2.3:a:example\ corp:widget\/suite:1.0:update:*:*:*:*:*:*", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizeCpe_ExpandsEditionFields()
|
||||
{
|
||||
var input = "cpe:/a:Vendor:Product:1.0:update:~pro~~windows~~:en-US";
|
||||
@@ -57,7 +63,8 @@ public sealed class CpeNormalizerTests
|
||||
Assert.Equal("cpe:2.3:a:vendor:product:1.0:update:*:en-us:pro:*:windows:*", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizeCpe_PreservesEscapedCharactersIn23()
|
||||
{
|
||||
var input = @"cpe:2.3:a:example:printer\/:1.2.3:*:*:*:*:*:*:*";
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Normalization.Cvss;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class CvssMetricNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalize_ComputesCvss31Defaults()
|
||||
{
|
||||
var vector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H";
|
||||
@@ -27,7 +29,8 @@ public sealed class CvssMetricNormalizerTests
|
||||
Assert.Equal(provenance, metric.Provenance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalize_NormalizesCvss20Severity()
|
||||
{
|
||||
var vector = "AV:N/AC:M/Au:S/C:P/I:P/A:P";
|
||||
@@ -41,7 +44,8 @@ public sealed class CvssMetricNormalizerTests
|
||||
Assert.Equal("medium", normalized.BaseSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalize_ReturnsFalseWhenVectorMissing()
|
||||
{
|
||||
var success = CvssMetricNormalizer.TryNormalize("3.1", string.Empty, 9.8, "CRITICAL", out var normalized);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class DebianEvrParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_RoundTripsExplicitEpoch()
|
||||
{
|
||||
var parsed = DebianEvr.Parse(" 1:1.2.3-1 ");
|
||||
@@ -13,7 +15,8 @@ public sealed class DebianEvrParserTests
|
||||
Assert.Equal("1:1.2.3-1", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_SuppressesZeroEpochWhenMissing()
|
||||
{
|
||||
var parsed = DebianEvr.Parse("1.2.3-1");
|
||||
@@ -21,7 +24,8 @@ public sealed class DebianEvrParserTests
|
||||
Assert.Equal("1.2.3-1", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_HandlesMissingRevision()
|
||||
{
|
||||
var parsed = DebianEvr.Parse("2:4.5");
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Normalization.Text;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class DescriptionNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_RemovesMarkupAndCollapsesWhitespace()
|
||||
{
|
||||
var candidates = new[]
|
||||
@@ -18,7 +20,8 @@ public sealed class DescriptionNormalizerTests
|
||||
Assert.Equal("en", result.Language);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_FallsBackToPreferredLanguage()
|
||||
{
|
||||
var candidates = new[]
|
||||
@@ -33,7 +36,8 @@ public sealed class DescriptionNormalizerTests
|
||||
Assert.Equal("en", result.Language);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Normalize_ReturnsDefaultWhenEmpty()
|
||||
{
|
||||
var result = DescriptionNormalizer.Normalize(Array.Empty<LocalizedText>());
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class NevraParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_RoundTripsTrimmedInput()
|
||||
{
|
||||
var parsed = Nevra.Parse(" kernel-0:4.18.0-80.el8.x86_64 ");
|
||||
@@ -13,7 +15,8 @@ public sealed class NevraParserTests
|
||||
Assert.Equal("kernel-0:4.18.0-80.el8.x86_64", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_ReconstructsKnownArchitecture()
|
||||
{
|
||||
var parsed = Nevra.Parse("bash-5.2.15-3.el9_4.arm64");
|
||||
@@ -21,7 +24,8 @@ public sealed class NevraParserTests
|
||||
Assert.Equal("bash-5.2.15-3.el9_4.arm64", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ToCanonicalString_HandlesMissingArchitecture()
|
||||
{
|
||||
var parsed = Nevra.Parse("openssl-libs-1:1.1.1k-7.el8");
|
||||
@@ -29,7 +33,8 @@ public sealed class NevraParserTests
|
||||
Assert.Equal("openssl-libs-1:1.1.1k-7.el8", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_ReturnsTrueForExplicitZeroEpoch()
|
||||
{
|
||||
var success = Nevra.TryParse("glibc-0:2.36-8.el9.x86_64", out var nevra);
|
||||
@@ -41,7 +46,8 @@ public sealed class NevraParserTests
|
||||
Assert.Equal("glibc-0:2.36-8.el9.x86_64", nevra.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_IgnoresUnknownArchitectureSuffix()
|
||||
{
|
||||
var success = Nevra.TryParse("package-1.0-1.el9.weirdarch", out var nevra);
|
||||
@@ -53,7 +59,8 @@ public sealed class NevraParserTests
|
||||
Assert.Equal("package-1.0-1.el9.weirdarch", nevra.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryParse_ReturnsFalseForMalformedNevra()
|
||||
{
|
||||
var success = Nevra.TryParse("bad-format", out var nevra);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Normalization.Identifiers;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class PackageUrlNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizePackageUrl_LowersTypeAndNamespace()
|
||||
{
|
||||
var input = "pkg:NPM/Acme/Widget@1.0.0?Arch=X86_64";
|
||||
@@ -20,7 +22,8 @@ public sealed class PackageUrlNormalizerTests
|
||||
Assert.Equal("widget", parsed.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizePackageUrl_OrdersQualifiers()
|
||||
{
|
||||
var input = "pkg:deb/debian/openssl?distro=x%2Fy&arch=amd64";
|
||||
@@ -31,7 +34,8 @@ public sealed class PackageUrlNormalizerTests
|
||||
Assert.Equal("pkg:deb/debian/openssl?arch=amd64&distro=x%2Fy", normalized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void TryNormalizePackageUrl_TrimsWhitespace()
|
||||
{
|
||||
var input = " pkg:pypi/Example/Package ";
|
||||
|
||||
@@ -2,13 +2,15 @@ using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Normalization.SemVer;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class SemVerRangeRuleBuilderTests
|
||||
{
|
||||
private const string Note = "spec:test";
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("< 1.5.0", null, NormalizedVersionRuleTypes.LessThan, null, true, "1.5.0", false, null, false)]
|
||||
[InlineData(">= 1.0.0, < 2.0.0", null, NormalizedVersionRuleTypes.Range, "1.0.0", true, "2.0.0", false, null, false)]
|
||||
[InlineData(">1.2.3, <=1.3.0", null, NormalizedVersionRuleTypes.Range, "1.2.3", false, null, false, "1.3.0", true)]
|
||||
@@ -46,7 +48,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(patched is null && expectedIntroduced is null && expectedFixed is null && expectedLastAffected is null ? null : Note, normalized.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_UsesPatchedVersionWhenUpperBoundMissing()
|
||||
{
|
||||
var results = SemVerRangeRuleBuilder.Build(">= 4.0.0", "4.3.6", Note);
|
||||
@@ -65,7 +68,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(Note, normalized.Notes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("^1.2.3", "1.2.3", "2.0.0")]
|
||||
[InlineData("~1.2.3", "1.2.3", "1.3.0")]
|
||||
[InlineData("~> 1.2", "1.2.0", "1.3.0")]
|
||||
@@ -81,7 +85,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(NormalizedVersionRuleTypes.Range, normalized.Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1.2.x", "1.2.0", "1.3.0")]
|
||||
[InlineData("1.x", "1.0.0", "2.0.0")]
|
||||
public void Build_HandlesWildcardNotation(string range, string expectedMin, string expectedMax)
|
||||
@@ -97,7 +102,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(NormalizedVersionRuleTypes.Range, normalized.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_PreservesPreReleaseAndMetadataInExactRule()
|
||||
{
|
||||
var results = SemVerRangeRuleBuilder.Build("= 2.5.1-alpha.1+build.7", null, Note);
|
||||
@@ -111,7 +117,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(Note, normalized.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_ParsesComparatorWithoutCommaSeparators()
|
||||
{
|
||||
var results = SemVerRangeRuleBuilder.Build(">=1.0.0 <1.2.0", null, Note);
|
||||
@@ -133,7 +140,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(Note, normalized.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Build_HandlesMultipleSegmentsSeparatedByOr()
|
||||
{
|
||||
var results = SemVerRangeRuleBuilder.Build(">=1.0.0 <1.2.0 || >=2.0.0 <2.2.0", null, Note);
|
||||
@@ -159,7 +167,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildNormalizedRules_ProjectsNormalizedRules()
|
||||
{
|
||||
var rules = SemVerRangeRuleBuilder.BuildNormalizedRules(">=1.0.0 <1.2.0", null, Note);
|
||||
@@ -174,7 +183,8 @@ public sealed class SemVerRangeRuleBuilderTests
|
||||
Assert.Equal(Note, rule.Notes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BuildNormalizedRules_ReturnsEmptyWhenNoRules()
|
||||
{
|
||||
var rules = SemVerRangeRuleBuilder.BuildNormalizedRules(" ", null, Note);
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Concelier.Core.Canonical;
|
||||
using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.SbomIntegration.Tests;
|
||||
|
||||
public class SbomAdvisoryMatcherTests
|
||||
@@ -29,7 +30,8 @@ public class SbomAdvisoryMatcherTests
|
||||
|
||||
#region Basic Matching Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_WithVulnerablePurl_ReturnsMatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +57,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result[0].Method.Should().Be(MatchMethod.ExactPurl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_WithMultipleVulnerablePurls_ReturnsAllMatches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -88,7 +91,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result.Should().Contain(m => m.CanonicalId == canonicalId2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_WithSafePurl_ReturnsNoMatches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -106,7 +110,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_PurlAffectedByMultipleAdvisories_ReturnsMultipleMatches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -135,7 +140,8 @@ public class SbomAdvisoryMatcherTests
|
||||
|
||||
#region Reachability Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_WithReachabilityMap_SetsIsReachable()
|
||||
{
|
||||
// Arrange
|
||||
@@ -161,7 +167,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result[0].IsReachable.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_WithDeploymentMap_SetsIsDeployed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -187,7 +194,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result[0].IsDeployed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_PurlNotInReachabilityMap_DefaultsToFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -216,7 +224,8 @@ public class SbomAdvisoryMatcherTests
|
||||
|
||||
#region Ecosystem Coverage Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("pkg:npm/lodash@4.17.20", "npm")]
|
||||
[InlineData("pkg:pypi/requests@2.27.0", "pypi")]
|
||||
[InlineData("pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", "maven")]
|
||||
@@ -244,7 +253,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result[0].Purl.Should().Be(purl);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("pkg:deb/debian/openssl@1.1.1n-0+deb11u3")]
|
||||
[InlineData("pkg:rpm/fedora/kernel@5.19.0-43.fc37")]
|
||||
[InlineData("pkg:apk/alpine/openssl@1.1.1q-r0")]
|
||||
@@ -271,7 +281,8 @@ public class SbomAdvisoryMatcherTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_EmptyPurlList_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +295,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_ServiceThrowsException_LogsAndContinues()
|
||||
{
|
||||
// Arrange
|
||||
@@ -314,7 +326,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result[0].Purl.Should().Be("pkg:npm/succeeding@1.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_LargePurlList_ProcessesEfficiently()
|
||||
{
|
||||
// Arrange
|
||||
@@ -337,7 +350,8 @@ public class SbomAdvisoryMatcherTests
|
||||
sw.ElapsedMilliseconds.Should().BeLessThan(5000); // Reasonable timeout
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MatchAsync_SetsMatchedAtTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
@@ -365,7 +379,8 @@ public class SbomAdvisoryMatcherTests
|
||||
|
||||
#region FindAffectingCanonicalIdsAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindAffectingCanonicalIdsAsync_ReturnsDistinctIds()
|
||||
{
|
||||
// Arrange
|
||||
@@ -389,7 +404,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result.Should().Contain(canonicalId2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindAffectingCanonicalIdsAsync_EmptyPurl_ReturnsEmpty()
|
||||
{
|
||||
// Act
|
||||
@@ -403,7 +419,8 @@ public class SbomAdvisoryMatcherTests
|
||||
|
||||
#region CheckMatchAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CheckMatchAsync_AffectedPurl_ReturnsMatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -425,7 +442,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result.Purl.Should().Be(purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CheckMatchAsync_AdvisoryNotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -442,7 +460,8 @@ public class SbomAdvisoryMatcherTests
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CheckMatchAsync_EmptyPurl_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -28,7 +28,8 @@ public class SbomParserTests
|
||||
|
||||
#region CycloneDX Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_CycloneDX_ExtractsPurls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +76,8 @@ public class SbomParserTests
|
||||
result.Purls.Should().Contain("pkg:npm/express@4.18.2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_CycloneDX_HandlesNestedComponents()
|
||||
{
|
||||
// Arrange
|
||||
@@ -112,7 +114,8 @@ public class SbomParserTests
|
||||
result.Purls.Should().Contain("pkg:npm/child@2.0.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_CycloneDX_SkipsComponentsWithoutPurl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -148,7 +151,8 @@ public class SbomParserTests
|
||||
result.UnresolvedComponents[0].Name.Should().Be("without-purl");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_CycloneDX_DeduplicatesPurls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +182,8 @@ public class SbomParserTests
|
||||
result.Purls.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_CycloneDX17_ExtractsPurls()
|
||||
{
|
||||
// Arrange - CycloneDX 1.7 format
|
||||
@@ -220,7 +225,8 @@ public class SbomParserTests
|
||||
|
||||
#region SPDX Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_SPDX_ExtractsPurls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -269,7 +275,8 @@ public class SbomParserTests
|
||||
result.Purls.Should().Contain("pkg:npm/express@4.18.2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_SPDX_IgnoresNonPurlExternalRefs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -313,7 +320,8 @@ public class SbomParserTests
|
||||
|
||||
#region Format Detection Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1.4")]
|
||||
[InlineData("1.5")]
|
||||
[InlineData("1.6")]
|
||||
@@ -340,7 +348,8 @@ public class SbomParserTests
|
||||
result.SpecVersion.Should().Be(specVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFormatAsync_SPDX2_DetectsFormat()
|
||||
{
|
||||
// Arrange
|
||||
@@ -362,7 +371,8 @@ public class SbomParserTests
|
||||
result.SpecVersion.Should().Be("SPDX-2.3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFormatAsync_UnknownFormat_ReturnsNotDetected()
|
||||
{
|
||||
// Arrange
|
||||
@@ -381,7 +391,8 @@ public class SbomParserTests
|
||||
result.IsDetected.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DetectFormatAsync_InvalidJson_ReturnsNotDetected()
|
||||
{
|
||||
// Arrange
|
||||
@@ -400,7 +411,8 @@ public class SbomParserTests
|
||||
|
||||
#region PURL Ecosystem Tests
|
||||
|
||||
[Theory]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("pkg:npm/lodash@4.17.21")]
|
||||
[InlineData("pkg:pypi/requests@2.28.0")]
|
||||
[InlineData("pkg:maven/org.apache.commons/commons-lang3@3.12.0")]
|
||||
@@ -440,7 +452,8 @@ public class SbomParserTests
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_EmptyComponents_ReturnsEmptyPurls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -462,7 +475,8 @@ public class SbomParserTests
|
||||
result.TotalComponents.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_NullStream_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -470,7 +484,8 @@ public class SbomParserTests
|
||||
_parser.ParseAsync(null!, SbomFormat.CycloneDX));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ParseAsync_ExtractsCpes()
|
||||
{
|
||||
// Arrange
|
||||
@@ -491,6 +506,7 @@ public class SbomParserTests
|
||||
|
||||
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
|
||||
using StellaOps.TestKit;
|
||||
// Act
|
||||
var result = await _parser.ParseAsync(stream, SbomFormat.CycloneDX);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using StellaOps.Messaging;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.SbomIntegration.Tests;
|
||||
|
||||
public class SbomRegistryServiceTests
|
||||
@@ -44,7 +45,8 @@ public class SbomRegistryServiceTests
|
||||
|
||||
#region RegisterSbomAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RegisterSbomAsync_NewSbom_CreatesRegistration()
|
||||
{
|
||||
// Arrange
|
||||
@@ -84,7 +86,8 @@ public class SbomRegistryServiceTests
|
||||
_repositoryMock.Verify(r => r.SaveAsync(It.IsAny<SbomRegistration>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RegisterSbomAsync_ExistingSbom_ReturnsExisting()
|
||||
{
|
||||
// Arrange
|
||||
@@ -122,7 +125,8 @@ public class SbomRegistryServiceTests
|
||||
_repositoryMock.Verify(r => r.SaveAsync(It.IsAny<SbomRegistration>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RegisterSbomAsync_NullInput_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
@@ -134,7 +138,8 @@ public class SbomRegistryServiceTests
|
||||
|
||||
#region LearnSbomAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbomAsync_MatchesAndUpdatesScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -223,7 +228,8 @@ public class SbomRegistryServiceTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbomAsync_NoMatches_ReturnsEmptyMatches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -258,7 +264,8 @@ public class SbomRegistryServiceTests
|
||||
result.ScoresUpdated.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbomAsync_EmitsEvent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -303,7 +310,8 @@ public class SbomRegistryServiceTests
|
||||
|
||||
#region RematchSbomAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RematchSbomAsync_ExistingSbom_RematcesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
@@ -368,7 +376,8 @@ public class SbomRegistryServiceTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RematchSbomAsync_NonExistentSbom_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -385,7 +394,8 @@ public class SbomRegistryServiceTests
|
||||
|
||||
#region UpdateSbomDeltaAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateSbomDeltaAsync_AddsPurls()
|
||||
{
|
||||
// Arrange
|
||||
@@ -441,7 +451,8 @@ public class SbomRegistryServiceTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateSbomDeltaAsync_NonExistentSbom_ThrowsInvalidOperation()
|
||||
{
|
||||
// Arrange
|
||||
@@ -460,7 +471,8 @@ public class SbomRegistryServiceTests
|
||||
|
||||
#region UnregisterAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UnregisterAsync_ExistingSbom_DeletesRegistrationAndMatches()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -16,6 +16,7 @@ using StellaOps.Concelier.SbomIntegration.Models;
|
||||
using StellaOps.Messaging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.SbomIntegration.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -43,7 +44,8 @@ public class SbomScoreIntegrationTests
|
||||
|
||||
#region SBOM → Score Update Flow Tests (Task 17)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_WithMatches_UpdatesInterestScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -99,7 +101,8 @@ public class SbomScoreIntegrationTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_MultipleMatchesSameCanonical_UpdatesScoreOnce()
|
||||
{
|
||||
// Arrange
|
||||
@@ -156,7 +159,8 @@ public class SbomScoreIntegrationTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_NoMatches_NoScoreUpdates()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +214,8 @@ public class SbomScoreIntegrationTests
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_ScoringServiceFails_ContinuesWithOtherMatches()
|
||||
{
|
||||
// Arrange
|
||||
@@ -289,7 +294,8 @@ public class SbomScoreIntegrationTests
|
||||
|
||||
#region Reachability-Aware Scoring Tests (Task 21)
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_WithReachability_PassesReachabilityToScoring()
|
||||
{
|
||||
// Arrange
|
||||
@@ -348,7 +354,8 @@ public class SbomScoreIntegrationTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_WithDeployment_PassesDeploymentToScoring()
|
||||
{
|
||||
// Arrange
|
||||
@@ -407,7 +414,8 @@ public class SbomScoreIntegrationTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_FullReachabilityChain_PassesBothFlags()
|
||||
{
|
||||
// Arrange
|
||||
@@ -471,7 +479,8 @@ public class SbomScoreIntegrationTests
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task LearnSbom_MixedReachability_CorrectFlagsPerMatch()
|
||||
{
|
||||
// Arrange
|
||||
@@ -545,7 +554,8 @@ public class SbomScoreIntegrationTests
|
||||
|
||||
#region Score Calculation Verification
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestScoreCalculator_WithSbomMatch_AddsSbomFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -572,7 +582,8 @@ public class SbomScoreIntegrationTests
|
||||
result.Score.Should().BeGreaterThan(0.30); // in_sbom weight + no_vex_na
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestScoreCalculator_WithReachableMatch_AddsReachableFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -601,7 +612,8 @@ public class SbomScoreIntegrationTests
|
||||
result.Score.Should().BeGreaterThan(0.55); // in_sbom + reachable + no_vex_na
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestScoreCalculator_WithDeployedMatch_AddsDeployedFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -630,7 +642,8 @@ public class SbomScoreIntegrationTests
|
||||
result.Score.Should().BeGreaterThan(0.50); // in_sbom + deployed + no_vex_na
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void InterestScoreCalculator_FullReachabilityChain_MaximizesScore()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -4,9 +4,11 @@ using FluentAssertions;
|
||||
using StellaOps.Concelier.SourceIntel;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public sealed class ChangelogParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_SingleEntry_ExtractsCveAndMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -28,7 +30,8 @@ public sealed class ChangelogParserTests
|
||||
entry.Confidence.Should().Be(0.80);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_MultipleCvesInOneEntry_ExtractsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -50,7 +53,8 @@ public sealed class ChangelogParserTests
|
||||
result.Entries[0].CveIds.Should().Contain("CVE-2024-3333");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_MultipleEntries_ExtractsOnlyThoseWithCves()
|
||||
{
|
||||
// Arrange
|
||||
@@ -75,7 +79,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries[0].CveIds.Should().ContainSingle("CVE-2024-9999");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_NoCves_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -92,7 +97,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseRpmChangelog_SingleEntry_ExtractsCve()
|
||||
{
|
||||
// Arrange
|
||||
@@ -111,7 +117,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
entry.Confidence.Should().Be(0.80);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseRpmChangelog_MultipleCves_ExtractsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -127,7 +134,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries[0].CveIds.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseAlpineSecfixes_SingleVersion_ExtractsCves()
|
||||
{
|
||||
// Arrange
|
||||
@@ -149,7 +157,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
entry.Confidence.Should().Be(0.85); // Alpine has higher confidence
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseAlpineSecfixes_MultipleVersions_ExtractsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -169,7 +178,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries.Should().Contain(e => e.Version == "1.5.0-r1" && e.CveIds.Count == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseAlpineSecfixes_NoSecfixes_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,7 +192,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_ParsedAtTimestamp_IsRecorded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -202,7 +213,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.ParsedAt.Should().BeBefore(after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_DuplicateCves_AreNotDuplicated()
|
||||
{
|
||||
// Arrange
|
||||
@@ -222,7 +234,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries[0].CveIds.Should().ContainSingle("CVE-2024-1234");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseRpmChangelog_MultipleEntries_ExtractsOnlyWithCves()
|
||||
{
|
||||
// Arrange
|
||||
@@ -240,7 +253,8 @@ pkg (1.0.0-1) unstable; urgency=low
|
||||
result.Entries[0].Version.Should().Be("1.2.0-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParseDebianChangelog_DescriptionContainsCveReference_IsCaptured()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -4,9 +4,11 @@ using FluentAssertions;
|
||||
using StellaOps.Concelier.SourceIntel;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public sealed class PatchHeaderParserTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_Dep3FormatWithCve_ExtractsCveAndMetadata()
|
||||
{
|
||||
// Arrange
|
||||
@@ -33,7 +35,8 @@ Bug-Debian: https://bugs.debian.org/67890
|
||||
result.Confidence.Should().BeGreaterThan(0.80);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_MultipleCves_ExtractsAll()
|
||||
{
|
||||
// Arrange
|
||||
@@ -54,7 +57,8 @@ Origin: upstream
|
||||
result.CveIds.Should().Contain("CVE-2024-2222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_CveInFilename_ExtractsFromFilename()
|
||||
{
|
||||
// Arrange
|
||||
@@ -73,7 +77,8 @@ Origin: upstream
|
||||
result.CveIds.Should().ContainSingle("CVE-2024-9999");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_CveInBothHeaderAndFilename_ExtractsBoth()
|
||||
{
|
||||
// Arrange
|
||||
@@ -94,7 +99,8 @@ Origin: upstream
|
||||
result.CveIds.Should().Contain("CVE-2024-2222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_BugReferences_ExtractsFromMultipleSources()
|
||||
{
|
||||
// Arrange
|
||||
@@ -116,7 +122,8 @@ Bug-Ubuntu: https://launchpad.net/456
|
||||
result.BugReferences.Should().Contain(b => b.Contains("launchpad.net"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_ConfidenceCalculation_IncreasesWithMoreEvidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -139,7 +146,8 @@ Bug: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234
|
||||
resultDetailed.Confidence.Should().BeGreaterThan(resultMinimal.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_MultipleCvesInHeader_IncreasesConfidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -159,7 +167,8 @@ Origin: upstream
|
||||
resultMultiple.Confidence.Should().BeGreaterThan(resultSingle.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_UpstreamOrigin_IncreasesConfidence()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +187,8 @@ Origin: upstream
|
||||
resultUpstream.Confidence.Should().BeGreaterThan(resultNoOrigin.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_StopsAtDiffContent_DoesNotParseBody()
|
||||
{
|
||||
// Arrange
|
||||
@@ -199,7 +209,8 @@ Origin: upstream
|
||||
result.CveIds.Should().NotContain("CVE-9999-9999");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_NoCves_ReturnsEmptyCveList()
|
||||
{
|
||||
// Arrange
|
||||
@@ -217,7 +228,8 @@ Origin: vendor
|
||||
result.Confidence.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_ConfidenceCappedAt95Percent()
|
||||
{
|
||||
// Arrange
|
||||
@@ -238,7 +250,8 @@ Bug-Ubuntu: https://ubuntu.com/3
|
||||
result.Confidence.Should().BeLessOrEqualTo(0.95);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchDirectory_MultiplePatches_FiltersOnlyWithCves()
|
||||
{
|
||||
// Arrange - This would need filesystem setup, skipping actual implementation
|
||||
@@ -246,7 +259,8 @@ Bug-Ubuntu: https://ubuntu.com/3
|
||||
// Given a directory with patches, only those containing CVE references should be returned
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_ParsedAtTimestamp_IsRecorded()
|
||||
{
|
||||
// Arrange
|
||||
@@ -263,7 +277,8 @@ Bug-Ubuntu: https://ubuntu.com/3
|
||||
result.ParsedAt.Should().BeBefore(after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void ParsePatchFile_DuplicateCves_AreNotDuplicated()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -12,6 +12,7 @@ using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -41,7 +42,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetByIdAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ShouldReturnEntity_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -59,7 +61,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.MergeHash.Should().Be(canonical.MergeHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ShouldReturnNull_WhenNotExists()
|
||||
{
|
||||
// Act
|
||||
@@ -73,7 +76,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetByMergeHashAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByMergeHashAsync_ShouldReturnEntity_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -89,7 +93,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.Cve.Should().Be(canonical.Cve);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByMergeHashAsync_ShouldReturnNull_WhenNotExists()
|
||||
{
|
||||
// Act
|
||||
@@ -103,7 +108,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetByCveAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCveAsync_ShouldReturnAllMatchingEntities()
|
||||
{
|
||||
// Arrange
|
||||
@@ -124,7 +130,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
results.Should().AllSatisfy(r => r.Cve.Should().Be(cve));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCveAsync_ShouldReturnEmptyList_WhenNoMatches()
|
||||
{
|
||||
// Act
|
||||
@@ -138,7 +145,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetByAffectsKeyAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByAffectsKeyAsync_ShouldReturnAllMatchingEntities()
|
||||
{
|
||||
// Arrange
|
||||
@@ -163,7 +171,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region UpsertAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldInsertNewEntity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -184,7 +193,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
retrieved.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldUpdateExistingByMergeHash()
|
||||
{
|
||||
// Arrange
|
||||
@@ -221,7 +231,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.UpdatedAt.Should().BeAfter(result.CreatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldPreserveExistingValues_WhenNewValuesAreNull()
|
||||
{
|
||||
// Arrange
|
||||
@@ -256,7 +267,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.Summary.Should().Be("Original Summary");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldStoreWeaknessArray()
|
||||
{
|
||||
// Arrange
|
||||
@@ -271,7 +283,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result!.Weakness.Should().BeEquivalentTo(["CWE-79", "CWE-89", "CWE-120"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldStoreVersionRangeAsJson()
|
||||
{
|
||||
// Arrange
|
||||
@@ -292,7 +305,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region UpdateStatusAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateStatusAsync_ShouldUpdateStatus()
|
||||
{
|
||||
// Arrange
|
||||
@@ -308,7 +322,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result!.Status.Should().Be("withdrawn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateStatusAsync_ShouldUpdateTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
@@ -332,7 +347,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region DeleteAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ShouldRemoveEntity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -351,7 +367,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ShouldCascadeDeleteSourceEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -382,7 +399,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region CountAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountAsync_ShouldReturnActiveCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -404,7 +422,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region StreamActiveAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task StreamActiveAsync_ShouldStreamOnlyActiveEntities()
|
||||
{
|
||||
// Arrange
|
||||
@@ -430,7 +449,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region Source Edge Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSourceEdgesAsync_ShouldReturnEdgesForCanonical()
|
||||
{
|
||||
// Arrange
|
||||
@@ -453,7 +473,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
edges.Should().BeInAscendingOrder(e => e.PrecedenceRank);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddSourceEdgeAsync_ShouldInsertNewEdge()
|
||||
{
|
||||
// Arrange
|
||||
@@ -477,7 +498,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.SourceId.Should().Be(source.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddSourceEdgeAsync_ShouldUpsertOnConflict()
|
||||
{
|
||||
// Arrange
|
||||
@@ -506,7 +528,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result!.PrecedenceRank.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AddSourceEdgeAsync_ShouldStoreDsseEnvelope()
|
||||
{
|
||||
// Arrange
|
||||
@@ -529,7 +552,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.DsseEnvelope.Should().Contain("signatures");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetSourceEdgesByAdvisoryIdAsync_ShouldReturnMatchingEdges()
|
||||
{
|
||||
// Arrange
|
||||
@@ -555,7 +579,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region Statistics Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStatisticsAsync_ShouldReturnCorrectCounts()
|
||||
{
|
||||
// Arrange
|
||||
@@ -584,7 +609,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region Unique Constraint Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithDuplicateMergeHash_ShouldUpdateNotInsert()
|
||||
{
|
||||
// Arrange
|
||||
@@ -609,7 +635,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithEmptyWeaknessArray_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -624,7 +651,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result!.Weakness.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithNullOptionalFields_ShouldSucceed()
|
||||
{
|
||||
// Arrange
|
||||
@@ -652,7 +680,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result.EpssScore.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithEpssScore_ShouldStoreCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
@@ -667,7 +696,8 @@ public sealed class AdvisoryCanonicalRepositoryTests : IAsyncLifetime
|
||||
result!.EpssScore.Should().Be(0.9754m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithExploitKnown_ShouldOrWithExisting()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,7 +37,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldInsertNewAdvisory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -55,7 +57,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
result.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_ShouldUpdateExistingAdvisory()
|
||||
{
|
||||
// Arrange
|
||||
@@ -87,7 +90,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
result.UpdatedAt.Should().BeAfter(result.CreatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ShouldReturnAdvisory_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -103,7 +107,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
result.AdvisoryKey.Should().Be(advisory.AdvisoryKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ShouldReturnNull_WhenNotExists()
|
||||
{
|
||||
// Act
|
||||
@@ -113,7 +118,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByKeyAsync_ShouldReturnAdvisory_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -128,7 +134,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
result!.AdvisoryKey.Should().Be(advisory.AdvisoryKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByVulnIdAsync_ShouldReturnAdvisory_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -143,7 +150,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
result!.PrimaryVulnId.Should().Be(advisory.PrimaryVulnId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithAliases_ShouldStoreAliases()
|
||||
{
|
||||
// Arrange
|
||||
@@ -178,7 +186,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
storedAliases.Should().Contain(a => a.AliasType == "ghsa" && !a.IsPrimary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByAliasAsync_ShouldReturnAdvisoriesWithMatchingAlias()
|
||||
{
|
||||
// Arrange
|
||||
@@ -206,7 +215,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
results[0].Id.Should().Be(advisory.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithAffected_ShouldStoreAffectedPackages()
|
||||
{
|
||||
// Arrange
|
||||
@@ -238,7 +248,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
storedAffected[0].Purl.Should().Be(purl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAffectingPackageAsync_ShouldReturnAdvisoriesAffectingPurl()
|
||||
{
|
||||
// Arrange
|
||||
@@ -266,7 +277,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
results[0].Id.Should().Be(advisory.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAffectingPackageNameAsync_ShouldReturnAdvisoriesByEcosystemAndName()
|
||||
{
|
||||
// Arrange
|
||||
@@ -294,7 +306,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
results[0].Id.Should().Be(advisory.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBySeverityAsync_ShouldReturnAdvisoriesWithMatchingSeverity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -312,7 +325,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
criticalResults.Should().NotContain(a => a.Id == lowAdvisory.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetModifiedSinceAsync_ShouldReturnRecentlyModifiedAdvisories()
|
||||
{
|
||||
// Arrange
|
||||
@@ -328,7 +342,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
results.Should().Contain(a => a.Id == advisory.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountAsync_ShouldReturnTotalAdvisoryCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -344,7 +359,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
newCount.Should().Be(initialCount + 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountBySeverityAsync_ShouldReturnCountsGroupedBySeverity()
|
||||
{
|
||||
// Arrange
|
||||
@@ -364,7 +380,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
counts["MEDIUM"].Should().BeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_WithCvss_ShouldStoreCvssScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -394,7 +411,8 @@ public sealed class AdvisoryRepositoryTests : IAsyncLifetime
|
||||
storedCvss[0].BaseSeverity.Should().Be("CRITICAL");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeterministicOrdering_GetModifiedSinceAsync_ShouldReturnConsistentOrder()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -13,6 +13,7 @@ using StellaOps.Concelier.Interest.Models;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -40,7 +41,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetByCanonicalIdAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCanonicalIdAsync_ShouldReturnScore_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -58,7 +60,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.ComputedAt.Should().BeCloseTo(score.ComputedAt, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCanonicalIdAsync_ShouldReturnNull_WhenNotExists()
|
||||
{
|
||||
// Act
|
||||
@@ -72,7 +75,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetByCanonicalIdsAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCanonicalIdsAsync_ShouldReturnMatchingScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -93,7 +97,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Keys.Should().NotContain(score2.CanonicalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCanonicalIdsAsync_ShouldReturnEmptyDictionary_WhenNoMatches()
|
||||
{
|
||||
// Act
|
||||
@@ -103,7 +108,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCanonicalIdsAsync_ShouldReturnEmptyDictionary_WhenEmptyInput()
|
||||
{
|
||||
// Act
|
||||
@@ -117,7 +123,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region SaveAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldInsertNewScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -133,7 +140,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Reasons.Should().BeEquivalentTo(["in_sbom", "reachable", "deployed"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldUpdateExistingScore_OnConflict()
|
||||
{
|
||||
// Arrange
|
||||
@@ -156,7 +164,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Reasons.Should().BeEquivalentTo(["in_sbom", "reachable", "deployed", "no_vex_na"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldStoreLastSeenInBuild()
|
||||
{
|
||||
// Arrange
|
||||
@@ -172,7 +181,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result!.LastSeenInBuild.Should().Be(buildId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldHandleNullLastSeenInBuild()
|
||||
{
|
||||
// Arrange
|
||||
@@ -187,7 +197,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result!.LastSeenInBuild.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldStoreEmptyReasons()
|
||||
{
|
||||
// Arrange
|
||||
@@ -206,7 +217,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region SaveManyAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveManyAsync_ShouldInsertMultipleScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -225,7 +237,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveManyAsync_ShouldUpsertOnConflict()
|
||||
{
|
||||
// Arrange
|
||||
@@ -250,7 +263,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result!.Score.Should().Be(0.8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveManyAsync_ShouldHandleEmptyInput()
|
||||
{
|
||||
// Act - should not throw
|
||||
@@ -265,7 +279,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region DeleteAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ShouldRemoveScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -284,7 +299,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DeleteAsync_ShouldNotThrow_WhenNotExists()
|
||||
{
|
||||
// Act - should not throw
|
||||
@@ -297,7 +313,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetLowScoreCanonicalIdsAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLowScoreCanonicalIdsAsync_ShouldReturnIdsBelowThreshold()
|
||||
{
|
||||
// Arrange
|
||||
@@ -323,7 +340,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Should().NotContain(highScore.CanonicalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLowScoreCanonicalIdsAsync_ShouldRespectMinAge()
|
||||
{
|
||||
// Arrange - one old, one recent
|
||||
@@ -344,7 +362,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Should().Contain(oldScore.CanonicalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetLowScoreCanonicalIdsAsync_ShouldRespectLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -368,7 +387,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetHighScoreCanonicalIdsAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetHighScoreCanonicalIdsAsync_ShouldReturnIdsAboveThreshold()
|
||||
{
|
||||
// Arrange
|
||||
@@ -392,7 +412,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Should().NotContain(lowScore.CanonicalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetHighScoreCanonicalIdsAsync_ShouldRespectLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -414,7 +435,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetTopScoresAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetTopScoresAsync_ShouldReturnTopScoresDescending()
|
||||
{
|
||||
// Arrange
|
||||
@@ -436,7 +458,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result[2].Score.Should().Be(0.2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetTopScoresAsync_ShouldRespectLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -456,7 +479,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetAllAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetAllAsync_ShouldReturnPaginatedResults()
|
||||
{
|
||||
// Arrange
|
||||
@@ -483,7 +507,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetStaleCanonicalIdsAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStaleCanonicalIdsAsync_ShouldReturnIdsOlderThanCutoff()
|
||||
{
|
||||
// Arrange
|
||||
@@ -503,7 +528,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result.Should().Contain(stale.CanonicalId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetStaleCanonicalIdsAsync_ShouldRespectLimit()
|
||||
{
|
||||
// Arrange
|
||||
@@ -526,7 +552,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region CountAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountAsync_ShouldReturnTotalCount()
|
||||
{
|
||||
// Arrange
|
||||
@@ -541,7 +568,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
count.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CountAsync_ShouldReturnZero_WhenEmpty()
|
||||
{
|
||||
// Act
|
||||
@@ -555,7 +583,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region GetDistributionAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetDistributionAsync_ShouldReturnCorrectDistribution()
|
||||
{
|
||||
// Arrange - create scores in different tiers
|
||||
@@ -583,7 +612,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
distribution.MedianScore.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetDistributionAsync_ShouldReturnEmptyDistribution_WhenNoScores()
|
||||
{
|
||||
// Act
|
||||
@@ -599,7 +629,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
distribution.MedianScore.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetScoreDistributionAsync_ShouldBeAliasForGetDistributionAsync()
|
||||
{
|
||||
// Arrange
|
||||
@@ -620,7 +651,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldHandleMaxScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -634,7 +666,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result!.Score.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldHandleMinScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -648,7 +681,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result!.Score.Should().Be(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task SaveAsync_ShouldHandleManyReasons()
|
||||
{
|
||||
// Arrange
|
||||
@@ -663,7 +697,8 @@ public sealed class InterestScoreRepositoryTests : IAsyncLifetime
|
||||
result!.Reasons.Should().BeEquivalentTo(reasons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetTopScoresAsync_ShouldOrderByScoreThenComputedAt()
|
||||
{
|
||||
// Arrange - same score, different computed_at
|
||||
|
||||
@@ -16,6 +16,7 @@ using StellaOps.Concelier.Interest.Models;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -85,7 +86,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region ComputeScoreAsync Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_WithNoSignals_ReturnsBaseScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -100,7 +102,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
score.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_WithSbomMatch_IncludesInSbomFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -121,7 +124,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
score.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_WithReachableAndDeployed_IncludesAllFactors()
|
||||
{
|
||||
// Arrange
|
||||
@@ -144,7 +148,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
score.Reasons.Should().Contain("no_vex_na");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ComputeScoreAsync_WithVexNotAffected_ExcludesNoVexFactor()
|
||||
{
|
||||
// Arrange
|
||||
@@ -176,7 +181,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region UpdateScoreAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_PersistsToPostgres()
|
||||
{
|
||||
// Arrange
|
||||
@@ -198,7 +204,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
retrieved.Reasons.Should().BeEquivalentTo(["in_sbom", "reachable", "deployed"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_UpdatesCacheWhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
@@ -222,7 +229,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_UpsertsBehavior()
|
||||
{
|
||||
// Arrange
|
||||
@@ -257,7 +265,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region GetScoreAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetScoreAsync_ReturnsPersistedScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -278,7 +287,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
result!.Score.Should().Be(0.65);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetScoreAsync_ReturnsNullForNonExistent()
|
||||
{
|
||||
// Act
|
||||
@@ -292,7 +302,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region BatchUpdateAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BatchUpdateAsync_ComputesAndPersistsMultipleScores()
|
||||
{
|
||||
// Arrange
|
||||
@@ -320,7 +331,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
score3!.Score.Should().Be(0.15); // only no_vex_na
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task BatchUpdateAsync_UpdatesCacheForEachScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -346,7 +358,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region GetTopScoresAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetTopScoresAsync_ReturnsScoresInDescendingOrder()
|
||||
{
|
||||
// Arrange
|
||||
@@ -378,7 +391,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region GetDistributionAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetDistributionAsync_ReturnsCorrectDistribution()
|
||||
{
|
||||
// Arrange
|
||||
@@ -407,7 +421,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region DegradeToStubsAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DegradeToStubsAsync_DelegatesToAdvisoryStore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -437,7 +452,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DegradeToStubsAsync_RespectsMinAge()
|
||||
{
|
||||
// Arrange - one old, one recent
|
||||
@@ -468,7 +484,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region RestoreFromStubsAsync Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RestoreFromStubsAsync_RestoresHighScoreStubs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -493,7 +510,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task RestoreFromStubsAsync_SkipsNonStubs()
|
||||
{
|
||||
// Arrange
|
||||
@@ -519,7 +537,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Full Flow Integration Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullFlow_RecordSignals_ComputeScore_PersistAndCache()
|
||||
{
|
||||
// Arrange
|
||||
@@ -559,7 +578,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
retrievedScore!.Score.Should().Be(0.90);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FullFlow_VexStatementReducesScore()
|
||||
{
|
||||
// Arrange
|
||||
@@ -599,7 +619,8 @@ public sealed class InterestScoringServiceIntegrationTests : IAsyncLifetime
|
||||
|
||||
#region Cache Disabled Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpdateScoreAsync_SkipsCacheWhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.Concelier.Storage.Postgres.Models;
|
||||
using StellaOps.Concelier.Storage.Postgres.Repositories;
|
||||
using Xunit;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.Concelier.Storage.Postgres.Tests;
|
||||
|
||||
/// <summary>
|
||||
@@ -31,7 +32,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplaceAsync_ShouldInsertKevFlags()
|
||||
{
|
||||
// Arrange
|
||||
@@ -64,7 +66,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
results[0].VendorProject.Should().Be("Microsoft");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByCveAsync_ShouldReturnKevFlags_WhenExists()
|
||||
{
|
||||
// Arrange
|
||||
@@ -89,7 +92,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
results[0].CveId.Should().Be(advisory.PrimaryVulnId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByAdvisoryAsync_ShouldReturnKevFlags()
|
||||
{
|
||||
// Arrange
|
||||
@@ -115,7 +119,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
results[0].VendorProject.Should().Be("Apache");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplaceAsync_ShouldReplaceExistingFlags()
|
||||
{
|
||||
// Arrange
|
||||
@@ -155,7 +160,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
results[0].VendorProject.Should().Be("Replaced");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplaceAsync_WithEmptyCollection_ShouldRemoveAllFlags()
|
||||
{
|
||||
// Arrange
|
||||
@@ -180,7 +186,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ReplaceAsync_ShouldHandleMultipleFlags()
|
||||
{
|
||||
// Arrange
|
||||
@@ -215,7 +222,8 @@ public sealed class KevFlagRepositoryTests : IAsyncLifetime
|
||||
results.Should().Contain(k => k.VendorProject == "Vendor2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByAdvisoryAsync_ShouldReturnFlagsOrderedByDateAddedDescending()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user