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); - }; - } }