fix(expense): resolve current user id from 'sub' JWT claim
Live verification revealed the JWT carries the user id in the 'sub' claim (NameClaimType=sub, MapInboundClaims=false), so ClaimTypes.NameIdentifier is null at runtime. This caused ExpensesController.GetMine/GetById to throw NullReferenceException (500) on the '!.Value', and made the services fall back to 'system' — silently defeating the self-ownership guard. Resolve via NameIdentifier (unit tests) then 'sub' (real tokens). Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,16 @@ public class ExpenseServiceTests
|
||||
return new ExpenseService(db, http.Object, fs);
|
||||
}
|
||||
|
||||
// Builds a service whose principal carries ONLY the "sub" claim (no NameIdentifier),
|
||||
// mirroring the real JWT (NameClaimType="sub", MapInboundClaims=false).
|
||||
private static ExpenseService SvcWithSubClaim(AppDbContext db, FakeStorage fs, string userId)
|
||||
{
|
||||
var http = new Mock<IHttpContextAccessor>();
|
||||
var ctx = new DefaultHttpContext { User = new(new ClaimsIdentity(new[] { new Claim("sub", userId) })) };
|
||||
http.Setup(x => x.HttpContext).Returns(ctx);
|
||||
return new ExpenseService(db, http.Object, fs);
|
||||
}
|
||||
|
||||
private static CreateExpenseRequest Reimb() => new()
|
||||
{
|
||||
Type = "StaffReimbursement", MinistryId = 1, CategoryGroupId = 1, SubCategoryId = 1,
|
||||
@@ -69,6 +79,24 @@ public class ExpenseServiceTests
|
||||
ExpenseDate = r.ExpenseDate, Notes = r.Notes,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Create_Reimbursement_ResolvesUserId_FromSubClaim()
|
||||
{
|
||||
// Regression: the real JWT exposes the user id as "sub", not ClaimTypes.NameIdentifier.
|
||||
// SubmittedBy must be the sub value (not "system"), or the self-ownership guard breaks.
|
||||
var db = BuildDb("ignored");
|
||||
db.Ministries.Add(new Ministry { Id = 1, Name_en = "Worship" });
|
||||
db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 1, Name_en = "Equipment" });
|
||||
db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Purchase" });
|
||||
await db.SaveChangesAsync();
|
||||
var svc = SvcWithSubClaim(db, new FakeStorage(), "user-guid-123");
|
||||
|
||||
var id = await svc.CreateAsync(Reimb(), isFinance: false);
|
||||
|
||||
var e = await db.Expenses.FindAsync(id);
|
||||
Assert.Equal("user-guid-123", e!.SubmittedBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_Vendor_AsFinance_IsImmediatelyPaid()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user