Gaps fill up, fixes, ui restructuring

This commit is contained in:
master
2026-02-19 22:10:54 +02:00
parent b5829dce5c
commit 04cacdca8a
331 changed files with 42859 additions and 2174 deletions

View File

@@ -0,0 +1,324 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Persistence.Postgres.Models;
using StellaOps.Concelier.Persistence.Postgres.Repositories;
using StellaOps.Concelier.WebService.Extensions;
using StellaOps.Concelier.WebService.Options;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Concelier.WebService.Tests;
public sealed class AdvisorySourceWebAppFactory : WebApplicationFactory<Program>
{
public AdvisorySourceWebAppFactory()
{
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__CONNECTIONSTRING", "Host=localhost;Port=5432;Database=test-advisory-sources");
Environment.SetEnvironmentVariable("CONCELIER__POSTGRESSTORAGE__COMMANDTIMEOUTSECONDS", "30");
Environment.SetEnvironmentVariable("CONCELIER__TELEMETRY__ENABLED", "false");
Environment.SetEnvironmentVariable("CONCELIER_SKIP_OPTIONS_VALIDATION", "1");
Environment.SetEnvironmentVariable("CONCELIER_TEST_STORAGE_DSN", "Host=localhost;Port=5432;Database=test-advisory-sources");
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Testing");
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Testing");
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((_, config) =>
{
var overrides = new Dictionary<string, string?>
{
{ "PostgresStorage:ConnectionString", "Host=localhost;Port=5432;Database=test-advisory-sources" },
{ "PostgresStorage:CommandTimeoutSeconds", "30" },
{ "Telemetry:Enabled", "false" }
};
config.AddInMemoryCollection(overrides);
});
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
})
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, static _ => { });
services.AddAuthorization(options =>
{
// Endpoint behavior in this test suite focuses on tenant/header/repository behavior.
// Authorization policy is exercised in dedicated auth coverage tests.
options.AddPolicy("Concelier.Advisories.Read", policy => policy.RequireAssertion(static _ => true));
});
services.RemoveAll<ILeaseStore>();
services.AddSingleton<ILeaseStore, Fixtures.TestLeaseStore>();
services.RemoveAll<IAdvisorySourceReadRepository>();
services.AddSingleton<IAdvisorySourceReadRepository, StubAdvisorySourceReadRepository>();
services.RemoveAll<ISourceRepository>();
services.AddSingleton<ISourceRepository, StubSourceRepository>();
services.AddSingleton<ConcelierOptions>(new ConcelierOptions
{
PostgresStorage = new ConcelierOptions.PostgresStorageOptions
{
ConnectionString = "Host=localhost;Port=5432;Database=test-advisory-sources",
CommandTimeoutSeconds = 30
},
Telemetry = new ConcelierOptions.TelemetryOptions
{
Enabled = false
}
});
services.AddSingleton<IConfigureOptions<ConcelierOptions>>(_ => new ConfigureOptions<ConcelierOptions>(opts =>
{
opts.PostgresStorage ??= new ConcelierOptions.PostgresStorageOptions();
opts.PostgresStorage.ConnectionString = "Host=localhost;Port=5432;Database=test-advisory-sources";
opts.PostgresStorage.CommandTimeoutSeconds = 30;
opts.Telemetry ??= new ConcelierOptions.TelemetryOptions();
opts.Telemetry.Enabled = false;
}));
});
}
private sealed class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string SchemeName = "AdvisorySourceTests";
public TestAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "advisory-source-tests")
};
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, SchemeName));
var ticket = new AuthenticationTicket(principal, SchemeName);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
private sealed class StubAdvisorySourceReadRepository : IAdvisorySourceReadRepository
{
private static readonly AdvisorySourceFreshnessRecord[] Records =
[
new(
SourceId: Guid.Parse("0e94e643-4045-4af8-b0c4-f4f04f769279"),
SourceKey: "nvd",
SourceName: "NVD",
SourceFamily: "nvd",
SourceUrl: "https://nvd.nist.gov",
Priority: 100,
Enabled: true,
LastSyncAt: DateTimeOffset.Parse("2026-02-19T08:11:00Z"),
LastSuccessAt: DateTimeOffset.Parse("2026-02-19T08:10:00Z"),
LastError: null,
SyncCount: 220,
ErrorCount: 1,
FreshnessSlaSeconds: 14400,
WarningRatio: 0.8m,
FreshnessAgeSeconds: 3600,
FreshnessStatus: "healthy",
SignatureStatus: "signed",
TotalAdvisories: 220,
SignedAdvisories: 215,
UnsignedAdvisories: 5,
SignatureFailureCount: 1),
new(
SourceId: Guid.Parse("fc9d6356-01d8-4012-8ce7-31e0f983f8c3"),
SourceKey: "ghsa",
SourceName: "GHSA",
SourceFamily: "ghsa",
SourceUrl: "https://github.com/advisories",
Priority: 80,
Enabled: false,
LastSyncAt: DateTimeOffset.Parse("2026-02-19T01:00:00Z"),
LastSuccessAt: DateTimeOffset.Parse("2026-02-18T20:30:00Z"),
LastError: "timeout",
SyncCount: 200,
ErrorCount: 8,
FreshnessSlaSeconds: 14400,
WarningRatio: 0.8m,
FreshnessAgeSeconds: 43200,
FreshnessStatus: "stale",
SignatureStatus: "unsigned",
TotalAdvisories: 200,
SignedAdvisories: 0,
UnsignedAdvisories: 200,
SignatureFailureCount: 0)
];
public Task<IReadOnlyList<AdvisorySourceFreshnessRecord>> ListAsync(
bool includeDisabled = false,
CancellationToken cancellationToken = default)
{
IReadOnlyList<AdvisorySourceFreshnessRecord> items = includeDisabled
? Records
: Records.Where(static record => record.Enabled).ToList();
return Task.FromResult(items);
}
public Task<AdvisorySourceFreshnessRecord?> GetBySourceIdAsync(
Guid sourceId,
CancellationToken cancellationToken = default)
{
return Task.FromResult(Records.FirstOrDefault(record => record.SourceId == sourceId));
}
}
private sealed class StubSourceRepository : ISourceRepository
{
private static readonly IReadOnlyList<SourceEntity> Sources =
[
new SourceEntity
{
Id = Guid.Parse("0e94e643-4045-4af8-b0c4-f4f04f769279"),
Key = "nvd",
Name = "NVD",
SourceType = "nvd",
Url = "https://nvd.nist.gov",
Priority = 100,
Enabled = true,
CreatedAt = DateTimeOffset.Parse("2026-02-18T00:00:00Z"),
UpdatedAt = DateTimeOffset.Parse("2026-02-19T00:00:00Z")
}
];
public Task<SourceEntity> UpsertAsync(SourceEntity source, CancellationToken cancellationToken = default)
=> Task.FromResult(source);
public Task<SourceEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
=> Task.FromResult(Sources.FirstOrDefault(source => source.Id == id));
public Task<SourceEntity?> GetByKeyAsync(string key, CancellationToken cancellationToken = default)
=> Task.FromResult(Sources.FirstOrDefault(source => string.Equals(source.Key, key, StringComparison.OrdinalIgnoreCase)));
public Task<IReadOnlyList<SourceEntity>> ListAsync(bool? enabled = null, CancellationToken cancellationToken = default)
{
var items = Sources
.Where(source => enabled is null || source.Enabled == enabled.Value)
.ToList();
return Task.FromResult<IReadOnlyList<SourceEntity>>(items);
}
}
}
public sealed class AdvisorySourceEndpointsTests : IClassFixture<AdvisorySourceWebAppFactory>
{
private readonly AdvisorySourceWebAppFactory _factory;
public AdvisorySourceEndpointsTests(AdvisorySourceWebAppFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ListEndpoints_WithoutTenantHeader_ReturnsBadRequest()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/api/v1/advisory-sources", CancellationToken.None);
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ListEndpoints_WithTenantHeader_ReturnsRecords()
{
using var client = CreateTenantClient();
var response = await client.GetAsync("/api/v1/advisory-sources?includeDisabled=true", CancellationToken.None);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<AdvisorySourceListResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(payload);
Assert.Equal(2, payload!.TotalCount);
Assert.Contains(payload.Items, static item => item.SourceKey == "nvd");
Assert.Contains(payload.Items, static item => item.SourceKey == "ghsa");
var nvd = payload.Items.Single(static item => item.SourceKey == "nvd");
Assert.Equal(220, nvd.TotalAdvisories);
Assert.Equal(215, nvd.SignedAdvisories);
Assert.Equal(5, nvd.UnsignedAdvisories);
Assert.Equal(1, nvd.SignatureFailureCount);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SummaryEndpoint_ReturnsExpectedCounts()
{
using var client = CreateTenantClient();
var response = await client.GetAsync("/api/v1/advisory-sources/summary", CancellationToken.None);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<AdvisorySourceSummaryResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(payload);
Assert.Equal(2, payload!.TotalSources);
Assert.Equal(1, payload.HealthySources);
Assert.Equal(1, payload.StaleSources);
Assert.Equal(1, payload.DisabledSources);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FreshnessEndpoint_ByKey_ReturnsRecord()
{
using var client = CreateTenantClient();
var response = await client.GetAsync("/api/v1/advisory-sources/nvd/freshness", CancellationToken.None);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<AdvisorySourceFreshnessResponse>(cancellationToken: CancellationToken.None);
Assert.NotNull(payload);
Assert.Equal("nvd", payload!.Source.SourceKey);
Assert.Equal("healthy", payload.Source.FreshnessStatus);
Assert.Equal(220, payload.Source.TotalAdvisories);
Assert.Equal(215, payload.Source.SignedAdvisories);
Assert.Equal(5, payload.Source.UnsignedAdvisories);
Assert.Equal(1, payload.Source.SignatureFailureCount);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task FreshnessEndpoint_UnknownSource_ReturnsNotFound()
{
using var client = CreateTenantClient();
var response = await client.GetAsync("/api/v1/advisory-sources/unknown-source/freshness", CancellationToken.None);
Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode);
}
private HttpClient CreateTenantClient()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a");
return client;
}
}

View File

@@ -8,3 +8,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0243-M | DONE | Revalidated 2026-01-07. |
| AUDIT-0243-T | DONE | Revalidated 2026-01-07. |
| AUDIT-0243-A | DONE | Waived (test project; revalidated 2026-01-07). |
| BE8-07-TEST | DONE | Extended advisory-source endpoint coverage for total/signed/unsigned/signature-failure contract fields. |