using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Moq; using ROLAC.API.Data; using ROLAC.API.Data.Interceptors; using ROLAC.API.DTOs.Expense; using ROLAC.API.Entities; using ROLAC.API.Services; using ROLAC.API.Services.Logging; using Xunit; namespace ROLAC.API.Tests.Services; public class ExpenseSnapshotServiceTests { private static AppDbContext BuildDb(string userId) { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) }; var ctx = new DefaultHttpContext { User = new(new ClaimsIdentity(claims)) }; var mock = new Mock(); mock.Setup(x => x.HttpContext).Returns(ctx); return new AppDbContext(new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .AddInterceptors(new AuditSaveChangesInterceptor(new CurrentUserAccessor(mock.Object))).Options); } private static (ExpenseSnapshotService svc, AppDbContext db) Build(string userId = "u1") { var db = BuildDb(userId); db.Ministries.Add(new Ministry { Id = 1, Name_en = "Worship", Name_zh = "敬拜" }); db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 1, Name_en = "Facilities" }); db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 1, GroupId = 1, Name_en = "Rent" }); db.SaveChanges(); return (new ExpenseSnapshotService(db), db); } private static CreateExpenseSnapshotRequest Rent() => new() { Name = "Monthly Rent", MinistryId = 1, Description = "Office rent", VendorName = "Landlord X", CheckNumber = "1001", Lines = { new ExpenseLineInput { CategoryGroupId = 1, SubCategoryId = 1, Amount = 1200m } }, }; [Fact] public async Task Create_PersistsHeaderAndLines_StampsCreator() { var (svc, db) = Build("creator-1"); var id = await svc.CreateAsync(Rent()); var saved = await db.ExpenseSnapshots.FindAsync(id); Assert.Equal("Monthly Rent", saved!.Name); Assert.Equal("creator-1", saved.CreatedBy); Assert.Equal(1, await db.ExpenseSnapshotLines.CountAsync(l => l.SnapshotId == id)); } [Fact] public async Task Create_WithNoLines_Throws() { var (svc, _) = Build(); var r = Rent(); r.Lines.Clear(); await Assert.ThrowsAsync(() => svc.CreateAsync(r)); } [Fact] public async Task GetById_ReturnsLines_TotalsAndCreatorName() { var (svc, db) = Build("creator-1"); db.Members.Add(new Member { Id = 5, FirstName_en = "Joy", LastName_en = "Wong" }); db.Users.Add(new AppUser { Id = "creator-1", MemberId = 5 }); await db.SaveChangesAsync(); var id = await svc.CreateAsync(Rent()); var dto = await svc.GetByIdAsync(id); Assert.NotNull(dto); Assert.Equal(1200m, dto!.TotalAmount); Assert.Equal(1, dto.LineCount); Assert.Equal("Rent", dto.Lines.Single().SubCategoryName); Assert.Equal("Joy Wong", dto.CreatedByName); } [Fact] public async Task GetAll_ReturnsNewestFirst() { var (svc, _) = Build(); var first = await svc.CreateAsync(Rent()); var second = await svc.CreateAsync(Rent()); var all = await svc.GetAllAsync(); Assert.Equal(2, all.Count); Assert.Equal(second, all[0].Id); Assert.Equal(first, all[1].Id); } [Fact] public async Task Update_RenamesAndReplacesLines() { var (svc, db) = Build(); db.ExpenseCategoryGroups.Add(new ExpenseCategoryGroup { Id = 2, Name_en = "Utilities" }); db.ExpenseSubCategories.Add(new ExpenseSubCategory { Id = 2, GroupId = 2, Name_en = "Internet" }); await db.SaveChangesAsync(); var id = await svc.CreateAsync(Rent()); await svc.UpdateAsync(id, new UpdateExpenseSnapshotRequest { Name = "Monthly Internet", MinistryId = 1, Description = "ISP", Lines = { new ExpenseLineInput { CategoryGroupId = 2, SubCategoryId = 2, Amount = 80m } }, }); var dto = await svc.GetByIdAsync(id); Assert.Equal("Monthly Internet", dto!.Name); Assert.Equal(80m, dto.TotalAmount); Assert.Equal("Internet", dto.Lines.Single().SubCategoryName); Assert.Equal(1, await db.ExpenseSnapshotLines.CountAsync(l => l.SnapshotId == id)); } [Fact] public async Task Update_MissingId_Throws() { var (svc, _) = Build(); await Assert.ThrowsAsync(() => svc.UpdateAsync(999, new UpdateExpenseSnapshotRequest { Name = "x", MinistryId = 1, Description = "x", Lines = { new ExpenseLineInput { CategoryGroupId = 1, SubCategoryId = 1, Amount = 1m } }, })); } [Fact] public async Task Delete_SoftDeletes_HidesFromQueries() { var (svc, db) = Build(); var id = await svc.CreateAsync(Rent()); await svc.DeleteAsync(id); Assert.Empty(await svc.GetAllAsync()); Assert.Null(await db.ExpenseSnapshots.FirstOrDefaultAsync(s => s.Id == id)); } }