diff --git a/src/Yavsc.Org.Tests/TestUserMiddleware.cs b/src/Yavsc.Org.Tests/TestUserMiddleware.cs
new file mode 100644
index 00000000..c6348625
--- /dev/null
+++ b/src/Yavsc.Org.Tests/TestUserMiddleware.cs
@@ -0,0 +1,46 @@
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Yavsc.Org.Tests;
+
+///
+/// Middleware that promotes the X-Test-Role request header to
+/// an authenticated on
+/// . Lets downstream code (controllers,
+/// services) call User.GetUserId() and other claim-based
+/// helpers as if a real login had happened, while
+/// independently short-circuits
+/// [Authorize(...)] policy checks.
+///
+/// This middleware is added to the pipeline by
+/// so it runs after the
+/// production authentication middleware; setting User before
+/// UseAuthentication would have it overwritten on the next
+/// middleware.
+///
+public class TestUserMiddleware : IMiddleware
+{
+ public const string UserId = "test-user";
+
+ public Task InvokeAsync(HttpContext context, RequestDelegate next)
+ {
+ var role = context.Request.Headers[TestAuthPolicyProvider.HeaderName].ToString();
+ if (!string.IsNullOrEmpty(role) &&
+ (context.User.Identity is null || !context.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, UserId),
+ },
+ authenticationType: "TestAuth");
+ context.User = new ClaimsPrincipal(identity);
+ }
+ return next(context);
+ }
+}
diff --git a/src/Yavsc.Org.Tests/TestUserStartupFilter.cs b/src/Yavsc.Org.Tests/TestUserStartupFilter.cs
new file mode 100644
index 00000000..53439ed9
--- /dev/null
+++ b/src/Yavsc.Org.Tests/TestUserStartupFilter.cs
@@ -0,0 +1,47 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+
+namespace Yavsc.Org.Tests;
+
+///
+/// Startup filter that injects after
+/// the authentication and authorization middleware. The
+/// contract wraps the existing
+/// pipeline: the next delegate is the rest of the app's
+/// pipeline, so we run our middleware before it but
+/// after anything that was registered as a startup filter
+/// earlier in the chain.
+///
+/// In practice this puts TestUserMiddleware ahead of
+/// UseAuthentication (registered inside ConfigurePipeline)
+/// because the production code path runs UseAuthentication
+/// synchronously inside Configure, after all startup filters
+/// have wrapped it. We want the opposite: the test identity must be
+/// visible to authorization and the controller, so we register
+/// TestUserMiddleware via
+/// inside the filter such that it runs late in the chain. The
+/// simplest way to achieve that is to register the middleware
+/// after the production authorization pipeline: we wrap with our
+/// middleware inside the filter, so our delegate sits between the
+/// framework middleware (set up by Configure) and the rest of the
+/// pipeline — meaning requests flow:
+/// framework authN/authZ → TestUserMiddleware → next pipeline.
+///
+public class TestUserStartupFilter : IStartupFilter
+{
+ public Action Configure(Action next)
+ {
+ return app =>
+ {
+ // Replay the production pipeline first (this is what
+ // Program.Main + ConfigurePipeline set up, including
+ // UseAuthentication and UseAuthorization).
+ next(app);
+ // Then add our middleware on top. UseMiddleware wires
+ // it through the same IMiddlewareActivator the framework
+ // uses, so the dependency on TestUserMiddleware is
+ // resolved from the request scope.
+ app.UseMiddleware();
+ };
+ }
+}
diff --git a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs
index c62f7f8a..49d3b663 100644
--- a/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs
+++ b/src/Yavsc.Org.Tests/TestWebApplicationFactory.cs
@@ -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;
/// [Authorize("AdministratorOnly")] (and any other policy
/// requiring a role) is satisfied by sending an
/// X-Test-Role: Administrator header, without a real login.
-/// Also injects a middleware that promotes the same header into a
-/// real on HttpContext.User so
-/// that user code reading User.GetUserId() sees a logged-in
-/// identity.
+/// Also adds which installs
+/// so that User.GetUserId()
+/// in user code sees a logged-in identity derived from the same
+/// header.
///
public class TestWebApplicationFactory : WebApplicationFactory
{
@@ -39,39 +36,12 @@ public class TestWebApplicationFactory : WebApplicationFactory
// the test one. The default registered by AddAuthorization
// becomes irrelevant: any GetPolicyAsync call is routed here.
services.AddSingleton();
- });
- // 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();
+ services.AddTransient();
});
}
-
- 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);
- };
- }
}