fix tests. new product advisories enhancements

This commit is contained in:
master
2026-01-25 19:11:36 +02:00
parent c70e83719e
commit 6e687b523a
504 changed files with 40610 additions and 3785 deletions

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Globalization;
@@ -21,6 +23,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.InMemoryRunner;
@@ -38,6 +41,11 @@ using StellaOps.Concelier.Storage.Advisories;
using StellaOps.Concelier.Storage.Observations;
using StellaOps.Concelier.Storage.Linksets;
using StellaOps.Concelier.Core.Raw;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Concelier.RawModels;
using StellaOps.Concelier.WebService.Jobs;
using StellaOps.Concelier.WebService.Options;
using StellaOps.Concelier.WebService.Contracts;
@@ -51,6 +59,9 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using StellaOps.Concelier.WebService.Diagnostics;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using DsseProvenance = StellaOps.Provenance.DsseProvenance;
using TrustInfo = StellaOps.Provenance.TrustInfo;
using DocumentObject = StellaOps.Concelier.Documents.DocumentObject;
namespace StellaOps.Concelier.WebService.Tests;
@@ -72,6 +83,9 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
public ValueTask InitializeAsync()
{
_runner = InMemoryDbRunner.Start();
// Use an empty connection string - the factory sets a default Postgres connection string
// and the stub services bypass actual database operations
_factory = new ConcelierApplicationFactory(string.Empty);
WarmupFactory(_factory);
return ValueTask.CompletedTask;
@@ -80,6 +94,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
public ValueTask DisposeAsync()
{
_factory.Dispose();
_runner.Dispose();
return ValueTask.CompletedTask;
}
@@ -2033,6 +2048,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLETRACING", "false");
Environment.SetEnvironmentVariable("CONCELIER_TELEMETRY__ENABLEMETRICS", "false");
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
const string EvidenceRootKey = "CONCELIER_EVIDENCE__ROOT";
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
_additionalPreviousEnvironment[EvidenceRootKey] = Environment.GetEnvironmentVariable(EvidenceRootKey);
@@ -2074,6 +2091,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((context, configurationBuilder) =>
{
var settings = new Dictionary<string, string?>
@@ -2091,9 +2110,30 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
builder.ConfigureServices(services =>
{
// Remove ConcelierDataSource to skip Postgres initialization during tests
// This allows tests to run without a real database connection
services.RemoveAll<ConcelierDataSource>();
services.AddSingleton<ILeaseStore, Fixtures.TestLeaseStore>();
services.AddSingleton<StubJobCoordinator>();
services.AddSingleton<IJobCoordinator>(sp => sp.GetRequiredService<StubJobCoordinator>());
// Register stubs for services required by AdvisoryRawService and AdvisoryObservationQueryService
services.RemoveAll<IAdvisoryRawService>();
services.AddSingleton<IAdvisoryRawService, StubAdvisoryRawService>();
services.RemoveAll<IAdvisoryObservationLookup>();
services.AddSingleton<IAdvisoryObservationLookup, StubAdvisoryObservationLookup>();
services.RemoveAll<IAdvisoryObservationQueryService>();
services.AddSingleton<IAdvisoryObservationQueryService, StubAdvisoryObservationQueryService>();
// Register stubs for storage and event log services
services.RemoveAll<IStorageDatabase>();
services.AddSingleton<IStorageDatabase>(new StorageDatabase("test"));
services.RemoveAll<IAdvisoryStore>();
services.AddSingleton<IAdvisoryStore, StubAdvisoryStore>();
services.RemoveAll<IAdvisoryEventLog>();
services.AddSingleton<IAdvisoryEventLog, StubAdvisoryEventLog>();
services.PostConfigure<ConcelierOptions>(options =>
{
options.PostgresStorage ??= new ConcelierOptions.PostgresStorageOptions();
@@ -2309,6 +2349,188 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
}
}
}
private sealed class StubAdvisoryRawService : IAdvisoryRawService
{
public Task<AdvisoryRawUpsertResult> IngestAsync(AdvisoryRawDocument document, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var record = new AdvisoryRawRecord(Guid.NewGuid().ToString("D"), document, DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch);
return Task.FromResult(new AdvisoryRawUpsertResult(true, record));
}
public Task<AdvisoryRawRecord?> FindByIdAsync(string tenant, string id, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<AdvisoryRawRecord?>(null);
}
public Task<AdvisoryRawQueryResult> QueryAsync(AdvisoryRawQueryOptions options, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryRawQueryResult(Array.Empty<AdvisoryRawRecord>(), null, false));
}
public Task<IReadOnlyList<AdvisoryRawRecord>> FindByAdvisoryKeyAsync(
string tenant,
string advisoryKey,
IReadOnlyCollection<string> sourceVendors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult<IReadOnlyList<AdvisoryRawRecord>>(Array.Empty<AdvisoryRawRecord>());
}
public Task<AdvisoryRawVerificationResult> VerifyAsync(AdvisoryRawVerificationRequest request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryRawVerificationResult(
request.Tenant,
request.Since,
request.Until,
0,
Array.Empty<AdvisoryRawVerificationViolation>(),
false));
}
}
private sealed class StubAdvisoryObservationLookup : IAdvisoryObservationLookup
{
public ValueTask<IReadOnlyList<AdvisoryObservation>> ListByTenantAsync(
string tenant,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.FromResult<IReadOnlyList<AdvisoryObservation>>(Array.Empty<AdvisoryObservation>());
}
public ValueTask<IReadOnlyList<AdvisoryObservation>> FindByFiltersAsync(
string tenant,
IReadOnlyCollection<string> observationIds,
IReadOnlyCollection<string> aliases,
IReadOnlyCollection<string> purls,
IReadOnlyCollection<string> cpes,
AdvisoryObservationCursor? cursor,
int limit,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.FromResult<IReadOnlyList<AdvisoryObservation>>(Array.Empty<AdvisoryObservation>());
}
}
private sealed class StubAdvisoryObservationQueryService : IAdvisoryObservationQueryService
{
public ValueTask<AdvisoryObservationQueryResult> QueryAsync(
AdvisoryObservationQueryOptions options,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var emptyLinkset = new AdvisoryObservationLinksetAggregate(
System.Collections.Immutable.ImmutableArray<string>.Empty,
System.Collections.Immutable.ImmutableArray<string>.Empty,
System.Collections.Immutable.ImmutableArray<string>.Empty,
System.Collections.Immutable.ImmutableArray<AdvisoryObservationReference>.Empty);
return ValueTask.FromResult(new AdvisoryObservationQueryResult(
System.Collections.Immutable.ImmutableArray<AdvisoryObservation>.Empty,
emptyLinkset,
null,
false));
}
}
private sealed class StubAdvisoryEventLog : IAdvisoryEventLog
{
private readonly ConcurrentDictionary<string, List<AdvisoryStatementInput>> _statements = new(StringComparer.OrdinalIgnoreCase);
public ValueTask AppendAsync(AdvisoryEventAppendRequest request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var statement in request.Statements)
{
var list = _statements.GetOrAdd(statement.VulnerabilityKey, _ => new List<AdvisoryStatementInput>());
lock (list)
{
list.Add(statement);
}
}
return ValueTask.CompletedTask;
}
public ValueTask<AdvisoryReplay> ReplayAsync(string vulnerabilityKey, DateTimeOffset? asOf, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (_statements.TryGetValue(vulnerabilityKey, out var statements) && statements.Count > 0)
{
var snapshots = statements
.Select(s => new AdvisoryStatementSnapshot(
s.StatementId ?? Guid.NewGuid(),
s.VulnerabilityKey,
s.AdvisoryKey ?? s.Advisory.AdvisoryKey,
s.Advisory,
System.Collections.Immutable.ImmutableArray<byte>.Empty,
s.AsOf,
DateTimeOffset.UtcNow,
System.Collections.Immutable.ImmutableArray<Guid>.Empty))
.ToImmutableArray();
return ValueTask.FromResult(new AdvisoryReplay(
vulnerabilityKey,
asOf,
snapshots,
System.Collections.Immutable.ImmutableArray<AdvisoryConflictSnapshot>.Empty));
}
return ValueTask.FromResult(new AdvisoryReplay(
vulnerabilityKey,
asOf,
System.Collections.Immutable.ImmutableArray<AdvisoryStatementSnapshot>.Empty,
System.Collections.Immutable.ImmutableArray<AdvisoryConflictSnapshot>.Empty));
}
public ValueTask AttachStatementProvenanceAsync(Guid statementId, DsseProvenance provenance, TrustInfo trust, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
private sealed class StubAdvisoryStore : IAdvisoryStore
{
private readonly ConcurrentDictionary<string, Advisory> _advisories = new(StringComparer.OrdinalIgnoreCase);
public Task UpsertAsync(Advisory advisory, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_advisories[advisory.AdvisoryKey] = advisory;
return Task.CompletedTask;
}
public Task<Advisory?> FindAsync(string advisoryKey, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_advisories.TryGetValue(advisoryKey, out var advisory);
return Task.FromResult<Advisory?>(advisory);
}
public Task<IReadOnlyList<Advisory>> GetRecentAsync(int limit, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var result = _advisories.Values
.OrderByDescending(a => a.Modified ?? a.Published ?? DateTimeOffset.MinValue)
.Take(limit)
.ToArray();
return Task.FromResult<IReadOnlyList<Advisory>>(result);
}
public async IAsyncEnumerable<Advisory> StreamAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var advisory in _advisories.Values.OrderBy(a => a.AdvisoryKey, StringComparer.OrdinalIgnoreCase))
{
cancellationToken.ThrowIfCancellationRequested();
yield return advisory;
await Task.Yield();
}
}
}
}
[Fact]