more features checks. setup improvements
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!));
|
||||
}
|
||||
}
|
||||
@@ -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!));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user