Fix router ASP.NET request body binding
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
# Sprint 20260307-011 - Router Case-Insensitive Forwarded Header Binding
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Remove the live `Failed to persist global context preferences.` degradation that still occurs after Topology operator actions on `https://stella-ops.local`.
|
||||||
|
- Fix the Router ASP.NET bridge so forwarded HTTP headers remain case-insensitive after request-frame transport round-trips.
|
||||||
|
- Add focused Router regression coverage for lowercase `content-type` on JSON request dispatch and verify the repaired live user flow with Playwright.
|
||||||
|
- Working directory: `src/Router/__Libraries/StellaOps.Microservice.AspNetCore`.
|
||||||
|
- Expected evidence: targeted Router tests, rebuilt `platform` container on the live stack, and Playwright artifacts for the affected topology actions.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Upstream dependency: `docs/implplan/SPRINT_20260307_010_FE_context_preferences_canonical_payload.md` aligned the Web payload with the Platform v2 contract and reduced the live failure from gateway `404` to service-side `400`.
|
||||||
|
- Safe parallelism: stay inside the Router ASP.NET bridge, its focused tests, Router docs, and sprint/task-board updates. Do not edit unrelated Web navigation or settings work from other agents.
|
||||||
|
- Deployment dependency: the live fix only requires rebuilding the `platform` image because the failing bridge code executes inside the Platform service process.
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `docs/modules/router/architecture.md`
|
||||||
|
- `src/Router/AGENTS.md`
|
||||||
|
- `src/Router/__Libraries/StellaOps.Microservice.AspNetCore/AGENTS.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### ROUTER-HDR-001 - Reproduce the live context persistence failure after the frontend contract fix
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: QA
|
||||||
|
Task description:
|
||||||
|
- Replay the Topology environment detail operator actions with live Playwright after the Web payload was corrected.
|
||||||
|
- Capture the remaining request/response behavior and determine whether the fault still sits in the gateway/bridge path or in Platform business logic.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Live Playwright reproduces the degraded banner after Topology operator actions.
|
||||||
|
- [x] Captured requests show the canonical payload emitted by Web.
|
||||||
|
- [x] Evidence isolates the remaining failure to Router-dispatched service execution rather than route discovery.
|
||||||
|
|
||||||
|
### ROUTER-HDR-002 - Restore case-insensitive forwarded header handling in the ASP.NET bridge
|
||||||
|
Status: DONE
|
||||||
|
Dependency: ROUTER-HDR-001
|
||||||
|
Owners: Developer
|
||||||
|
Task description:
|
||||||
|
- Update `AspNetRouterRequestDispatcher` so forwarded headers behave according to HTTP semantics after frame transport round-trips.
|
||||||
|
- Add a focused regression that round-trips a lowercase `content-type` header and verifies JSON request metadata survives dispatch.
|
||||||
|
- Ensure minimal-API request-body detection is populated so JSON body binding continues to work after Router frame dispatch.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Lowercase forwarded `content-type` reaches dispatched ASP.NET endpoints.
|
||||||
|
- [x] The regression fails on the broken behavior and passes with the fix.
|
||||||
|
- [x] The change remains scoped to Router bridge semantics without unrelated transport rewrites.
|
||||||
|
|
||||||
|
### ROUTER-HDR-003 - Rebuild Platform and verify the live topology operator actions
|
||||||
|
Status: DONE
|
||||||
|
Dependency: ROUTER-HDR-002
|
||||||
|
Owners: QA
|
||||||
|
Task description:
|
||||||
|
- Rebuild the `stellaops/platform:dev` image with the Router bridge fix, recreate only the Platform container, and replay the affected operator actions using Playwright.
|
||||||
|
- Confirm the degraded banner disappears and the downstream routes still render with the persisted environment context.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Live Playwright confirms the degraded banner is gone for `Open Targets`, `Open Agents`, `Open Runs`, and `Open Security Triage`.
|
||||||
|
- [x] `PUT /api/v2/context/preferences` succeeds on the live stack.
|
||||||
|
- [x] The downstream routes remain functional after context persistence updates.
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-03-07 | Sprint created after live Playwright replay on `https://stella-ops.local/setup/topology/environments/dev/posture` still showed `EVENTS: DEGRADED Failed to persist global context preferences.` even after the Web payload was reduced to canonical v2 fields. | QA |
|
||||||
|
| 2026-03-07 | Rebuilt only `platform` with the latest Router transport changes and replayed the live flow. The request path moved from gateway `404` to routed `400`, proving route registration was fixed and the remaining failure was inside dispatched request execution. | QA |
|
||||||
|
| 2026-03-07 | Live capture showed `PUT /api/v2/context/preferences` carrying canonical JSON while Router logs confirmed dispatch to `platform/1.0.0-alpha1`; the remaining suspicion is case-sensitive header rehydration dropping HTTP/2 lowercase `content-type` before ASP.NET body binding. | QA |
|
||||||
|
| 2026-03-07 | Added focused Router bridge regressions, including a real minimal-API JSON body-binding replay. The stronger test reproduced the same `400` until the dispatcher restored case-insensitive header lookup and populated ASP.NET request-body detection for frame-dispatched POST/PUT/PATCH requests. | Developer |
|
||||||
|
| 2026-03-07 | Rebuilt and recreated only `platform`, then replayed the four Topology operator actions with live Playwright. `PUT /api/v2/context/preferences` now returns `200` and the UI remains on `EVENTS: CONNECTED` for Targets, Agents, Runs, and Security Triage. | QA |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: fix the Router bridge at the header rehydration layer instead of widening Platform contracts or adding UI-side workarounds.
|
||||||
|
- Decision: include explicit ASP.NET request-body detection in the bridge because header casing alone was insufficient for minimal-API JSON binding after request-frame dispatch.
|
||||||
|
- Residual risk: other services using the same bridge should still be covered by broader Playwright/API sweeps because this fix addresses the common body-binding path but does not validate every endpoint surface.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- 2026-03-07: continue live Playwright QA sweeps now that Topology operator actions no longer degrade global context persistence.
|
||||||
@@ -17,6 +17,7 @@ Rollout policy: `docs/operations/multi-tenant-rollout-and-compatibility.md`
|
|||||||
- Microservices communicate with the Gateway using binary transports (TCP, TLS, UDP, RabbitMQ)
|
- Microservices communicate with the Gateway using binary transports (TCP, TLS, UDP, RabbitMQ)
|
||||||
- HTTP is not used for internal microservice-to-gateway traffic
|
- HTTP is not used for internal microservice-to-gateway traffic
|
||||||
- Request/response bodies are opaque to the router (raw bytes/streams)
|
- Request/response bodies are opaque to the router (raw bytes/streams)
|
||||||
|
- Forwarded HTTP headers remain case-insensitive across Router frame transport and ASP.NET bridge dispatch; lowercase HTTP/2 names such as `content-type` must be preserved for JSON-bound endpoints, and the ASP.NET bridge must mark POST/PUT/PATCH requests as body-capable so minimal-API JSON binding survives frame dispatch
|
||||||
|
|
||||||
### Transport Architecture
|
### Transport Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
httpRequest.QueryString = queryString;
|
httpRequest.QueryString = queryString;
|
||||||
httpRequest.Scheme = "https"; // Router always uses secure transport conceptually
|
httpRequest.Scheme = "https"; // Router always uses secure transport conceptually
|
||||||
httpRequest.Host = new HostString(_options.ServiceName);
|
httpRequest.Host = new HostString(_options.ServiceName);
|
||||||
|
httpRequest.Protocol = "HTTP/2";
|
||||||
|
|
||||||
// Copy headers
|
// Copy headers
|
||||||
foreach (var (key, value) in request.Headers)
|
foreach (var (key, value) in request.Headers)
|
||||||
@@ -189,13 +190,22 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
httpRequest.Body = new MemoryStream(request.Payload.ToArray());
|
httpRequest.Body = new MemoryStream(request.Payload.ToArray());
|
||||||
httpRequest.ContentLength = request.Payload.Length;
|
httpRequest.ContentLength = request.Payload.Length;
|
||||||
|
|
||||||
// Try to set Content-Type from headers
|
// HTTP header names are case-insensitive; browsers often emit lowercase
|
||||||
if (request.Headers.TryGetValue("Content-Type", out var contentType))
|
// names over HTTP/2, so read back from the copied header collection.
|
||||||
|
if (httpRequest.Headers.TryGetValue("Content-Type", out var contentType))
|
||||||
{
|
{
|
||||||
httpRequest.ContentType = contentType;
|
httpRequest.ContentType = contentType.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RouterRequestBodyDetectionFeature
|
||||||
|
{
|
||||||
|
CanHaveBody = request.Payload.Length > 0
|
||||||
|
|| HttpMethods.IsPost(request.Method)
|
||||||
|
|| HttpMethods.IsPut(request.Method)
|
||||||
|
|| HttpMethods.IsPatch(request.Method)
|
||||||
|
});
|
||||||
|
|
||||||
// Populate identity from StellaOps headers
|
// Populate identity from StellaOps headers
|
||||||
var identityResult = PopulateIdentity(httpContext, request.Headers);
|
var identityResult = PopulateIdentity(httpContext, request.Headers);
|
||||||
if (identityResult == IdentityPopulationResult.Rejected)
|
if (identityResult == IdentityPopulationResult.Rejected)
|
||||||
@@ -253,14 +263,14 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
{
|
{
|
||||||
failureReason = null;
|
failureReason = null;
|
||||||
|
|
||||||
if (!headers.TryGetValue(IdentityEnvelopeHeader, out var encodedEnvelope) ||
|
if (!TryGetHeaderValue(headers, IdentityEnvelopeHeader, out var encodedEnvelope) ||
|
||||||
string.IsNullOrWhiteSpace(encodedEnvelope))
|
string.IsNullOrWhiteSpace(encodedEnvelope))
|
||||||
{
|
{
|
||||||
failureReason = "envelope header missing";
|
failureReason = "envelope header missing";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!headers.TryGetValue(IdentityEnvelopeSignatureHeader, out var encodedSignature) ||
|
if (!TryGetHeaderValue(headers, IdentityEnvelopeSignatureHeader, out var encodedSignature) ||
|
||||||
string.IsNullOrWhiteSpace(encodedSignature))
|
string.IsNullOrWhiteSpace(encodedSignature))
|
||||||
{
|
{
|
||||||
failureReason = "envelope signature header missing";
|
failureReason = "envelope signature header missing";
|
||||||
@@ -357,26 +367,26 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
var claims = new List<Claim>();
|
var claims = new List<Claim>();
|
||||||
|
|
||||||
// Actor (subject/user ID)
|
// Actor (subject/user ID)
|
||||||
if (headers.TryGetValue(ActorHeader, out var actor) && !string.IsNullOrEmpty(actor))
|
if (TryGetHeaderValue(headers, ActorHeader, out var actor) && !string.IsNullOrEmpty(actor))
|
||||||
{
|
{
|
||||||
claims.Add(new Claim(ClaimTypes.NameIdentifier, actor));
|
claims.Add(new Claim(ClaimTypes.NameIdentifier, actor));
|
||||||
claims.Add(new Claim("sub", actor));
|
claims.Add(new Claim("sub", actor));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tenant
|
// Tenant
|
||||||
if (headers.TryGetValue(TenantHeader, out var tenant) && !string.IsNullOrEmpty(tenant))
|
if (TryGetHeaderValue(headers, TenantHeader, out var tenant) && !string.IsNullOrEmpty(tenant))
|
||||||
{
|
{
|
||||||
claims.Add(new Claim("tenant", tenant));
|
claims.Add(new Claim("tenant", tenant));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
if (headers.TryGetValue(SessionHeader, out var session) && !string.IsNullOrEmpty(session))
|
if (TryGetHeaderValue(headers, SessionHeader, out var session) && !string.IsNullOrEmpty(session))
|
||||||
{
|
{
|
||||||
claims.Add(new Claim("session", session));
|
claims.Add(new Claim("session", session));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scopes (space-separated)
|
// Scopes (space-separated)
|
||||||
if (headers.TryGetValue(ScopesHeader, out var scopes) && !string.IsNullOrEmpty(scopes))
|
if (TryGetHeaderValue(headers, ScopesHeader, out var scopes) && !string.IsNullOrEmpty(scopes))
|
||||||
{
|
{
|
||||||
foreach (var scope in scopes.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
foreach (var scope in scopes.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
@@ -385,7 +395,7 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Roles (space-separated)
|
// Roles (space-separated)
|
||||||
if (headers.TryGetValue(RolesHeader, out var roles) && !string.IsNullOrEmpty(roles))
|
if (TryGetHeaderValue(headers, RolesHeader, out var roles) && !string.IsNullOrEmpty(roles))
|
||||||
{
|
{
|
||||||
foreach (var role in roles.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
foreach (var role in roles.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
@@ -400,6 +410,29 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetHeaderValue(
|
||||||
|
IReadOnlyDictionary<string, string> headers,
|
||||||
|
string name,
|
||||||
|
out string value)
|
||||||
|
{
|
||||||
|
if (headers.TryGetValue(name, out value!))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (key, candidate) in headers)
|
||||||
|
{
|
||||||
|
if (string.Equals(key, name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma warning disable CS1998
|
#pragma warning disable CS1998
|
||||||
private async Task<RouteEndpoint?> MatchEndpointAsync(HttpContext httpContext)
|
private async Task<RouteEndpoint?> MatchEndpointAsync(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
@@ -768,6 +801,11 @@ public sealed class AspNetRouterRequestDispatcher : IAspNetRouterRequestDispatch
|
|||||||
public RouteValueDictionary RouteValues { get; set; } = new();
|
public RouteValueDictionary RouteValues { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class RouterRequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature
|
||||||
|
{
|
||||||
|
public bool CanHaveBody { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populates the IStellaOpsTenantAccessor. This mimics what StellaOpsTenantMiddleware does
|
/// Populates the IStellaOpsTenantAccessor. This mimics what StellaOpsTenantMiddleware does
|
||||||
/// in the normal HTTP pipeline: resolves the tenant from claims/headers and sets it
|
/// in the normal HTTP pipeline: resolves the tenant from claims/headers and sets it
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
|||||||
| AUDIT-0388-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Microservice.AspNetCore. |
|
| AUDIT-0388-T | DONE | Revalidated 2026-01-07; test coverage audit for StellaOps.Microservice.AspNetCore. |
|
||||||
| AUDIT-0388-A | TODO | Revalidated 2026-01-07 (open findings). |
|
| AUDIT-0388-A | TODO | Revalidated 2026-01-07 (open findings). |
|
||||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||||
|
| HDR-CASE-01 | DONE | Sprint `docs/implplan/SPRINT_20260307_011_Router_case_insensitive_forwarded_header_binding.md`: restored case-insensitive forwarded header handling in the ASP.NET bridge so lowercase HTTP/2 `content-type` reaches JSON-bound endpoints. |
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Matching;
|
using Microsoft.AspNetCore.Routing.Matching;
|
||||||
@@ -103,6 +104,100 @@ public sealed class AspNetRouterRequestDispatcherTests
|
|||||||
Assert.Equal("alice", Encoding.UTF8.GetString(response.Payload.ToArray()));
|
Assert.Equal("alice", Encoding.UTF8.GetString(response.Payload.ToArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_RoundTrippedLowercaseContentType_PreservesJsonRequestMetadata()
|
||||||
|
{
|
||||||
|
var routeEndpoint = new RouteEndpoint(
|
||||||
|
async context =>
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||||
|
var body = await reader.ReadToEndAsync();
|
||||||
|
var isJson = string.Equals(context.Request.ContentType, "application/json", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
context.Response.StatusCode = isJson && body == "{\"name\":\"stella\"}"
|
||||||
|
? StatusCodes.Status200OK
|
||||||
|
: StatusCodes.Status400BadRequest;
|
||||||
|
},
|
||||||
|
RoutePatternFactory.Parse("/api/v1/context/preferences"),
|
||||||
|
order: 0,
|
||||||
|
new EndpointMetadataCollection(new HttpMethodMetadata(["PUT"])),
|
||||||
|
displayName: "Preferences");
|
||||||
|
|
||||||
|
var dispatcher = CreateDispatcher(
|
||||||
|
routeEndpoint,
|
||||||
|
new StellaRouterBridgeOptions
|
||||||
|
{
|
||||||
|
ServiceName = "platform",
|
||||||
|
Version = "1.0.0-alpha1",
|
||||||
|
Region = "local",
|
||||||
|
AuthorizationTrustMode = GatewayAuthorizationTrustMode.ServiceEnforced
|
||||||
|
});
|
||||||
|
|
||||||
|
var transportFrame = FrameConverter.ToFrame(new RequestFrame
|
||||||
|
{
|
||||||
|
RequestId = "req-3",
|
||||||
|
Method = "PUT",
|
||||||
|
Path = "/api/v1/context/preferences",
|
||||||
|
Headers = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["content-type"] = "application/json"
|
||||||
|
},
|
||||||
|
Payload = Encoding.UTF8.GetBytes("{\"name\":\"stella\"}")
|
||||||
|
});
|
||||||
|
|
||||||
|
var roundTrippedRequest = FrameConverter.ToRequestFrame(transportFrame);
|
||||||
|
Assert.NotNull(roundTrippedRequest);
|
||||||
|
|
||||||
|
var response = await dispatcher.DispatchAsync(roundTrippedRequest!);
|
||||||
|
|
||||||
|
Assert.Equal(StatusCodes.Status200OK, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_RoundTrippedLowercaseContentType_BindsMinimalApiJsonBody()
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder();
|
||||||
|
var app = builder.Build();
|
||||||
|
app.MapPut("/api/v1/context/preferences", (PreferencesBody body) => Results.Ok(body));
|
||||||
|
|
||||||
|
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 = "platform",
|
||||||
|
Version = "1.0.0-alpha1",
|
||||||
|
Region = "local",
|
||||||
|
AuthorizationTrustMode = GatewayAuthorizationTrustMode.ServiceEnforced
|
||||||
|
},
|
||||||
|
NullLogger<AspNetRouterRequestDispatcher>.Instance);
|
||||||
|
|
||||||
|
var transportFrame = FrameConverter.ToFrame(new RequestFrame
|
||||||
|
{
|
||||||
|
RequestId = "req-4",
|
||||||
|
Method = "PUT",
|
||||||
|
Path = "/api/v1/context/preferences",
|
||||||
|
Headers = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["content-type"] = "application/json"
|
||||||
|
},
|
||||||
|
Payload = Encoding.UTF8.GetBytes("{\"regions\":[],\"environments\":[\"dev\"],\"timeWindow\":\"24h\"}")
|
||||||
|
});
|
||||||
|
|
||||||
|
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.Status200OK, response.StatusCode);
|
||||||
|
Assert.Contains("\"environments\":[\"dev\"]", responseBody, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("\"timeWindow\":\"24h\"", responseBody, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static AspNetRouterRequestDispatcher CreateDispatcher(RouteEndpoint endpoint, StellaRouterBridgeOptions options)
|
private static AspNetRouterRequestDispatcher CreateDispatcher(RouteEndpoint endpoint, StellaRouterBridgeOptions options)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
@@ -130,4 +225,6 @@ public sealed class AspNetRouterRequestDispatcherTests
|
|||||||
{
|
{
|
||||||
public override Task SelectAsync(HttpContext httpContext, CandidateSet candidates) => Task.CompletedTask;
|
public override Task SelectAsync(HttpContext httpContext, CandidateSet candidates) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record PreferencesBody(string[] Regions, string[] Environments, string TimeWindow);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
|||||||
| RVM-02 | DONE | Added `AddRouterMicroservice()` DI tests for disabled mode, gateway validation, TCP registration, and Valkey messaging options wiring via plugin-based transport activation; extended ASP.NET discovery tests for OpenAPI metadata/schema extraction and typed response-schema selection fallback. |
|
| RVM-02 | DONE | Added `AddRouterMicroservice()` DI tests for disabled mode, gateway validation, TCP registration, and Valkey messaging options wiring via plugin-based transport activation; extended ASP.NET discovery tests for OpenAPI metadata/schema extraction and typed response-schema selection fallback. |
|
||||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Router/__Tests/StellaOps.Router.AspNet.Tests/StellaOps.Router.AspNet.Tests.md. |
|
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Router/__Tests/StellaOps.Router.AspNet.Tests/StellaOps.Router.AspNet.Tests.md. |
|
||||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||||
|
| HDR-CASE-01 | DONE | Added the lowercase `content-type` request-frame regression so JSON body dispatch survives Router frame round-trips. |
|
||||||
|
|||||||
Reference in New Issue
Block a user