test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
using System.Text;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
2026-07-06 21:49:53 +01:00
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
2026-07-06 21:49:53 +01:00
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
using Microsoft.IdentityModel.Tokens;
|
2026-07-06 21:58:14 +01:00
|
|
|
using Yavsc.Blogs.Controllers;
|
2026-07-06 21:49:53 +01:00
|
|
|
using Yavsc.Models;
|
|
|
|
|
using Yavsc.Services;
|
|
|
|
|
using Yavsc.Tests.Shared;
|
|
|
|
|
|
|
|
|
|
namespace Yavsc.Blogs.Tests;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Test host for the Yavsc.Blogs API surface. Specialisation of
|
|
|
|
|
/// <see cref="WebHostFixture"/> that wires up only the bits the
|
|
|
|
|
/// blog API actually depends on:
|
|
|
|
|
///
|
|
|
|
|
/// <list type="bullet">
|
|
|
|
|
/// <item><description>An in-memory <see cref="ApplicationDbContext"/>
|
|
|
|
|
/// (the real one — no mock) so <c>BlogSpotService.Index</c> can run
|
|
|
|
|
/// against an empty table and return an empty list.</description></item>
|
|
|
|
|
/// <item><description>A trivial <see cref="IFileSystemAuthManager"/>
|
|
|
|
|
/// stub: the GET index path doesn't read the file system, so any
|
|
|
|
|
/// implementation is fine.</description></item>
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
/// <item><description>The real <c>BlogSpotService</c>, which calls
|
|
|
|
|
/// <c>IAuthorizationService.AuthorizeAsync(user, blog, new EditPermission())</c>
|
|
|
|
|
/// on PUT. The fixture registers the real
|
|
|
|
|
/// <see cref="PermissionHandler"/> so the resource-based ownership
|
|
|
|
|
/// check runs end-to-end; tests that want a 204 PUT must sign a
|
|
|
|
|
/// JWT whose <c>sub</c> matches the post's <c>AuthorId</c>.</description></item>
|
|
|
|
|
/// <item><description>A real <c>AddJwtBearer</c> with HS256,
|
|
|
|
|
/// sharing its <see cref="TestTokenIssuer.SigningKey"/> with the
|
|
|
|
|
/// token issuer. The production OIDC discovery path is bypassed:
|
|
|
|
|
/// the test host validates tokens locally, against the static
|
|
|
|
|
/// signing key, so no IdP is required to exercise auth.</description></item>
|
|
|
|
|
/// <item><description>The production <c>BlogScope</c> policy
|
|
|
|
|
/// (RequireAuthenticatedUser + RequireClaim("scope", "blogs"))
|
|
|
|
|
/// registered verbatim. Tests that omit the bearer header exercise
|
|
|
|
|
/// the unauthenticated path and get 401.</description></item>
|
2026-07-06 21:49:53 +01:00
|
|
|
/// </list>
|
|
|
|
|
///
|
|
|
|
|
/// No IdentityServer, no SMTP, no static assets — the Org fixture
|
|
|
|
|
/// owns all of that and we don't need any of it for blog integration
|
|
|
|
|
/// tests.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class BlogsWebServerFixture : WebHostFixture
|
|
|
|
|
{
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
private InMemoryDatabaseRoot? _inMemoryRoot;
|
|
|
|
|
|
2026-07-06 21:49:53 +01:00
|
|
|
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
|
|
|
|
{
|
|
|
|
|
// Use the real ApplicationDbContext with an in-memory store.
|
|
|
|
|
// BlogSpotService reads _context.BlogSpot directly, so any
|
|
|
|
|
// attempt to mock it would be wasted work; the real service
|
|
|
|
|
// against an empty table returns an empty list, which is
|
|
|
|
|
// exactly what the first test wants to assert.
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
//
|
|
|
|
|
// Share a single InMemoryDatabaseRoot across the test
|
|
|
|
|
// lifetime so POST + GET on the same fixture see the same
|
|
|
|
|
// store. Without the root, EF Core's In-Memory provider
|
|
|
|
|
// creates independent stores per DbContext in some
|
|
|
|
|
// configurations, and the second request would see an
|
|
|
|
|
// empty list even after the first wrote a row.
|
|
|
|
|
_inMemoryRoot = new InMemoryDatabaseRoot();
|
2026-07-06 21:49:53 +01:00
|
|
|
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
opt.UseInMemoryDatabase("Yavsc.Blogs.Tests", _inMemoryRoot));
|
2026-07-06 21:49:53 +01:00
|
|
|
|
|
|
|
|
// Trivial file-system auth: the GET index path never calls
|
|
|
|
|
// into it, but the DI container needs an instance.
|
|
|
|
|
builder.Services.AddSingleton<IFileSystemAuthManager>(
|
|
|
|
|
new NoopFileSystemAuthManager());
|
|
|
|
|
|
|
|
|
|
// Real BlogSpotService — same instance the production host
|
|
|
|
|
// builds (ApplicationDbContext, IAuthorizationService,
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
// IFileSystemAuthManager). With PermissionHandler registered
|
|
|
|
|
// below, Modify() now answers "is the caller the author of
|
|
|
|
|
// the post?" for real, which is exactly what we want to
|
|
|
|
|
// assert in the PUT tests.
|
2026-07-06 21:49:53 +01:00
|
|
|
builder.Services.AddScoped<BlogSpotService>();
|
|
|
|
|
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
// The real PermissionHandler: BlogSpotService calls
|
|
|
|
|
// IAuthorizationService.AuthorizeAsync(user, blog, new
|
|
|
|
|
// EditPermission()) on Modify, and PermissionHandler
|
|
|
|
|
// resolves it via IsOwner(user, blog) — i.e. blog.AuthorId
|
|
|
|
|
// == user.GetUserId(). To PUT a post, the test JWT must
|
|
|
|
|
// carry sub == post.AuthorId.
|
|
|
|
|
builder.Services.AddScoped<IAuthorizationHandler, PermissionHandler>();
|
|
|
|
|
|
2026-07-06 21:58:14 +01:00
|
|
|
// The BlogApiController is reached through MVC. AddControllers()
|
|
|
|
|
// by default scans the test assembly only; we explicitly add the
|
|
|
|
|
// Yavsc.Blogs application part so the controller is discovered
|
|
|
|
|
// and routed.
|
|
|
|
|
builder.Services.AddControllers()
|
|
|
|
|
.AddApplicationPart(typeof(BlogApiController).Assembly);
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
|
|
|
|
|
// Production BlogScope policy, verbatim. Two requirements:
|
|
|
|
|
// 1. RequireAuthenticatedUser: a request with no bearer
|
|
|
|
|
// token (or an invalid one) will be rejected.
|
|
|
|
|
// 2. RequireClaim("scope", "blogs"): the JWT must carry a
|
|
|
|
|
// "scope" claim whose value is "blogs".
|
|
|
|
|
// TestTokenIssuer.Issue() defaults to scope=blogs; the
|
|
|
|
|
// GetBlog_returns_401_when_no_token test omits the token
|
|
|
|
|
// entirely and asserts the policy fails closed.
|
2026-07-06 21:49:53 +01:00
|
|
|
builder.Services.AddAuthorization(opt =>
|
|
|
|
|
{
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
opt.AddPolicy("BlogScope", policy =>
|
|
|
|
|
{
|
|
|
|
|
policy.RequireAuthenticatedUser()
|
|
|
|
|
.RequireClaim("scope", "blogs");
|
|
|
|
|
});
|
2026-07-06 21:49:53 +01:00
|
|
|
});
|
|
|
|
|
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
// Real JWT Bearer authentication, sharing the signing key
|
|
|
|
|
// with TestTokenIssuer. No Authority → no OIDC discovery,
|
|
|
|
|
// no IdP roundtrip; the middleware validates the signature
|
|
|
|
|
// and the standard claims against the static configuration
|
|
|
|
|
// below. Production uses AddYavscJwtBearer with an IdP, but
|
|
|
|
|
// for the unit-test host that path is unwanted coupling.
|
|
|
|
|
builder.Services.AddAuthentication("Bearer")
|
|
|
|
|
.AddJwtBearer("Bearer", options =>
|
|
|
|
|
{
|
|
|
|
|
options.IncludeErrorDetails = true;
|
|
|
|
|
// MapInboundClaims = false here mirrors the
|
|
|
|
|
// JwtSecurityTokenHandler.DefaultInboundClaimTypeMap
|
|
|
|
|
// .Clear() in TestTokenIssuer: the validation
|
|
|
|
|
// pipeline must not rewrite "sub" to
|
|
|
|
|
// ClaimTypes.NameIdentifier, otherwise the
|
|
|
|
|
// PermissionHandler ownership check sees a null
|
|
|
|
|
// user id and rejects every PUT.
|
|
|
|
|
options.MapInboundClaims = false;
|
|
|
|
|
options.TokenValidationParameters = new TokenValidationParameters
|
|
|
|
|
{
|
|
|
|
|
ValidateIssuer = true,
|
|
|
|
|
ValidIssuer = TestTokenIssuer.Issuer,
|
|
|
|
|
ValidateAudience = false,
|
|
|
|
|
ValidateLifetime = true,
|
|
|
|
|
ValidateIssuerSigningKey = true,
|
|
|
|
|
IssuerSigningKey = TestTokenIssuer.SigningKey,
|
|
|
|
|
// "sub" stays "sub" (MapInboundClaims only
|
|
|
|
|
// remaps long Microsoft claim URIs, not sub).
|
|
|
|
|
// UserHelpers.GetUserId reads sub directly.
|
|
|
|
|
NameClaimType = "sub",
|
|
|
|
|
RoleClaimType = YavscConstants.RoleClaimType,
|
|
|
|
|
};
|
|
|
|
|
});
|
2026-07-06 21:49:53 +01:00
|
|
|
|
|
|
|
|
return builder.Build();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task<WebApplication> ConfigurePipelineAsync(WebApplication app)
|
|
|
|
|
{
|
|
|
|
|
app.UseRouting();
|
|
|
|
|
app.UseAuthentication();
|
|
|
|
|
app.UseAuthorization();
|
|
|
|
|
app.MapControllers();
|
|
|
|
|
await Task.CompletedTask;
|
|
|
|
|
return app;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Trivial <see cref="IFileSystemAuthManager"/> stub. The
|
|
|
|
|
/// blog API endpoints exercised by the first tests don't read the
|
|
|
|
|
/// file system, so the implementation can be a no-op.</summary>
|
|
|
|
|
private sealed class NoopFileSystemAuthManager : IFileSystemAuthManager
|
|
|
|
|
{
|
test(blogs): real JwtBearer in fixture, drop X-Test-Role bypass
Wire the Blogs integration test host with a real AddJwtBearer
(HS256, IssuerSigningKey shared with the new TestTokenIssuer) and
the production BlogScope policy verbatim, instead of the
TestAuthPolicyProvider / AllowAllAuthorizationService /
NoopAuthHandler stack that short-circuited every authorization
check.
Why: BlogSpotService.Modify calls
IAuthorizationService.AuthorizeAsync(user, blog, EditPermission);
the previous AllowAllAuthorizationService stub made that a
no-op, so the tests could not exercise the real ownership chain
and any change in PermissionHandler would silently slip through.
The new test host registers the real PermissionHandler, so a PUT
that succeeds (204) is now proof that PermissionHandler.IsOwner
accepted the request — i.e. the JWT's sub matched the post's
AuthorId, end-to-end.
Notes for future-me:
- JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear() is
called once on the first Issue() to keep the 'sub' claim
literal; without it UserHelpers.GetUserId (which reads 'sub')
gets ClaimTypes.NameIdentifier instead, returns null, and the
owner check fails for every PUT. The companion
options.MapInboundClaims = false on the validation pipeline
keeps both sides in sync.
- Production still uses AddYavscJwtBearer against the OIDC
authority; the test-only HS256 path is local to the test
process and never crosses a network boundary.
Coverage:
- GetBlog_returns_401_when_no_token_is_provided — anonymous
request, real policy fails closed.
- PutBlog_with_valid_token_and_owner_returns_204_and_Get_
reflects_update — POST then PUT then GET, all behind a real
JWT, asserting 204 + list contains the updated title.
Packages added to Directory.Packages.props at 8.2.1 to match
what Microsoft.AspNetCore.Authentication.JwtBearer 10.0.9 already
transitively pulls in (no version drift).
2026-07-06 23:29:51 +01:00
|
|
|
public FileAccessRight GetFilePathAccess(System.Security.Claims.ClaimsPrincipal user, string fileRelativePath)
|
2026-07-06 21:49:53 +01:00
|
|
|
=> FileAccessRight.None;
|
|
|
|
|
|
|
|
|
|
public void SetAccess(long circleId, string normalizedFullPath, FileAccessRight access)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|