Repair live canonical migrations and scanner cache bootstrap
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StellaOps.Concelier.Persistence.Postgres;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
namespace StellaOps.Concelier.Persistence.Tests;
|
||||
|
||||
public sealed class ConcelierInfrastructureRegistrationTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void AddConcelierPostgresStorage_RegistersStartupMigrationHost()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Postgres:Concelier:ConnectionString"] = "Host=postgres;Database=stellaops;Username=postgres;Password=postgres"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
|
||||
services.AddConcelierPostgresStorage(configuration);
|
||||
|
||||
services
|
||||
.Where(descriptor => descriptor.ServiceType == typeof(IHostedService))
|
||||
.Should()
|
||||
.ContainSingle("fresh installs need Concelier startup migrations to create the vuln schema before canonical advisory queries can execute");
|
||||
}
|
||||
}
|
||||
@@ -11,3 +11,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| TASK-015-011 | DONE | Added SbomRepository integration coverage. |
|
||||
| TASK-015-007d | DONE | Added license query coverage for SbomRepository. |
|
||||
| TASK-015-013 | DONE | Added SbomRepository integration coverage for model cards and policy fields. |
|
||||
| TASK-014-003 | DONE | 2026-03-09: added startup-migration registration coverage so Concelier canonical tables bootstrap on fresh deploys and verified `/api/v1/canonical` live after redeploy. |
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Concelier.Core.Canonical;
|
||||
using StellaOps.Concelier.WebService.Tests.Fixtures;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests.Canonical;
|
||||
|
||||
public sealed class CanonicalProductionRegistrationTests : IAsyncLifetime
|
||||
{
|
||||
private WebApplicationFactory<Program> factory = null!;
|
||||
private HttpClient client = null!;
|
||||
|
||||
private static readonly Guid CanonicalId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
factory = new ConcelierApplicationFactory()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<ICanonicalAdvisoryStore>();
|
||||
services.AddSingleton<ICanonicalAdvisoryStore>(new StubCanonicalAdvisoryStore());
|
||||
});
|
||||
});
|
||||
|
||||
client = factory.CreateClient();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
client.Dispose();
|
||||
factory.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryCanonical_UsesProductionRegistrationWithoutDiFailure()
|
||||
{
|
||||
var response = await client.GetAsync("/api/v1/canonical?cve=CVE-2026-1000");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
content.Should().Contain("CVE-2026-1000");
|
||||
}
|
||||
|
||||
private sealed class StubCanonicalAdvisoryStore : ICanonicalAdvisoryStore
|
||||
{
|
||||
private static readonly CanonicalAdvisory Advisory = new()
|
||||
{
|
||||
Id = CanonicalId,
|
||||
Cve = "CVE-2026-1000",
|
||||
AffectsKey = "pkg:npm/example@1.0.0",
|
||||
MergeHash = "sha256:canonical-production-registration",
|
||||
Status = CanonicalStatus.Active,
|
||||
Severity = "high",
|
||||
CreatedAt = new DateTimeOffset(2026, 3, 9, 0, 0, 0, TimeSpan.Zero),
|
||||
UpdatedAt = new DateTimeOffset(2026, 3, 9, 0, 0, 0, TimeSpan.Zero),
|
||||
SourceEdges =
|
||||
[
|
||||
new SourceEdge
|
||||
{
|
||||
Id = Guid.Parse("33333333-3333-3333-3333-333333333333"),
|
||||
CanonicalId = CanonicalId,
|
||||
SourceName = "nvd",
|
||||
SourceAdvisoryId = "NVD-2026-1000",
|
||||
SourceDocHash = "sha256:edge",
|
||||
PrecedenceRank = 40,
|
||||
FetchedAt = new DateTimeOffset(2026, 3, 9, 0, 0, 0, TimeSpan.Zero),
|
||||
CreatedAt = new DateTimeOffset(2026, 3, 9, 0, 0, 0, TimeSpan.Zero)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
public Task<CanonicalAdvisory?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
||||
=> Task.FromResult(id == CanonicalId ? Advisory : null);
|
||||
|
||||
public Task<CanonicalAdvisory?> GetByMergeHashAsync(string mergeHash, CancellationToken ct = default)
|
||||
=> Task.FromResult(
|
||||
string.Equals(mergeHash, Advisory.MergeHash, StringComparison.Ordinal) ? Advisory : null);
|
||||
|
||||
public Task<IReadOnlyList<CanonicalAdvisory>> GetByCveAsync(string cve, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<CanonicalAdvisory>>(
|
||||
string.Equals(cve, Advisory.Cve, StringComparison.Ordinal) ? [Advisory] : []);
|
||||
|
||||
public Task<IReadOnlyList<CanonicalAdvisory>> GetByArtifactAsync(string artifactKey, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<CanonicalAdvisory>>(
|
||||
string.Equals(artifactKey, Advisory.AffectsKey, StringComparison.Ordinal) ? [Advisory] : []);
|
||||
|
||||
public Task<PagedResult<CanonicalAdvisory>> QueryAsync(CanonicalQueryOptions options, CancellationToken ct = default)
|
||||
=> Task.FromResult(new PagedResult<CanonicalAdvisory>
|
||||
{
|
||||
Items = [Advisory],
|
||||
TotalCount = 1,
|
||||
Offset = options.Offset,
|
||||
Limit = options.Limit
|
||||
});
|
||||
|
||||
public Task<Guid> UpsertCanonicalAsync(UpsertCanonicalRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult(CanonicalId);
|
||||
|
||||
public Task UpdateStatusAsync(Guid id, CanonicalStatus status, CancellationToken ct = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<long> CountAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(1L);
|
||||
|
||||
public Task<SourceEdgeResult> AddSourceEdgeAsync(AddSourceEdgeRequest request, CancellationToken ct = default)
|
||||
=> Task.FromResult(SourceEdgeResult.Created(Guid.Parse("44444444-4444-4444-4444-444444444444")));
|
||||
|
||||
public Task<IReadOnlyList<SourceEdge>> GetSourceEdgesAsync(Guid canonicalId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<SourceEdge>>(Advisory.SourceEdges);
|
||||
|
||||
public Task<bool> SourceEdgeExistsAsync(Guid canonicalId, Guid sourceId, string docHash, CancellationToken ct = default)
|
||||
=> Task.FromResult(false);
|
||||
|
||||
public Task<IReadOnlyList<ProvenanceScopeDto>> GetProvenanceScopesAsync(Guid canonicalId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<ProvenanceScopeDto>>([]);
|
||||
|
||||
public Task<Guid> ResolveSourceIdAsync(string sourceKey, CancellationToken ct = default)
|
||||
=> Task.FromResult(Guid.Parse("55555555-5555-5555-5555-555555555555"));
|
||||
|
||||
public Task<int> GetSourcePrecedenceAsync(string sourceKey, CancellationToken ct = default)
|
||||
=> Task.FromResult(40);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user