using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Moq; using ROLAC.API.Data; using ROLAC.API.DTOs.Users; using ROLAC.API.Entities; using ROLAC.API.Services; using Xunit; namespace ROLAC.API.Tests.Services; public class UserManagementServiceTests { private static AppDbContext BuildDb() => new(new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); private static Mock> BuildUserManager( AppUser? findResult = null, bool createOk = true, IList? roles = null) { var store = new Mock>(); #pragma warning disable CS8625 var mgr = new Mock>( store.Object, null, null, null, null, null, null, null, null); #pragma warning restore CS8625 mgr.Setup(m => m.FindByIdAsync(It.IsAny())) .ReturnsAsync(findResult); mgr.Setup(m => m.FindByEmailAsync(It.IsAny())) .ReturnsAsync((AppUser?)null); mgr.Setup(m => m.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(createOk ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { Description = "fail" })); mgr.Setup(m => m.AddToRolesAsync(It.IsAny(), It.IsAny>())) .ReturnsAsync(IdentityResult.Success); mgr.Setup(m => m.GetRolesAsync(It.IsAny())) .ReturnsAsync(roles ?? new List { "member" }); mgr.Setup(m => m.UpdateAsync(It.IsAny())) .ReturnsAsync(IdentityResult.Success); mgr.Setup(m => m.RemoveFromRolesAsync(It.IsAny(), It.IsAny>())) .ReturnsAsync(IdentityResult.Success); mgr.Setup(m => m.GeneratePasswordResetTokenAsync(It.IsAny())) .ReturnsAsync("reset-token"); mgr.Setup(m => m.ResetPasswordAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Success); return mgr; } // ── CreateAsync ────────────────────────────────────────────────────────── [Fact] public async Task CreateAsync_ReturnsTempPassword() { using var db = BuildDb(); // Seed a Member so MemberId validation passes // Note: InMemory DB requires audit fields — we set them directly var member = new Member { FirstName_en = "A", LastName_en = "B", CreatedBy = "system", UpdatedBy = "system", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, }; db.Members.Add(member); await db.SaveChangesAsync(); var mgr = BuildUserManager(); // Capture the AppUser passed to CreateAsync AppUser? created = null; mgr.Setup(m => m.CreateAsync(It.IsAny(), It.IsAny())) .Callback((u, _) => { created = u; u.Id = Guid.NewGuid().ToString(); }) .ReturnsAsync(IdentityResult.Success); // Mock Users queryable to return empty (no existing user for this member) mgr.Setup(m => m.Users) .Returns(new List().AsQueryable()); var svc = new UserManagementService(mgr.Object, db, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance); var result = await svc.CreateAsync(new CreateUserRequest { MemberId = member.Id, Email = "test@rolac.org", Roles = ["member"], }); Assert.False(string.IsNullOrEmpty(result.TempPassword)); Assert.Equal(12, result.TempPassword.Length); Assert.NotNull(created); Assert.Equal(member.Id, created!.MemberId); } [Fact] public async Task CreateAsync_Throws_WhenMemberNotFound() { using var db = BuildDb(); var mgr = BuildUserManager(); mgr.Setup(m => m.Users) .Returns(new List().AsQueryable()); var svc = new UserManagementService(mgr.Object, db, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance); await Assert.ThrowsAsync(() => svc.CreateAsync(new CreateUserRequest { MemberId = 9999, Email = "x@y.com", Roles = ["member"] })); } [Fact] public async Task CreateAsync_Throws_WhenMemberAlreadyHasUser() { using var db = BuildDb(); var member = new Member { FirstName_en = "A", LastName_en = "B", CreatedBy = "system", UpdatedBy = "system", CreatedAt = DateTimeOffset.UtcNow, UpdatedAt = DateTimeOffset.UtcNow, }; db.Members.Add(member); await db.SaveChangesAsync(); var existingUser = new AppUser { Id = Guid.NewGuid().ToString(), UserName = "existing@test.com", Email = "existing@test.com", MemberId = member.Id, }; db.Users.Add(existingUser); await db.SaveChangesAsync(); var mgr = BuildUserManager(); // The service checks _userManager.Users — we need to return the existing user mgr.Setup(m => m.Users) .Returns(new List { existingUser }.AsQueryable()); var svc = new UserManagementService(mgr.Object, db, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance); await Assert.ThrowsAsync(() => svc.CreateAsync(new CreateUserRequest { MemberId = member.Id, Email = "new@test.com", Roles = ["member"] })); } // ── DeactivateAsync ────────────────────────────────────────────────────── [Fact] public async Task DeactivateAsync_SetsIsActiveFalse() { using var db = BuildDb(); var user = new AppUser { Id = "u1", UserName = "a@b.com", Email = "a@b.com", IsActive = true }; var mgr = BuildUserManager(findResult: user); var svc = new UserManagementService(mgr.Object, db, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance); await svc.DeactivateAsync("u1"); Assert.False(user.IsActive); Assert.Equal(DateTimeOffset.MaxValue, user.LockoutEnd); } [Fact] public async Task DeactivateAsync_ThrowsKeyNotFound_WhenUserMissing() { using var db = BuildDb(); var mgr = BuildUserManager(findResult: null); var svc = new UserManagementService(mgr.Object, db, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance); await Assert.ThrowsAsync(() => svc.DeactivateAsync("missing")); } // ── ResetPasswordAsync ─────────────────────────────────────────────────── [Fact] public async Task ResetPasswordAsync_ReturnsNewTempPassword() { using var db = BuildDb(); var user = new AppUser { Id = "u1", UserName = "a@b.com", Email = "a@b.com" }; var mgr = BuildUserManager(findResult: user); var svc = new UserManagementService(mgr.Object, db, ROLAC.API.Tests.TestSupport.NullAuditLogger.Instance); var pwd = await svc.ResetPasswordAsync("u1"); Assert.Equal(12, pwd.Length); } }