yavsc/src/Yavsc.Blogs.Tests/Fixtures/MappedClaimsBlogsWebServerFixture.cs
Paul Schneider a12830e49b
Some checks failed
Forgejo Release / release (push) Failing after 6m42s
Dotnet build and test / build (pull_request) Failing after 5m30s
test(blogs): configure SiteSettings.Blog in MappedClaimsBlogsWebServerFixture
BlogSpotService's constructor throws InvalidOperationException
("SiteSettings.Blog is not configured.") when IOptions<SiteSettings>.Blog
is null, so every request to BlogApiController 500'd resolving the
service — all 3 BlogApiMappedClaimsTests were failing with
InternalServerError.

The mapped-claims fixture never configured SiteSettings (the main
BlogsWebServerFixture does, pointing Blog at a per-run temp dir).
Mirror that: Configure<SiteSettings>(s => s.Blog = <temp dir>)
before AddScoped<BlogSpotService>(). These tests are JSON-only
(AttachFiles short-circuits), but the service must be constructible.

Verified: BlogApiMappedClaimsTests 3/3 green; full Yavsc.Blogs.Tests
37/37 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-19 14:14:28 +01:00

122 lines
4.7 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Yavsc.Blogs.Controllers;
using Yavsc.Models;
using Yavsc.Services;
using Yavsc.Tests.Shared;
namespace Yavsc.Blogs.Tests;
/// <summary>
/// Dedicated integration-test host that mirrors the production JWT
/// remapping behavior: MapInboundClaims remains enabled and the
/// default inbound map rewrites "sub" to ClaimTypes.NameIdentifier.
/// This is the closest in-process reproduction of the production
/// authentication surface for the blog API.
/// </summary>
public sealed class MappedClaimsBlogsWebServerFixture : IDisposable, IBackendFixture
{
private readonly InMemoryDatabaseRoot _inMemoryRoot = new();
private readonly Dictionary<string, string> _savedInboundMap;
private WebApplication? _app = null;
public MappedClaimsBlogsWebServerFixture()
{
_savedInboundMap = new Dictionary<string, string>(JwtSecurityTokenHandler.DefaultInboundClaimTypeMap);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap["sub"] = ClaimTypes.NameIdentifier;
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls("http://127.0.0.1:5104");
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseInMemoryDatabase("Yavsc.Blogs.Tests.MappedClaims", _inMemoryRoot));
builder.Services.AddSingleton<IFileSystemAuthManager>(new NoopFileSystemAuthManager());
// BlogSpotService's constructor throws InvalidOperationException
// ("SiteSettings.Blog is not configured.") when Blog is null, so
// every request hitting BlogApiController would 500 resolving the
// service. These tests never upload files (JSON-only POST/PUT), so
// AttachFiles short-circuits — but the service still has to be
// constructible. Point Blog at a per-run temp directory, mirroring
// BlogsWebServerFixture.
builder.Services.Configure<SiteSettings>(s =>
{
s.Blog = Path.Combine(Path.GetTempPath(), "postit-blogs-mappedclaims-" + Guid.NewGuid().ToString("N"));
});
builder.Services.AddScoped<BlogSpotService>();
builder.Services.AddScoped<IAuthorizationHandler, PermissionHandler>();
builder.Services.AddControllers()
.AddApplicationPart(typeof(BlogApiController).Assembly);
builder.Services.AddAuthorization(opt =>
{
opt.AddPolicy("BlogScope", policy =>
{
policy.RequireAuthenticatedUser()
.RequireClaim("scope", "blogs");
});
});
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.IncludeErrorDetails = true;
options.MapInboundClaims = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = TestTokenIssuer.Issuer,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = TestTokenIssuer.SigningKey,
RoleClaimType = Yavsc.Constants.RoleClaimType,
NameClaimType = Yavsc.Constants.NameClaimType,
};
});
_app = builder.Build();
_app.UseRouting();
_app.UseAuthentication();
_app.UseAuthorization();
_app.MapControllers();
_app.StartAsync().GetAwaiter().GetResult();
Addresses = ["http://127.0.0.1:5104"];
Services = _app.Services;
}
public IReadOnlyList<string> Addresses { get; }
public IServiceProvider Services { get; }
public void Dispose()
{
if (_app is null) return;
_app.StopAsync().GetAwaiter().GetResult();
_app.DisposeAsync().AsTask().GetAwaiter().GetResult();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
foreach (var kvp in _savedInboundMap)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[kvp.Key] = kvp.Value;
}
}
private sealed class NoopFileSystemAuthManager : IFileSystemAuthManager
{
public FileAccessRight GetFilePathAccess(System.Security.Claims.ClaimsPrincipal user, string fileRelativePath)
=> FileAccessRight.None;
public void SetAccess(long circleId, string normalizedFullPath, FileAccessRight access)
{
}
}
}