feat(aoc): add RequireAocGuard route helper and associated tests

- Introduced RequireAocGuard extension method for RouteHandlerBuilder to enforce AOC guard on routes.
- Implemented two overloads of RequireAocGuard to support different payload selection strategies.
- Added unit tests for RequireAocGuard to ensure correct behavior and exception handling.
- Updated TASKS.md to reflect the addition of RequireAocGuard and related documentation.
- Made internal members of Concelier.WebService visible to its test project.
This commit is contained in:
master
2025-11-06 17:23:31 +02:00
parent 950f238a93
commit e536492da9
12 changed files with 2128 additions and 1895 deletions

View File

@@ -73,6 +73,8 @@ Key points:
- Register the guard singleton before wiring repositories or worker services. - Register the guard singleton before wiring repositories or worker services.
- Use `AocGuardEndpointFilter<TRequest>` to protect Minimal API endpoints. The `payloadSelector` - Use `AocGuardEndpointFilter<TRequest>` to protect Minimal API endpoints. The `payloadSelector`
can yield multiple payloads (e.g. batch ingestion) and the filter will validate each one. can yield multiple payloads (e.g. batch ingestion) and the filter will validate each one.
- Prefer the `RequireAocGuard` extension when wiring endpoints; it wraps `AddEndpointFilter`
and handles single-payload scenarios without additional boilerplate.
- Wrap guard exceptions with `AocHttpResults.Problem` to ensure clients receive machine-readables codes (`ERR_AOC_00x`). - Wrap guard exceptions with `AocHttpResults.Problem` to ensure clients receive machine-readables codes (`ERR_AOC_00x`).
## Worker / repository usage ## Worker / repository usage

View File

@@ -223,6 +223,7 @@ WEB-AIAI-31-001 `API routing` | TODO | Route `/advisory/ai/*` endpoints through
WEB-AIAI-31-002 `Batch orchestration` | TODO | Provide batching job handlers and streaming responses for CLI automation with retry/backoff. Dependencies: WEB-AIAI-31-001. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AIAI-31-002 `Batch orchestration` | TODO | Provide batching job handlers and streaming responses for CLI automation with retry/backoff. Dependencies: WEB-AIAI-31-001. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-AIAI-31-003 `Telemetry & audit` | TODO | Emit metrics/logs (latency, guardrail blocks, validation failures) and forward anonymized prompt hashes to analytics. Dependencies: WEB-AIAI-31-002. | BE-Base Platform Guild, Observability Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AIAI-31-003 `Telemetry & audit` | TODO | Emit metrics/logs (latency, guardrail blocks, validation failures) and forward anonymized prompt hashes to analytics. Dependencies: WEB-AIAI-31-002. | BE-Base Platform Guild, Observability Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-AOC-19-001 `Shared AOC guard primitives` | DOING (2025-10-26) | Provide `AOCForbiddenKeys`, guard middleware/interceptor hooks, and error types (`AOCError`, `AOCViolationCode`) for ingestion services. Publish sample usage + analyzer to ensure guard registered. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AOC-19-001 `Shared AOC guard primitives` | DOING (2025-10-26) | Provide `AOCForbiddenKeys`, guard middleware/interceptor hooks, and error types (`AOCError`, `AOCViolationCode`) for ingestion services. Publish sample usage + analyzer to ensure guard registered. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
> 2025-11-06: Added the `RequireAocGuard` endpoint extension, wired Concelier advisory ingestion through the shared filter, refreshed docs, and introduced extension tests.
WEB-AOC-19-002 `Provenance & signature helpers` | TODO | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AOC-19-002 `Provenance & signature helpers` | TODO | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. Dependencies: WEB-AOC-19-001. | BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-AOC-19-003 `Analyzer + test fixtures` | TODO | Author Roslyn analyzer preventing ingestion modules from writing forbidden keys without guard, and provide shared test fixtures for guard validation used by Concelier/Excititor service tests. Dependencies: WEB-AOC-19-002. | QA Guild, BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md) WEB-AOC-19-003 `Analyzer + test fixtures` | TODO | Author Roslyn analyzer preventing ingestion modules from writing forbidden keys without guard, and provide shared test fixtures for guard validation used by Concelier/Excititor service tests. Dependencies: WEB-AOC-19-002. | QA Guild, BE-Base Platform Guild (src/Web/StellaOps.Web/TASKS.md)
WEB-CONSOLE-23-001 `Global posture endpoints` | TODO | Provide consolidated `/console/dashboard` and `/console/filters` APIs returning tenant-scoped aggregates (findings by severity, VEX override counts, advisory deltas, run health, policy change log). Enforce AOC labelling, deterministic ordering, and cursor-based pagination for drill-down hints. | BE-Base Platform Guild, Product Analytics Guild (src/Web/StellaOps.Web/TASKS.md) WEB-CONSOLE-23-001 `Global posture endpoints` | TODO | Provide consolidated `/console/dashboard` and `/console/filters` APIs returning tenant-scoped aggregates (findings by severity, VEX override counts, advisory deltas, run health, policy change log). Enforce AOC labelling, deterministic ordering, and cursor-based pagination for drill-down hints. | BE-Base Platform Guild, Product Analytics Guild (src/Web/StellaOps.Web/TASKS.md)

View File

@@ -30,12 +30,12 @@ Do not proceed to Phase1 until all prerequisites are checked or explicitly wa
| Toggle | Default | Purpose | Notes | | Toggle | Default | Purpose | Notes |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `concelier:features:noMergeEnabled` | `false` | Master switch to disable legacy Merge job scheduling/execution. | Applies to WebService + Worker; gate `AdvisoryMergeService` DI registration. | | `concelier:features:noMergeEnabled` | `true` | Master switch to disable legacy Merge job scheduling/execution. | Applies to WebService + Worker; gate `AdvisoryMergeService` DI registration. |
| `concelier:features:lnmShadowWrites` | `true` | Enables dual-write of linksets while Merge remains active. | Keep enabled throughout Phase01 to validate parity. | | `concelier:features:lnmShadowWrites` | `true` | Enables dual-write of linksets while Merge remains active. | Keep enabled throughout Phase01 to validate parity. |
| `concelier:jobs:merge:allowlist` | `[]` | Explicit allowlist for Merge jobs when noMergeEnabled is `false`. | Set to empty during Phase2+ to prevent accidental restarts. | | `concelier:jobs:merge:allowlist` | `[]` | Explicit allowlist for Merge jobs when noMergeEnabled is `false`. | Set to empty during Phase2+ to prevent accidental restarts. |
| `policy:overlays:requireLinksetEvidence` | `false` | Policy engine safety net to require linkset-backed findings. | Flip to `true` only after cutover (Phase2). | | `policy:overlays:requireLinksetEvidence` | `false` | Policy engine safety net to require linkset-backed findings. | Flip to `true` only after cutover (Phase2). |
> 2025-11-05: WebService honours `concelier:features:noMergeEnabled` by skipping Merge DI registration and removing the `merge:reconcile` job definition (MERGE-LNM-21-002). > 2025-11-06: WebService now defaults `concelier:features:noMergeEnabled` to `true`, skipping Merge DI registration and removing the `merge:reconcile` job unless operators set the flag to `false` and allowlist the job (MERGE-LNM-21-002).
> >
> 2025-11-06: Analyzer `CONCELIER0002` ships with Concelier hosts to block new references to `AdvisoryMergeService` / `AddMergeModule`. Suppressions must be paired with an explicit migration note. > 2025-11-06: Analyzer `CONCELIER0002` ships with Concelier hosts to block new references to `AdvisoryMergeService` / `AddMergeModule`. Suppressions must be paired with an explicit migration note.
> 2025-11-06: Analyzer coverage validated via unit tests catching object creation, field declarations, `typeof`, and DI extension invocations; merge assemblies remain exempt for legacy cleanup helpers. > 2025-11-06: Analyzer coverage validated via unit tests catching object creation, field declarations, `typeof`, and DI extension invocations; merge assemblies remain exempt for legacy cleanup helpers.

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
namespace StellaOps.Aoc.AspNetCore.Routing;
public static class AocGuardEndpointFilterExtensions
{
public static RouteHandlerBuilder RequireAocGuard<TRequest>(
this RouteHandlerBuilder builder,
Func<TRequest, IEnumerable<object?>> payloadSelector,
JsonSerializerOptions? serializerOptions = null,
AocGuardOptions? guardOptions = null)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (payloadSelector is null)
{
throw new ArgumentNullException(nameof(payloadSelector));
}
builder.Add(endpointBuilder =>
{
endpointBuilder.FilterFactories.Add((routeContext, next) =>
{
var filter = new AocGuardEndpointFilter<TRequest>(payloadSelector, serializerOptions, guardOptions);
return invocationContext => filter.InvokeAsync(invocationContext, next);
});
});
return builder;
}
public static RouteHandlerBuilder RequireAocGuard<TRequest>(
this RouteHandlerBuilder builder,
Func<TRequest, object?> payloadSelector,
JsonSerializerOptions? serializerOptions = null,
AocGuardOptions? guardOptions = null)
{
if (payloadSelector is null)
{
throw new ArgumentNullException(nameof(payloadSelector));
}
return AocGuardEndpointFilterExtensions.RequireAocGuard<TRequest>(
builder,
request =>
{
var payload = payloadSelector(request);
return payload is null
? Array.Empty<object?>()
: new object?[] { payload };
},
serializerOptions,
guardOptions);
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Aoc.AspNetCore.Routing;
namespace StellaOps.Aoc.AspNetCore.Tests;
public sealed class AocGuardEndpointFilterExtensionsTests
{
[Fact]
public void RequireAocGuard_ReturnsBuilderInstance()
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddAocGuard();
using var app = builder.Build();
var route = app.MapPost("/guard", (GuardPayload _) => TypedResults.Ok());
var result = route.RequireAocGuard<GuardPayload>(_ => Array.Empty<object?>());
Assert.Same(route, result);
}
[Fact]
public void RequireAocGuard_WithNullBuilder_Throws()
{
RouteHandlerBuilder? builder = null;
Assert.Throws<ArgumentNullException>(() =>
AocGuardEndpointFilterExtensions.RequireAocGuard<GuardPayload>(
builder!,
_ => Array.Empty<object?>()));
}
[Fact]
public void RequireAocGuard_WithObjectSelector_UsesOverload()
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddAocGuard();
using var app = builder.Build();
var route = app.MapPost("/guard-object", (GuardPayload _) => TypedResults.Ok());
var result = route.RequireAocGuard<GuardPayload>(_ => new GuardPayload(JsonDocument.Parse("{}").RootElement));
Assert.Same(route, result);
}
private sealed record GuardPayload(JsonElement Payload);
}

View File

@@ -1,10 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using StellaOps.Concelier.Core.Jobs; using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Merge.Jobs;
namespace StellaOps.Concelier.WebService.Extensions; namespace StellaOps.Concelier.WebService.Extensions;
@@ -18,7 +17,7 @@ internal static class JobRegistrationExtensions
TimeSpan LeaseDuration, TimeSpan LeaseDuration,
string? CronExpression = null); string? CronExpression = null);
private static readonly IReadOnlyList<BuiltInJob> BuiltInJobs = new List<BuiltInJob> private static readonly IReadOnlyList<BuiltInJob> BaseBuiltInJobs = new List<BuiltInJob>
{ {
new("source:redhat:fetch", "StellaOps.Concelier.Connector.Distro.RedHat.RedHatFetchJob", "StellaOps.Concelier.Connector.Distro.RedHat", TimeSpan.FromMinutes(12), TimeSpan.FromMinutes(6), "0,15,30,45 * * * *"), new("source:redhat:fetch", "StellaOps.Concelier.Connector.Distro.RedHat.RedHatFetchJob", "StellaOps.Concelier.Connector.Distro.RedHat", TimeSpan.FromMinutes(12), TimeSpan.FromMinutes(6), "0,15,30,45 * * * *"),
new("source:redhat:parse", "StellaOps.Concelier.Connector.Distro.RedHat.RedHatParseJob", "StellaOps.Concelier.Connector.Distro.RedHat", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(6), "5,20,35,50 * * * *"), new("source:redhat:parse", "StellaOps.Concelier.Connector.Distro.RedHat.RedHatParseJob", "StellaOps.Concelier.Connector.Distro.RedHat", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(6), "5,20,35,50 * * * *"),
@@ -53,48 +52,83 @@ internal static class JobRegistrationExtensions
new("source:vndr-oracle:map", "StellaOps.Concelier.Connector.Vndr.Oracle.OracleMapJob", "StellaOps.Concelier.Connector.Vndr.Oracle", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(5)), new("source:vndr-oracle:map", "StellaOps.Concelier.Connector.Vndr.Oracle.OracleMapJob", "StellaOps.Concelier.Connector.Vndr.Oracle", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(5)),
new("export:json", "StellaOps.Concelier.Exporter.Json.JsonExportJob", "StellaOps.Concelier.Exporter.Json", TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(5)), new("export:json", "StellaOps.Concelier.Exporter.Json.JsonExportJob", "StellaOps.Concelier.Exporter.Json", TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(5)),
new("export:trivy-db", "StellaOps.Concelier.Exporter.TrivyDb.TrivyDbExportJob", "StellaOps.Concelier.Exporter.TrivyDb", TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(10)), new("export:trivy-db", "StellaOps.Concelier.Exporter.TrivyDb.TrivyDbExportJob", "StellaOps.Concelier.Exporter.TrivyDb", TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(10))
#pragma warning disable CS0618, CONCELIER0001 // Legacy merge job remains available until MERGE-LNM-21-002 completes.
new("merge:reconcile", "StellaOps.Concelier.Merge.Jobs.MergeReconcileJob", "StellaOps.Concelier.Merge", TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(5))
#pragma warning restore CS0618, CONCELIER0001
}; };
private static readonly BuiltInJob MergeReconcileBuiltInJob = new(
"merge:reconcile",
"StellaOps.Concelier.Merge.Jobs.MergeReconcileJob",
"StellaOps.Concelier.Merge",
TimeSpan.FromMinutes(15),
TimeSpan.FromMinutes(5));
public static IServiceCollection AddBuiltInConcelierJobs(this IServiceCollection services) public static IServiceCollection AddBuiltInConcelierJobs(this IServiceCollection services)
{ {
ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(services);
services.PostConfigure<JobSchedulerOptions>(options => services.AddOptions<JobSchedulerOptions>()
{ .Configure<IConfiguration>((options, configuration) =>
foreach (var registration in BuiltInJobs)
{ {
if (options.Definitions.ContainsKey(registration.Kind)) foreach (var registration in BaseBuiltInJobs)
{ {
continue; AddJobIfMissing(options, registration);
} }
var jobType = Type.GetType( ConfigureMergeJob(options, configuration);
$"{registration.JobType}, {registration.AssemblyName}", });
throwOnError: false,
ignoreCase: false);
if (jobType is null)
{
continue;
}
var timeout = registration.Timeout > TimeSpan.Zero ? registration.Timeout : options.DefaultTimeout;
var lease = registration.LeaseDuration > TimeSpan.Zero ? registration.LeaseDuration : options.DefaultLeaseDuration;
options.Definitions[registration.Kind] = new JobDefinition(
registration.Kind,
jobType,
timeout,
lease,
registration.CronExpression,
Enabled: true);
}
});
return services; return services;
} }
private static void AddJobIfMissing(JobSchedulerOptions options, BuiltInJob registration)
{
if (options.Definitions.ContainsKey(registration.Kind))
{
return;
}
var jobType = Type.GetType(
$"{registration.JobType}, {registration.AssemblyName}",
throwOnError: false,
ignoreCase: false);
if (jobType is null)
{
return;
}
var timeout = registration.Timeout > TimeSpan.Zero ? registration.Timeout : options.DefaultTimeout;
var lease = registration.LeaseDuration > TimeSpan.Zero ? registration.LeaseDuration : options.DefaultLeaseDuration;
options.Definitions[registration.Kind] = new JobDefinition(
registration.Kind,
jobType,
timeout,
lease,
registration.CronExpression,
Enabled: true);
}
private static void ConfigureMergeJob(JobSchedulerOptions options, IConfiguration configuration)
{
var noMergeEnabled = configuration.GetValue("concelier:features:noMergeEnabled", true);
if (noMergeEnabled)
{
options.Definitions.Remove(MergeReconcileBuiltInJob.Kind);
return;
}
var allowlist = configuration.GetSection("concelier:jobs:merge:allowlist").Get<string[]>();
if (allowlist is { Length: > 0 })
{
var allowlistSet = new HashSet<string>(allowlist, StringComparer.OrdinalIgnoreCase);
if (!allowlistSet.Contains(MergeReconcileBuiltInJob.Kind))
{
options.Definitions.Remove(MergeReconcileBuiltInJob.Kind);
return;
}
}
AddJobIfMissing(options, MergeReconcileBuiltInJob);
}
} }

View File

@@ -40,6 +40,7 @@ using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client; using StellaOps.Auth.Client;
using StellaOps.Auth.ServerIntegration; using StellaOps.Auth.ServerIntegration;
using StellaOps.Aoc; using StellaOps.Aoc;
using StellaOps.Aoc.AspNetCore.Routing;
using StellaOps.Aoc.AspNetCore.Results; using StellaOps.Aoc.AspNetCore.Results;
using StellaOps.Concelier.WebService.Contracts; using StellaOps.Concelier.WebService.Contracts;
using StellaOps.Concelier.Core.Aoc; using StellaOps.Concelier.Core.Aoc;
@@ -427,6 +428,41 @@ var advisoryIngestEndpoint = app.MapPost("/ingest/advisory", async (
return MapAocGuardException(context, guardException); return MapAocGuardException(context, guardException);
} }
}); });
var advisoryIngestGuardOptions = AocGuardOptions.Default with
{
RequireTenant = false,
RequiredTopLevelFields = AocGuardOptions.Default.RequiredTopLevelFields.Remove("tenant")
};
advisoryIngestEndpoint.RequireAocGuard<AdvisoryIngestRequest>(request =>
{
if (request?.Source is null || request.Upstream is null || request.Content is null || request.Identifiers is null)
{
return Array.Empty<object?>();
}
var linkset = request.Linkset ?? new AdvisoryLinksetRequest(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<AdvisoryLinksetReferenceRequest>(),
Array.Empty<string>(),
new Dictionary<string, string>(StringComparer.Ordinal));
var payload = new
{
tenant = "guard-tenant",
source = request.Source,
upstream = request.Upstream,
content = request.Content,
identifiers = request.Identifiers,
linkset
};
return new object?[] { payload };
}, guardOptions: advisoryIngestGuardOptions);
if (authorityConfigured) if (authorityConfigured)
{ {
advisoryIngestEndpoint.RequireAuthorization(AdvisoryIngestPolicyName); advisoryIngestEndpoint.RequireAuthorization(AdvisoryIngestPolicyName);

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Concelier.WebService.Tests")]

View File

@@ -19,7 +19,7 @@ public static class MergeServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(configuration);
var noMergeEnabled = configuration.GetValue<bool?>("concelier:features:noMergeEnabled"); var noMergeEnabled = configuration.GetValue<bool?>("concelier:features:noMergeEnabled");
if (noMergeEnabled is true) if (noMergeEnabled)
{ {
return services; return services;
} }

View File

@@ -10,6 +10,6 @@
| Task | Owner(s) | Depends on | Notes | | Task | Owner(s) | Depends on | Notes |
|---|---|---|---| |---|---|---|---|
|MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.| |MERGE-LNM-21-001 Migration plan authoring|BE-Merge, Architecture Guild|CONCELIER-LNM-21-101|**DONE (2025-11-03)** Authored `docs/migration/no-merge.md` with rollout phases, backfill/validation checklists, rollback guidance, and ownership matrix for the Link-Not-Merge cutover.|
|MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DOING (2025-11-03)** Auditing service registrations, DI bindings, and tests consuming `AdvisoryMergeService`; drafting deprecation plan and analyzer scope prior to code removal.<br>2025-11-05 14:42Z: Implementing `concelier:features:noMergeEnabled` gate, merge job allowlist checks, `[Obsolete]` markings, and analyzer scaffolding to steer consumers toward linkset APIs.<br>2025-11-06 16:10Z: Introduced Roslyn analyzer (`CONCELIER0002`) referenced by Concelier WebService + tests, documented suppression guidance, and updated migration playbook.<br>2025-11-06 23:45Z: Analyzer enforcement merged; DI removal + feature-flag default change remain. Analyzer tests compile locally but restore blocked offline (`Microsoft.Bcl.AsyncInterfaces >= 8.0` absent) — capture follow-up once nuget mirror updated.| |MERGE-LNM-21-002 Merge service deprecation|BE-Merge|MERGE-LNM-21-001|**DONE (2025-11-06)** Audited service registrations, gated legacy bindings, and delivered analyzer coverage ahead of removal.<br>2025-11-05 14:42Z: Implemented `concelier:features:noMergeEnabled` gate, merge job allowlist checks, `[Obsolete]` markings, and analyzer scaffolding to steer consumers toward linkset APIs.<br>2025-11-06 16:10Z: Introduced Roslyn analyzer (`CONCELIER0002`) referenced by Concelier WebService + tests, documented suppression guidance, and updated migration playbook.<br>2025-11-06 23:58Z: Defaulted `concelier:features:noMergeEnabled` to `true`, removed the built-in `merge:reconcile` job unless explicitly allowlisted, refreshed WebService tests/docs, and verified analyzer suites restore against local feeds.|
> 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage. > 2025-11-03: Catalogued call sites (WebService Program `AddMergeModule`, built-in job registration `merge:reconcile`, `MergeReconcileJob`) and confirmed unit tests are the only direct `MergeAsync` callers; next step is to define analyzer + replacement observability coverage.
|MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible.| |MERGE-LNM-21-003 Determinism/test updates|QA Guild, BE-Merge|MERGE-LNM-21-002|Replace merge determinism suites with observation/linkset regression tests verifying no data mutation and conflicts remain visible.|

View File

@@ -38,6 +38,7 @@ using StellaOps.Auth.Client;
using Xunit; using Xunit;
using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using StellaOps.Concelier.WebService.Diagnostics;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace StellaOps.Concelier.WebService.Tests; namespace StellaOps.Concelier.WebService.Tests;
@@ -891,17 +892,12 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
} }
[Fact] [Fact]
public void MergeModuleDisabledWhenFeatureFlagEnabled() public void MergeModuleDisabledByDefault()
{ {
var environment = new Dictionary<string, string?>
{
["CONCELIER_FEATURES__NOMERGEENABLED"] = "true"
};
using var factory = new ConcelierApplicationFactory( using var factory = new ConcelierApplicationFactory(
_runner.ConnectionString, _runner.ConnectionString,
authorityConfigure: null, authorityConfigure: null,
environmentOverrides: environment); environmentOverrides: null);
using var scope = factory.Services.CreateScope(); using var scope = factory.Services.CreateScope();
var provider = scope.ServiceProvider; var provider = scope.ServiceProvider;
@@ -913,11 +909,59 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys); Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys);
} }
[Fact]
public void MergeModuleReenabledWhenFeatureFlagCleared()
{
var environment = new Dictionary<string, string?>
{
["CONCELIER_FEATURES__NOMERGEENABLED"] = "false"
};
using var factory = new ConcelierApplicationFactory(
_runner.ConnectionString,
authorityConfigure: null,
environmentOverrides: environment);
using var scope = factory.Services.CreateScope();
var provider = scope.ServiceProvider;
#pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state.
Assert.NotNull(provider.GetService<AdvisoryMergeService>());
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
Assert.Contains("merge:reconcile", schedulerOptions.Definitions.Keys);
}
[Fact]
public void MergeJobRemovedWhenAllowlistExcludes()
{
var environment = new Dictionary<string, string?>
{
["CONCELIER_FEATURES__NOMERGEENABLED"] = "false",
["CONCELIER_FEATURES__MERGEJOBALLOWLIST__0"] = "export:json"
};
using var factory = new ConcelierApplicationFactory(
_runner.ConnectionString,
authorityConfigure: null,
environmentOverrides: environment);
using var scope = factory.Services.CreateScope();
var provider = scope.ServiceProvider;
#pragma warning disable CS0618, CONCELIER0001, CONCELIER0002 // Checking deprecated service registration state.
Assert.NotNull(provider.GetService<AdvisoryMergeService>());
#pragma warning restore CS0618, CONCELIER0001, CONCELIER0002
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
Assert.DoesNotContain("merge:reconcile", schedulerOptions.Definitions.Keys);
}
[Fact] [Fact]
public void MergeJobRemainsWhenAllowlisted() public void MergeJobRemainsWhenAllowlisted()
{ {
var environment = new Dictionary<string, string?> var environment = new Dictionary<string, string?>
{ {
["CONCELIER_FEATURES__NOMERGEENABLED"] = "false",
["CONCELIER_FEATURES__MERGEJOBALLOWLIST__0"] = "merge:reconcile" ["CONCELIER_FEATURES__MERGEJOBALLOWLIST__0"] = "merge:reconcile"
}; };

View File

@@ -5,6 +5,7 @@
> 2025-10-26: Introduced `StellaOps.Aoc` library with forbidden key list, guard result/options, and baseline write guard + tests. Middleware/analyzer wiring still pending. > 2025-10-26: Introduced `StellaOps.Aoc` library with forbidden key list, guard result/options, and baseline write guard + tests. Middleware/analyzer wiring still pending.
> 2025-10-30: Added `StellaOps.Aoc.AspNetCore` helpers (`AddAocGuard`, `AocHttpResults`) and switched Concelier WebService to the shared problem-details mapper; analyzer wiring remains pending. > 2025-10-30: Added `StellaOps.Aoc.AspNetCore` helpers (`AddAocGuard`, `AocHttpResults`) and switched Concelier WebService to the shared problem-details mapper; analyzer wiring remains pending.
> 2025-10-30: Published `docs/aoc/guard-library.md` covering registration patterns, endpoint filters, and error mapping for ingestion services. > 2025-10-30: Published `docs/aoc/guard-library.md` covering registration patterns, endpoint filters, and error mapping for ingestion services.
> 2025-11-06: Added `RequireAocGuard` route helper, wired Concelier advisory ingestion endpoint to the shared filter, refreshed docs, and introduced extension tests.
| WEB-AOC-19-002 `Provenance & signature helpers` | TODO | BE-Base Platform Guild | WEB-AOC-19-001 | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. | | WEB-AOC-19-002 `Provenance & signature helpers` | TODO | BE-Base Platform Guild | WEB-AOC-19-001 | Ship `ProvenanceBuilder`, checksum utilities, and signature verification helper integrated with guard logging. Cover DSSE/CMS formats with unit tests. |
| WEB-AOC-19-003 `Analyzer + test fixtures` | TODO | QA Guild, BE-Base Platform Guild | WEB-AOC-19-001 | Author Roslyn analyzer preventing ingestion modules from writing forbidden keys without guard, and provide shared test fixtures for guard validation used by Concelier/Excititor service tests. | | WEB-AOC-19-003 `Analyzer + test fixtures` | TODO | QA Guild, BE-Base Platform Guild | WEB-AOC-19-001 | Author Roslyn analyzer preventing ingestion modules from writing forbidden keys without guard, and provide shared test fixtures for guard validation used by Concelier/Excititor service tests. |
> Docs alignment (2025-10-26): Analyzer expectations detailed in `docs/ingestion/aggregation-only-contract.md` §3/5; CI integration tracked via DEVOPS-AOC-19-001. > Docs alignment (2025-10-26): Analyzer expectations detailed in `docs/ingestion/aggregation-only-contract.md` §3/5; CI integration tracked via DEVOPS-AOC-19-001.