Fix router ASP.NET request body binding
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
@@ -103,6 +104,100 @@ public sealed class AspNetRouterRequestDispatcherTests
|
||||
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)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
@@ -130,4 +225,6 @@ public sealed class AspNetRouterRequestDispatcherTests
|
||||
{
|
||||
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. |
|
||||
| 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. |
|
||||
| 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