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.Members; using ROLAC.API.Entities; using ROLAC.API.Services; using Xunit; namespace ROLAC.API.Tests.Services; public class MemberServiceTests { private static IHttpContextAccessor BuildAccessor(string userId = "test-user") { 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 mock.Object; } /// /// Builds an InMemory AppDbContext that includes the AuditSaveChangesInterceptor /// so that CreatedBy/UpdatedBy are stamped on save (required by InMemory null checks). /// private static AppDbContext BuildDb(string userId = "test-user") { var accessor = BuildAccessor(userId); var interceptor = new AuditSaveChangesInterceptor(new ROLAC.API.Services.Logging.CurrentUserAccessor(accessor)); return new AppDbContext( new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .AddInterceptors(interceptor) .Options); } // ── Create ─────────────────────────────────────────────────────────────── [Fact] public async Task CreateAsync_ReturnsNewId() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); var request = new CreateMemberRequest { FirstName_en = "Chris", LastName_en = "Chen" }; var id = await svc.CreateAsync(request); Assert.True(id > 0); var saved = await db.Members.FindAsync(id); Assert.NotNull(saved); Assert.Equal("Chris", saved.FirstName_en); } [Fact] public async Task CreateAsync_SavesNickName() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); var request = new CreateMemberRequest { FirstName_en = "Yuan", LastName_en = "Chen", NickName = "Chris" }; var id = await svc.CreateAsync(request); var saved = await db.Members.FindAsync(id); Assert.Equal("Chris", saved!.NickName); } // ── GetById ────────────────────────────────────────────────────────────── [Fact] public async Task GetByIdAsync_ReturnsDto_WhenExists() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); var id = await svc.CreateAsync( new CreateMemberRequest { FirstName_en = "A", LastName_en = "B" }); var dto = await svc.GetByIdAsync(id); Assert.NotNull(dto); Assert.Equal(id, dto.Id); Assert.Equal("A", dto.FirstName_en); } [Fact] public async Task GetByIdAsync_ReturnsNull_WhenNotFound() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); var dto = await svc.GetByIdAsync(9999); Assert.Null(dto); } // ── GetPaged ───────────────────────────────────────────────────────────── [Fact] public async Task GetPagedAsync_FiltersOnSearch() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); await svc.CreateAsync(new CreateMemberRequest { FirstName_en = "Chris", LastName_en = "Chen" }); await svc.CreateAsync(new CreateMemberRequest { FirstName_en = "Alice", LastName_en = "Wang" }); var result = await svc.GetPagedAsync(1, 20, "Chris", null, null); Assert.Equal(1, result.TotalCount); Assert.Equal("Chris", result.Items[0].FirstName_en); } [Fact] public async Task GetPagedAsync_FiltersOnStatus() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); await svc.CreateAsync(new CreateMemberRequest { FirstName_en = "A", LastName_en = "A", Status = "Member" }); await svc.CreateAsync(new CreateMemberRequest { FirstName_en = "B", LastName_en = "B", Status = "Visitor" }); var result = await svc.GetPagedAsync(1, 20, null, "Visitor", null); Assert.Equal(1, result.TotalCount); Assert.Equal("Visitor", result.Items[0].Status); } [Fact] public async Task GetPagedAsync_SearchesNickName() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); await svc.CreateAsync(new CreateMemberRequest { FirstName_en = "Yuan", LastName_en = "Chen", NickName = "Chris" }); var result = await svc.GetPagedAsync(1, 20, "Chris", null, null); Assert.Equal(1, result.TotalCount); } // ── Update ─────────────────────────────────────────────────────────────── [Fact] public async Task UpdateAsync_PersistsChanges() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); var id = await svc.CreateAsync( new CreateMemberRequest { FirstName_en = "Old", LastName_en = "Name" }); await svc.UpdateAsync(id, new UpdateMemberRequest { FirstName_en = "New", LastName_en = "Name", Country = "USA", Status = "Member", LanguagePreference = "en" }); var saved = await db.Members.FindAsync(id); Assert.Equal("New", saved!.FirstName_en); } [Fact] public async Task UpdateAsync_ThrowsKeyNotFound_WhenMissing() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); await Assert.ThrowsAsync(() => svc.UpdateAsync(9999, new UpdateMemberRequest { FirstName_en = "X", LastName_en = "Y", Country = "USA", Status = "Member", LanguagePreference = "en" })); } // ── Delete (soft) ──────────────────────────────────────────────────────── [Fact] public async Task DeleteAsync_SoftDeletesMember() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor("deleter-id")); var id = await svc.CreateAsync( new CreateMemberRequest { FirstName_en = "A", LastName_en = "B" }); await svc.DeleteAsync(id); // Query-filtered view returns null var filtered = await db.Members.FindAsync(id); Assert.Null(filtered); // Raw view shows IsDeleted = true var raw = await db.Members.IgnoreQueryFilters() .FirstAsync(m => m.Id == id); Assert.True(raw.IsDeleted); Assert.Equal("deleter-id", raw.DeletedBy); Assert.NotNull(raw.DeletedAt); } [Fact] public async Task DeleteAsync_ThrowsKeyNotFound_WhenMissing() { using var db = BuildDb(); var svc = new MemberService(db, BuildAccessor()); await Assert.ThrowsAsync(() => svc.DeleteAsync(9999)); } }