test(client): refactor InjectTestUser as a real IMiddleware

Move the X-Test-Role-to-User promotion out of an inline
RequestDelegate and into a proper IMiddleware implementation,
wired through IStartupFilter so it lands after the production
UseAuthentication/UseAuthorization in the request pipeline.

The previous app.Use(...) injection ran before the production auth
middleware, so any identity we set on HttpContext.User was being
overwritten by the next middleware. Wrapping the production
pipeline in TestUserStartupFilter.Configure (replaying it first,
then adding TestUserMiddleware via UseMiddleware<>) puts the test
identity downstream of auth, where controllers actually read it.

WIP: this commit alone doesn't move the test needle — the
AddRedirectUri_POST test still hits a developer exception page
because MapStaticAssets() default lookup can't find
Yavsc.Org.Tests.staticwebassets.endpoints.json in the test bin.
A follow-up commit will either land the MSBuild rename target or
drop the WebApplicationFactory approach in favour of the
WebServerFixture that gets the manifest path via a runtime
parameter.
This commit is contained in:
Paul Schneider 2026-06-21 21:24:44 +01:00
commit afd02ab5aa
3 changed files with 102 additions and 39 deletions

View file

@ -1,11 +1,8 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Yavsc.Org.Tests;
@ -19,10 +16,10 @@ namespace Yavsc.Org.Tests;
/// <c>[Authorize("AdministratorOnly")]</c> (and any other policy
/// requiring a role) is satisfied by sending an
/// <c>X-Test-Role: Administrator</c> header, without a real login.
/// Also injects a middleware that promotes the same header into a
/// real <see cref="ClaimsPrincipal"/> on <c>HttpContext.User</c> so
/// that user code reading <c>User.GetUserId()</c> sees a logged-in
/// identity.
/// Also adds <see cref="TestUserStartupFilter"/> which installs
/// <see cref="TestUserMiddleware"/> so that <c>User.GetUserId()</c>
/// in user code sees a logged-in identity derived from the same
/// header.
/// </summary>
public class TestWebApplicationFactory : WebApplicationFactory<Program>
{
@ -39,39 +36,12 @@ public class TestWebApplicationFactory : WebApplicationFactory<Program>
// the test one. The default registered by AddAuthorization
// becomes irrelevant: any GetPolicyAsync call is routed here.
services.AddSingleton<IAuthorizationPolicyProvider, TestAuthPolicyProvider>();
});
// Promote the X-Test-Role header to an authenticated identity
// on the request, so anything that reads User.GetUserId() (or
// any other claim-based helper) downstream sees a logged-in
// user. The policy provider above only short-circuits
// [Authorize(...)] checks; it does not touch HttpContext.User.
builder.Configure(app =>
{
app.Use(InjectTestUser);
// Register the test middleware and its startup filter.
// The startup filter wraps the production pipeline so
// TestUserMiddleware runs after UseAuthentication/Authorization.
services.AddTransient<TestUserMiddleware>();
services.AddTransient<IStartupFilter, TestUserStartupFilter>();
});
}
private static RequestDelegate InjectTestUser(RequestDelegate next)
{
return async ctx =>
{
var role = ctx.Request.Headers[TestAuthPolicyProvider.HeaderName].ToString();
if (!string.IsNullOrEmpty(role) &&
(ctx.User.Identity is null || !ctx.User.Identity.IsAuthenticated))
{
var identity = new ClaimsIdentity(
new[]
{
new Claim(
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
role),
new Claim(ClaimTypes.NameIdentifier, "test-user"),
},
authenticationType: "TestAuth");
ctx.User = new ClaimsPrincipal(identity);
}
await next(ctx);
};
}
}