Restore platform ownership for v2 evidence routes
This commit is contained in:
@@ -14,3 +14,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| RGH-03 | DONE | 2026-03-05: Aligned `/api/v1/search*` and `/api/v1/advisory-ai*` route translations to AdvisoryAI `/v1/*`, added compose/runtime parity safeguards, and verified setup smoke coverage. |
|
||||
| RGH-04 | DONE | 2026-03-10: Tightened frontdoor route boundaries for `/policy`, restored explicit platform ownership for `/api/v1/aoc*` and `/api/v1/administration*`, and stripped `/doctor`/`/scheduler` shell prefixes before microservice dispatch. |
|
||||
| RGH-05 | DONE | 2026-03-10: Added segment boundaries to `/doctor` and `/scheduler` frontdoor prefixes so lazy web chunks stay on static hosting while compatibility API prefixes still dispatch to the correct microservice. |
|
||||
| RGH-06 | DONE | 2026-03-10: Restored explicit platform ownership for `/api/v2/evidence*` so environment posture and mission-control evidence read models no longer fall through to the generic v2 service resolver. |
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
{ "Type": "Microservice", "Path": "^/api/v2/releases(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/releases$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/v2/security(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/security$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/v2/topology(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/topology$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/v2/evidence(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/evidence$1" },
|
||||
{ "Type": "Microservice", "Path": "^/api/v2/integrations(.*)", "IsRegex": true, "TranslatesTo": "http://platform.stella-ops.local/api/v2/integrations$1" },
|
||||
|
||||
{ "Type": "Microservice", "Path": "^/api/v1/([^/]+)(.*)", "IsRegex": true, "TranslatesTo": "http://$1.stella-ops.local/api/v1/$1$2" },
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed class GatewayRouteSearchMappingsTests
|
||||
("^/api/v2/releases(.*)", "http://platform.stella-ops.local/api/v2/releases$1", "Microservice", true),
|
||||
("^/api/v2/security(.*)", "http://platform.stella-ops.local/api/v2/security$1", "Microservice", true),
|
||||
("^/api/v2/topology(.*)", "http://platform.stella-ops.local/api/v2/topology$1", "Microservice", true),
|
||||
("^/api/v2/evidence(.*)", "http://platform.stella-ops.local/api/v2/evidence$1", "Microservice", true),
|
||||
("^/api/v2/integrations(.*)", "http://platform.stella-ops.local/api/v2/integrations$1", "Microservice", true),
|
||||
("^/scheduler(?=/|$)(.*)", "http://scheduler.stella-ops.local$1", "Microservice", true),
|
||||
("^/doctor(?=/|$)(.*)", "http://doctor.stella-ops.local$1", "Microservice", true),
|
||||
|
||||
@@ -390,6 +390,53 @@ public sealed class RouteDispatchMiddlewareMicroserviceTests
|
||||
context.Items[RouterHttpContextKeys.TranslatedRequestPath] as string);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_SpecificPlatformV2EvidenceRoute_PrefersPlatformOverGenericCatchAll()
|
||||
{
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = @"^/api/v2/evidence(.*)",
|
||||
IsRegex = true,
|
||||
TranslatesTo = "http://platform.stella-ops.local/api/v2/evidence$1"
|
||||
},
|
||||
new StellaOpsRoute
|
||||
{
|
||||
Type = StellaOpsRouteType.Microservice,
|
||||
Path = @"^/api/v2/([^/]+)(.*)",
|
||||
IsRegex = true,
|
||||
TranslatesTo = "http://$1.stella-ops.local/api/v2/$1$2"
|
||||
}
|
||||
]);
|
||||
|
||||
var httpClientFactory = new Mock<IHttpClientFactory>();
|
||||
httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny<string>())).Returns(new HttpClient());
|
||||
|
||||
var nextCalled = false;
|
||||
var middleware = new RouteDispatchMiddleware(
|
||||
_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
resolver,
|
||||
httpClientFactory.Object,
|
||||
NullLogger<RouteDispatchMiddleware>.Instance);
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/api/v2/evidence/packs";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(nextCalled);
|
||||
Assert.Equal(
|
||||
"platform",
|
||||
context.Items[RouterHttpContextKeys.RouteTargetMicroservice] as string);
|
||||
Assert.False(context.Items.ContainsKey(RouterHttpContextKeys.TranslatedRequestPath));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/doctor/api/v1/doctor/checks", @"^/doctor(?=/|$)(.*)", "http://doctor.stella-ops.local$1", "doctor", "/api/v1/doctor/checks")]
|
||||
[InlineData("/scheduler/api/v1/scheduler/runs", @"^/scheduler(?=/|$)(.*)", "http://scheduler.stella-ops.local$1", "scheduler", "/api/v1/scheduler/runs")]
|
||||
|
||||
@@ -152,6 +152,29 @@ public sealed class StellaOpsRouteResolverTests
|
||||
Assert.Equal(expectedCapture, result.RegexMatch!.Groups[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_SpecificPlatformV2EvidenceRoute_BeatsGenericCatchAll()
|
||||
{
|
||||
var resolver = new StellaOpsRouteResolver(
|
||||
[
|
||||
MakeRoute(
|
||||
@"^/api/v2/evidence(.*)",
|
||||
isRegex: true,
|
||||
translatesTo: "http://platform.stella-ops.local/api/v2/evidence$1"),
|
||||
MakeRoute(
|
||||
@"^/api/v2/([^/]+)(.*)",
|
||||
isRegex: true,
|
||||
translatesTo: "http://$1.stella-ops.local/api/v2/$1$2")
|
||||
]);
|
||||
|
||||
var result = resolver.Resolve(new PathString("/api/v2/evidence/packs"));
|
||||
|
||||
Assert.NotNull(result.Route);
|
||||
Assert.Equal(@"^/api/v2/evidence(.*)", result.Route!.Path);
|
||||
Assert.NotNull(result.RegexMatch);
|
||||
Assert.Equal("/packs", result.RegexMatch!.Groups[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_NoMatch_ReturnsNull()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user