fix(router): ship audit bundle frontdoor cutover

This commit is contained in:
master
2026-03-08 14:30:12 +02:00
parent 8852928115
commit 30532800ec
9 changed files with 367 additions and 16 deletions

View File

@@ -313,6 +313,30 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains("regex", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_DuplicateExactRoutePath_Throws()
{
var options = CreateValidOptions();
options.Routes.Add(new StellaOpsRoute
{
Type = StellaOpsRouteType.ReverseProxy,
Path = "/v1/audit-bundles",
TranslatesTo = "http://exportcenter.stella-ops.local/v1/audit-bundles"
});
options.Routes.Add(new StellaOpsRoute
{
Type = StellaOpsRouteType.ReverseProxy,
Path = "/v1/audit-bundles",
TranslatesTo = "http://evidencelocker.stella-ops.local/v1/audit-bundles"
});
var exception = Assert.Throws<InvalidOperationException>(() =>
GatewayOptionsValidator.Validate(options));
Assert.Contains("Duplicate route path", exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("/v1/audit-bundles", exception.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_ValidRegex_DoesNotThrow()
{

View File

@@ -1,9 +1,11 @@
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
@@ -198,6 +200,87 @@ public sealed class AspNetRouterRequestDispatcherTests
Assert.Contains("\"timeWindow\":\"24h\"", responseBody, StringComparison.Ordinal);
}
[Fact]
public async Task DispatchAsync_BindsAuditBundleCreateRequest_WithAcceptedUnionResult()
{
var builder = WebApplication.CreateBuilder();
var app = builder.Build();
app.MapPost(
"/v1/audit-bundles",
Results<Accepted<AuditBundleAcceptedResponse>, BadRequest<AuditBundleErrorEnvelope>> (
[FromBody] AuditBundleCreateRequest body) =>
{
if (string.IsNullOrWhiteSpace(body.Subject.Name))
{
return TypedResults.BadRequest(
new AuditBundleErrorEnvelope(
new AuditBundleErrorDetail("INVALID_REQUEST", "Subject name is required")));
}
return TypedResults.Accepted(
$"/v1/audit-bundles/{body.Subject.Name}",
new AuditBundleAcceptedResponse(
$"bndl-{body.Subject.Name}",
"Pending",
$"/v1/audit-bundles/{body.Subject.Name}",
30));
});
var endpointRouteBuilder = (IEndpointRouteBuilder)app;
var endpointDataSource = new StaticEndpointDataSource(
endpointRouteBuilder.DataSources.SelectMany(static dataSource => dataSource.Endpoints).ToArray());
var dispatcher = new AspNetRouterRequestDispatcher(
app.Services,
endpointDataSource,
new StellaRouterBridgeOptions
{
ServiceName = "exportcenter",
Version = "1.0.0-alpha1",
Region = "local",
AuthorizationTrustMode = GatewayAuthorizationTrustMode.ServiceEnforced
},
NullLogger<AspNetRouterRequestDispatcher>.Instance);
var transportFrame = FrameConverter.ToFrame(new RequestFrame
{
RequestId = "req-audit-1",
Method = "POST",
Path = "/v1/audit-bundles",
Headers = new Dictionary<string, string>
{
["content-type"] = "application/json"
},
Payload = Encoding.UTF8.GetBytes("""
{
"subject": {
"type": "IMAGE",
"name": "asset-review-prod",
"digest": {
"sha256": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
}
},
"includeContent": {
"vulnReports": true,
"sbom": true,
"vexDecisions": true,
"policyEvaluations": true,
"attestations": true
}
}
""")
});
var roundTrippedRequest = FrameConverter.ToRequestFrame(transportFrame);
Assert.NotNull(roundTrippedRequest);
var response = await dispatcher.DispatchAsync(roundTrippedRequest!);
var responseBody = Encoding.UTF8.GetString(response.Payload.ToArray());
Assert.Equal(StatusCodes.Status202Accepted, response.StatusCode);
Assert.Contains("\"bundleId\":\"bndl-asset-review-prod\"", responseBody, StringComparison.Ordinal);
Assert.Contains("\"status\":\"Pending\"", responseBody, StringComparison.Ordinal);
}
private static AspNetRouterRequestDispatcher CreateDispatcher(RouteEndpoint endpoint, StellaRouterBridgeOptions options)
{
var services = new ServiceCollection();
@@ -227,4 +310,27 @@ public sealed class AspNetRouterRequestDispatcherTests
}
private sealed record PreferencesBody(string[] Regions, string[] Environments, string TimeWindow);
private sealed record AuditBundleCreateRequest(
AuditBundleSubjectRef Subject,
AuditBundleTimeWindow? TimeWindow,
AuditBundleContentSelection IncludeContent,
string? CallbackUrl = null);
private sealed record AuditBundleSubjectRef(
string Type,
string Name,
IReadOnlyDictionary<string, string> Digest);
private sealed record AuditBundleTimeWindow(DateTimeOffset? From, DateTimeOffset? To);
private sealed record AuditBundleContentSelection(
bool VulnReports = true,
bool Sbom = true,
bool VexDecisions = true,
bool PolicyEvaluations = true,
bool Attestations = true);
private sealed record AuditBundleAcceptedResponse(
string BundleId,
string Status,
string StatusUrl,
int? EstimatedCompletionSeconds);
private sealed record AuditBundleErrorEnvelope(AuditBundleErrorDetail Error);
private sealed record AuditBundleErrorDetail(string Code, string Message);
}