more features checks. setup improvements

This commit is contained in:
master
2026-02-13 02:04:55 +02:00
parent 9911b7d73c
commit 9ca2de05df
675 changed files with 37550 additions and 1826 deletions

View File

@@ -0,0 +1,282 @@
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Concelier.Merge.Identity;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Advisories;
namespace StellaOps.Concelier.Merge.Tests.Identity;
public sealed class MergeHashShadowWriteServiceTests
{
private readonly InMemoryAdvisoryStore _advisoryStore = new();
private readonly Mock<IMergeHashCalculator> _calculator = new();
private readonly MergeHashShadowWriteService _service;
public MergeHashShadowWriteServiceTests()
{
_service = new MergeHashShadowWriteService(
_advisoryStore,
_calculator.Object,
NullLogger<MergeHashShadowWriteService>.Instance);
}
private static Advisory CreateAdvisory(string key, string? mergeHash = null)
=> new(
advisoryKey: key,
title: $"Advisory {key}",
summary: null,
language: null,
published: null,
modified: null,
severity: null,
exploitKnown: false,
aliases: null,
references: null,
affectedPackages: null,
cvssMetrics: null,
provenance: null,
mergeHash: mergeHash);
// --- BackfillAllAsync ---
[Fact]
public async Task BackfillAllAsync_NoAdvisories_ReturnsZeroCounts()
{
var result = await _service.BackfillAllAsync(CancellationToken.None);
Assert.Equal(0, result.Processed);
Assert.Equal(0, result.Updated);
Assert.Equal(0, result.Skipped);
Assert.Equal(0, result.Failed);
}
[Fact]
public async Task BackfillAllAsync_AdvisoryWithoutHash_ComputesAndPersists()
{
var advisory = CreateAdvisory("CVE-2024-0001");
await _advisoryStore.UpsertAsync(advisory, CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:abc123");
var result = await _service.BackfillAllAsync(CancellationToken.None);
Assert.Equal(1, result.Processed);
Assert.Equal(1, result.Updated);
Assert.Equal(0, result.Skipped);
Assert.Equal(0, result.Failed);
var stored = await _advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.NotNull(stored);
Assert.Equal("sha256:abc123", stored.MergeHash);
}
[Fact]
public async Task BackfillAllAsync_AdvisoryAlreadyHasHash_SkipsIt()
{
var advisory = CreateAdvisory("CVE-2024-0002", mergeHash: "sha256:existing");
await _advisoryStore.UpsertAsync(advisory, CancellationToken.None);
var result = await _service.BackfillAllAsync(CancellationToken.None);
Assert.Equal(1, result.Processed);
Assert.Equal(0, result.Updated);
Assert.Equal(1, result.Skipped);
Assert.Equal(0, result.Failed);
_calculator.Verify(c => c.ComputeMergeHash(It.IsAny<Advisory>()), Times.Never);
}
[Fact]
public async Task BackfillAllAsync_MixedAdvisories_UpdatesOnlyMissing()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0002", "sha256:existing"), CancellationToken.None);
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0003"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:computed");
var result = await _service.BackfillAllAsync(CancellationToken.None);
Assert.Equal(3, result.Processed);
Assert.Equal(2, result.Updated);
Assert.Equal(1, result.Skipped);
Assert.Equal(0, result.Failed);
}
[Fact]
public async Task BackfillAllAsync_CalculatorThrows_CountsAsFailedAndContinues()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0002"), CancellationToken.None);
var callCount = 0;
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns(() =>
{
callCount++;
if (callCount == 1) throw new InvalidOperationException("bad input");
return "sha256:ok";
});
var result = await _service.BackfillAllAsync(CancellationToken.None);
Assert.Equal(2, result.Processed);
Assert.Equal(1, result.Updated);
Assert.Equal(0, result.Skipped);
Assert.Equal(1, result.Failed);
}
[Fact]
public async Task BackfillAllAsync_Cancellation_ThrowsOperationCanceled()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => _service.BackfillAllAsync(cts.Token));
}
// --- BackfillOneAsync ---
[Fact]
public async Task BackfillOneAsync_AdvisoryNotFound_ReturnsFalse()
{
var result = await _service.BackfillOneAsync("CVE-2024-MISSING", false, CancellationToken.None);
Assert.False(result);
}
[Fact]
public async Task BackfillOneAsync_AdvisoryWithoutHash_ComputesAndPersists()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:newHash");
var result = await _service.BackfillOneAsync("CVE-2024-0001", false, CancellationToken.None);
Assert.True(result);
var stored = await _advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.Equal("sha256:newHash", stored!.MergeHash);
}
[Fact]
public async Task BackfillOneAsync_AdvisoryAlreadyHasHash_NoForce_ReturnsFalse()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001", "sha256:old"), CancellationToken.None);
var result = await _service.BackfillOneAsync("CVE-2024-0001", false, CancellationToken.None);
Assert.False(result);
_calculator.Verify(c => c.ComputeMergeHash(It.IsAny<Advisory>()), Times.Never);
}
[Fact]
public async Task BackfillOneAsync_AdvisoryAlreadyHasHash_ForceTrue_Recomputes()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001", "sha256:old"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:recomputed");
var result = await _service.BackfillOneAsync("CVE-2024-0001", true, CancellationToken.None);
Assert.True(result);
var stored = await _advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.Equal("sha256:recomputed", stored!.MergeHash);
}
[Fact]
public async Task BackfillOneAsync_CalculatorThrows_PropagatesException()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Throws(new InvalidOperationException("bad"));
await Assert.ThrowsAsync<InvalidOperationException>(
() => _service.BackfillOneAsync("CVE-2024-0001", false, CancellationToken.None));
}
[Fact]
public async Task BackfillOneAsync_NullOrWhitespaceKey_ThrowsArgumentException()
{
await Assert.ThrowsAsync<ArgumentException>(
() => _service.BackfillOneAsync("", false, CancellationToken.None));
await Assert.ThrowsAsync<ArgumentException>(
() => _service.BackfillOneAsync(" ", false, CancellationToken.None));
}
// --- Constructor validation ---
[Fact]
public void Constructor_NullAdvisoryStore_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashShadowWriteService(null!, _calculator.Object, NullLogger<MergeHashShadowWriteService>.Instance));
}
[Fact]
public void Constructor_NullCalculator_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashShadowWriteService(_advisoryStore, null!, NullLogger<MergeHashShadowWriteService>.Instance));
}
[Fact]
public void Constructor_NullLogger_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashShadowWriteService(_advisoryStore, _calculator.Object, null!));
}
// --- ShadowWriteResult ---
[Fact]
public void ShadowWriteResult_RecordProperties_AreCorrect()
{
var result = new ShadowWriteResult(100, 50, 45, 5);
Assert.Equal(100, result.Processed);
Assert.Equal(50, result.Updated);
Assert.Equal(45, result.Skipped);
Assert.Equal(5, result.Failed);
}
// --- Enrichment preserves advisory fields ---
[Fact]
public async Task BackfillOneAsync_PreservesAllAdvisoryFields()
{
var original = new Advisory(
advisoryKey: "CVE-2024-9999",
title: "Test Advisory",
summary: "A summary",
language: "en",
published: new DateTimeOffset(2024, 6, 1, 0, 0, 0, TimeSpan.Zero),
modified: new DateTimeOffset(2024, 7, 1, 0, 0, 0, TimeSpan.Zero),
severity: "high",
exploitKnown: true,
aliases: new[] { "CVE-2024-9999" },
references: null,
affectedPackages: null,
cvssMetrics: null,
provenance: null,
description: "A description",
cwes: null,
canonicalMetricId: "metric-001");
await _advisoryStore.UpsertAsync(original, CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:enriched");
await _service.BackfillOneAsync("CVE-2024-9999", false, CancellationToken.None);
var stored = await _advisoryStore.FindAsync("CVE-2024-9999", CancellationToken.None);
Assert.NotNull(stored);
Assert.Equal("CVE-2024-9999", stored.AdvisoryKey);
Assert.Equal("Test Advisory", stored.Title);
Assert.Equal("A summary", stored.Summary);
Assert.Equal("en", stored.Language);
Assert.True(stored.ExploitKnown);
Assert.Equal("A description", stored.Description);
Assert.Equal("sha256:enriched", stored.MergeHash);
}
}

View File

@@ -0,0 +1,144 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Merge.Identity;
using StellaOps.Concelier.Merge.Jobs;
namespace StellaOps.Concelier.Merge.Tests.Jobs;
public sealed class MergeHashBackfillJobTests
{
private readonly MergeHashBackfillJob _job;
public MergeHashBackfillJobTests()
{
var advisoryStore = new StellaOps.Concelier.Storage.Advisories.InMemoryAdvisoryStore();
var calculator = new Mock<IMergeHashCalculator>();
calculator.Setup(c => c.ComputeMergeHash(It.IsAny<StellaOps.Concelier.Models.Advisory>()))
.Returns("sha256:test");
var shadowWriteService = new MergeHashShadowWriteService(
advisoryStore,
calculator.Object,
NullLogger<MergeHashShadowWriteService>.Instance);
_job = new MergeHashBackfillJob(
shadowWriteService,
NullLogger<MergeHashBackfillJob>.Instance);
}
private static JobExecutionContext CreateContext(Dictionary<string, object?>? parameters = null)
{
var services = new ServiceCollection().BuildServiceProvider();
return new JobExecutionContext(
Guid.NewGuid(),
"merge-hash-backfill",
"manual",
parameters ?? new Dictionary<string, object?>(),
services,
TimeProvider.System,
NullLogger.Instance);
}
[Fact]
public async Task ExecuteAsync_NoSeed_CallsBackfillAll()
{
var context = CreateContext();
// Should not throw - runs BackfillAllAsync on empty store
await _job.ExecuteAsync(context, CancellationToken.None);
}
[Fact]
public async Task ExecuteAsync_WithSeed_CallsBackfillOne()
{
var context = CreateContext(new Dictionary<string, object?>
{
["seed"] = "CVE-2024-0001"
});
// Advisory not found, but should not throw (BackfillOneAsync returns false)
await _job.ExecuteAsync(context, CancellationToken.None);
}
[Fact]
public async Task ExecuteAsync_WithSeedAndForce_ParsesForceParameter()
{
var context = CreateContext(new Dictionary<string, object?>
{
["seed"] = "CVE-2024-0001",
["force"] = "true"
});
await _job.ExecuteAsync(context, CancellationToken.None);
}
[Fact]
public async Task ExecuteAsync_EmptySeed_FallsBackToAll()
{
var context = CreateContext(new Dictionary<string, object?>
{
["seed"] = ""
});
// Empty seed should fall through to BackfillAllAsync
await _job.ExecuteAsync(context, CancellationToken.None);
}
[Fact]
public async Task ExecuteAsync_WhitespaceSeed_FallsBackToAll()
{
var context = CreateContext(new Dictionary<string, object?>
{
["seed"] = " "
});
await _job.ExecuteAsync(context, CancellationToken.None);
}
[Fact]
public async Task ExecuteAsync_ForceNotTrue_DefaultsToFalse()
{
var context = CreateContext(new Dictionary<string, object?>
{
["seed"] = "CVE-2024-0001",
["force"] = "false"
});
await _job.ExecuteAsync(context, CancellationToken.None);
}
[Fact]
public async Task ExecuteAsync_ForceNotString_DefaultsToFalse()
{
var context = CreateContext(new Dictionary<string, object?>
{
["seed"] = "CVE-2024-0001",
["force"] = 42 // not a string
});
await _job.ExecuteAsync(context, CancellationToken.None);
}
// --- Constructor validation ---
[Fact]
public void Constructor_NullShadowWriteService_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashBackfillJob(null!, NullLogger<MergeHashBackfillJob>.Instance));
}
[Fact]
public void Constructor_NullLogger_ThrowsArgumentNull()
{
var store = new StellaOps.Concelier.Storage.Advisories.InMemoryAdvisoryStore();
var calc = new Mock<IMergeHashCalculator>();
var svc = new MergeHashShadowWriteService(store, calc.Object, NullLogger<MergeHashShadowWriteService>.Instance);
Assert.Throws<ArgumentNullException>(() =>
new MergeHashBackfillJob(svc, null!));
}
}

View File

@@ -0,0 +1,245 @@
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Concelier.Merge.Identity;
using StellaOps.Concelier.Merge.Services;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Advisories;
namespace StellaOps.Concelier.Merge.Tests.Services;
public sealed class MergeHashBackfillServiceTests
{
private readonly InMemoryAdvisoryStore _advisoryStore = new();
private readonly Mock<IMergeHashCalculator> _calculator = new();
private readonly MergeHashBackfillService _service;
public MergeHashBackfillServiceTests()
{
_service = new MergeHashBackfillService(
_advisoryStore,
_calculator.Object,
NullLogger<MergeHashBackfillService>.Instance);
}
private static Advisory CreateAdvisory(string key, string? mergeHash = null)
=> new(
advisoryKey: key,
title: $"Advisory {key}",
summary: null,
language: null,
published: null,
modified: null,
severity: null,
exploitKnown: false,
aliases: null,
references: null,
affectedPackages: null,
cvssMetrics: null,
provenance: null,
mergeHash: mergeHash);
// --- BackfillAsync ---
[Fact]
public async Task BackfillAsync_NoAdvisories_ReturnsZeroCounts()
{
var result = await _service.BackfillAsync();
Assert.Equal(0, result.TotalProcessed);
Assert.Equal(0, result.Updated);
Assert.Equal(0, result.Skipped);
Assert.Equal(0, result.Errors);
Assert.False(result.DryRun);
}
[Fact]
public async Task BackfillAsync_AdvisoryWithoutHash_ComputesAndPersists()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:abc123");
var result = await _service.BackfillAsync();
Assert.Equal(1, result.TotalProcessed);
Assert.Equal(1, result.Updated);
Assert.Equal(0, result.Skipped);
Assert.Equal(0, result.Errors);
var stored = await _advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.NotNull(stored);
Assert.Equal("sha256:abc123", stored.MergeHash);
}
[Fact]
public async Task BackfillAsync_AdvisoryAlreadyHasHash_SkipsIt()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0002", "sha256:existing"), CancellationToken.None);
var result = await _service.BackfillAsync();
Assert.Equal(1, result.TotalProcessed);
Assert.Equal(0, result.Updated);
Assert.Equal(1, result.Skipped);
Assert.Equal(0, result.Errors);
_calculator.Verify(c => c.ComputeMergeHash(It.IsAny<Advisory>()), Times.Never);
}
[Fact]
public async Task BackfillAsync_DryRun_ComputesButDoesNotPersist()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:computed");
var result = await _service.BackfillAsync(dryRun: true);
Assert.Equal(1, result.TotalProcessed);
Assert.Equal(1, result.Updated);
Assert.True(result.DryRun);
// Advisory should NOT have been updated in store
var stored = await _advisoryStore.FindAsync("CVE-2024-0001", CancellationToken.None);
Assert.Null(stored!.MergeHash);
}
[Fact]
public async Task BackfillAsync_CalculatorThrows_CountsAsErrorAndContinues()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0002"), CancellationToken.None);
var callCount = 0;
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns(() =>
{
callCount++;
if (callCount == 1) throw new InvalidOperationException("bad");
return "sha256:ok";
});
var result = await _service.BackfillAsync();
Assert.Equal(2, result.TotalProcessed);
Assert.Equal(1, result.Updated);
Assert.Equal(0, result.Skipped);
Assert.Equal(1, result.Errors);
}
[Fact]
public async Task BackfillAsync_MixedAdvisories_CorrectCounts()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0002", "sha256:has"), CancellationToken.None);
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0003"), CancellationToken.None);
_calculator.Setup(c => c.ComputeMergeHash(It.IsAny<Advisory>()))
.Returns("sha256:filled");
var result = await _service.BackfillAsync();
Assert.Equal(3, result.TotalProcessed);
Assert.Equal(2, result.Updated);
Assert.Equal(1, result.Skipped);
Assert.Equal(0, result.Errors);
}
[Fact]
public async Task BackfillAsync_Cancellation_ThrowsOperationCanceled()
{
await _advisoryStore.UpsertAsync(CreateAdvisory("CVE-2024-0001"), CancellationToken.None);
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => _service.BackfillAsync(cancellationToken: cts.Token));
}
[Fact]
public async Task BackfillAsync_RecordsDuration()
{
var result = await _service.BackfillAsync();
Assert.True(result.Duration >= TimeSpan.Zero);
}
// --- ComputeMergeHash (preview) ---
[Fact]
public void ComputeMergeHash_DelegatesToCalculator()
{
var advisory = CreateAdvisory("CVE-2024-0001");
_calculator.Setup(c => c.ComputeMergeHash(advisory))
.Returns("sha256:preview");
var hash = _service.ComputeMergeHash(advisory);
Assert.Equal("sha256:preview", hash);
_calculator.Verify(c => c.ComputeMergeHash(advisory), Times.Once);
}
[Fact]
public void ComputeMergeHash_NullAdvisory_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() => _service.ComputeMergeHash(null!));
}
// --- MergeHashBackfillResult ---
[Fact]
public void BackfillResult_SuccessRate_AllUpdatedOrSkipped()
{
var result = new MergeHashBackfillResult(100, 60, 40, 0, false, TimeSpan.FromSeconds(5));
Assert.Equal(100.0, result.SuccessRate);
}
[Fact]
public void BackfillResult_SuccessRate_WithErrors()
{
var result = new MergeHashBackfillResult(100, 50, 40, 10, false, TimeSpan.FromSeconds(5));
Assert.Equal(90.0, result.SuccessRate);
}
[Fact]
public void BackfillResult_SuccessRate_ZeroProcessed_Returns100()
{
var result = new MergeHashBackfillResult(0, 0, 0, 0, false, TimeSpan.Zero);
Assert.Equal(100.0, result.SuccessRate);
}
[Fact]
public void BackfillResult_AvgTimePerAdvisoryMs_CorrectCalculation()
{
var result = new MergeHashBackfillResult(10, 5, 5, 0, false, TimeSpan.FromMilliseconds(1000));
Assert.Equal(100.0, result.AvgTimePerAdvisoryMs);
}
[Fact]
public void BackfillResult_AvgTimePerAdvisoryMs_ZeroProcessed_ReturnsZero()
{
var result = new MergeHashBackfillResult(0, 0, 0, 0, false, TimeSpan.FromMilliseconds(100));
Assert.Equal(0.0, result.AvgTimePerAdvisoryMs);
}
// --- Constructor validation ---
[Fact]
public void Constructor_NullAdvisoryStore_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashBackfillService(null!, _calculator.Object, NullLogger<MergeHashBackfillService>.Instance));
}
[Fact]
public void Constructor_NullCalculator_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashBackfillService(_advisoryStore, null!, NullLogger<MergeHashBackfillService>.Instance));
}
[Fact]
public void Constructor_NullLogger_ThrowsArgumentNull()
{
Assert.Throws<ArgumentNullException>(() =>
new MergeHashBackfillService(_advisoryStore, _calculator.Object, null!));
}
}