yavsc/src/Yavsc.Org.Tests/NonRegression/BillingServiceTests.cs

56 lines
2.3 KiB
C#
Raw Normal View History

using Microsoft.EntityFrameworkCore;
using Yavsc.Abstract.Workflow;
using Yavsc.Helpers;
using Yavsc.Models;
using Yavsc.Models.Haircut;
using Yavsc.Services;
using Xunit;
2026-06-19 19:59:15 +01:00
namespace Yavsc
{
Server: wire ProfileService so access tokens carry the name claim The custom ProfileService (Yavsc.Services.ProfileService) was never registered with IdentityServer, so IS8 fell back to its default profile service — which only emits claims listed in each API resource's UserClaims (RequestedClaimTypes). "name" was never among them, so every access token lacked "name" and GetUserName() resolved to null on all bearer resource hosts (Blogs/Api): the MyFiles upload 500 and the unreachable User.Identity.Name. The class was dead code; all prior tweaks to it (FullName→UserName) and to ServiceExtensions (MapInboundClaims=false) were necessary but insufficient until it was actually wired in. - HostingExtensions: AddProfileService<ProfileService>() after AddAspNetIdentity<ApplicationUser>(). - ProfileService: emit name=user.UserName, email, roles for any user grant whose client has "profile" in AllowedScopes, independent of the caller (the old ParsedScopes gate only fired for the id-token caller, never the access-token caller). - ServiceExtensions: JwtBearer NameClaimType=JwtClaimTypes.Name, MapInboundClaims=false (keep short claim names as-emitted). - FileSystemHelpers: EnsureDestinationDirectory uses GetUserName() (the "name" claim) instead of User.Identity.Name for the per-user storage root. Tests: - TestTokenIssuer: stamp a "name" claim = subject so test tokens mirror production shape (fixes FileSystemApiTests upload 500). - Remoting: PasswordToken_carries_name_claim_equal_to_login mints a real JWT via the IS token endpoint (password grant, openid profile) and asserts the access token carries name=login and GetUserName() resolves to it. - WebServerFixture: AccessTokenType=0 (Jwt in IS8; 1=Reference, the inverse of Duende); allow openid+profile on the test client; re-seed the openid/profile identity resources in ReseedAuthTestData (a ResetAndMigrateDatabase wipes the shared in-memory store and previously left them gone → invalid_scope). - BillingServiceTests: [Collection("Yavsc Server")] to serialize with the host build that mutates the static BillingService/Config registries (pre-existing parallel race that the new test's timing surfaced). Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 20:42:54 +01:00
// BillingService and Config expose static mutable registries that
// WorkflowHelpers.ConfigureBillingService() also mutates from the
// WebServerFixture host build (HostingExtensions.cs). Without a
// collection, xUnit runs this class in parallel with the
// "Yavsc Server" collection, and the host build's ConfigureBilling
// call can land between this test's two ConfigureBilling calls,
// breaking the idempotency count assertion (a flaky failure that
// surfaces whenever the fixture's timing shifts). Pinning the class
// to the same collection serializes it with the host build.
[Collection("Yavsc Server")]
[Trait("regression", "II")]
public class BillingServiceTests
{
[Fact]
public void ConfigureBillingService_CanBeCalledTwiceWithoutThrowing()
{
// First initialization should populate the billing registry.
WorkflowHelpers.ConfigureBillingService();
int firstBillingCount = BillingService.Billing.Count;
int firstSettingsCount = BillingService.UserSettings.Count;
int firstProfileTypesCount = Config.ProfileTypes.Count;
// Second call should be idempotent and not throw.
WorkflowHelpers.ConfigureBillingService();
Assert.Equal(firstBillingCount, BillingService.Billing.Count);
Assert.Equal(firstSettingsCount, BillingService.UserSettings.Count);
Assert.Equal(firstProfileTypesCount, Config.ProfileTypes.Count);
}
[Fact]
public void RegisterBilling_DuplicateRegistrationThrowsInvalidOperationException()
{
WorkflowHelpers.ConfigureBillingService();
2026-07-04 22:35:53 +01:00
var firstRegistrar = new Func<ApplicationDbContext, long, IQuery>((db, id) =>
2026-07-05 23:56:10 +01:00
db.HairCutQueries.Include(q => q.Prestation).Include(q => q.Regularization).Single(q => q.Id == id));
2026-04-19 20:36:03 +01:00
const string testCode = "Brush";
2026-07-04 22:35:53 +01:00
Assert.Throws<InvalidOperationException>(() =>
WorkflowHelpers.RegisterBilling<HairCutQuery>(testCode, firstRegistrar));
}
}
}