using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.AspNetCore.Identity; using Moq; using SeaHaven.DataServices.Dto; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class UserServiceTests { private static UserService NewService( Mock userData, Mock email, out Mock> store) { var (manager, s, _) = IdentityTestHelpers.CreateUserManager(); store = s; return new UserService(manager, userData.Object, email.Object); } [Fact] public async Task GetUsers_FormatsCreatedDate_AndHandlesNullDate() { var userData = new Mock(); userData.Setup(u => u.GetAllForListAsync(It.IsAny())) .ReturnsAsync(new List { new() { Id = "1", Name = "A", Email = "a@x.com", RoleName = "Admin", Status = "Active", CreatedDate = new DateTime(2026, 1, 5) }, new() { Id = "2", Name = "B", Email = "b@x.com", RoleName = "Tech", Status = "Active", CreatedDate = null } }); var service = NewService(userData, new Mock(), out _); var users = (await service.GetUsersAsync(CancellationToken.None)).ToList(); users.Should().HaveCount(2); users[0].Date.Should().Be("Jan 05 2026"); users[0].RoleName.Should().Be("Admin"); users[0].Status.Should().Be("Active"); users[1].Date.Should().Be(""); users[1].Name.Should().Be("B"); } [Fact] public async Task DeleteUser_NotFound_ReturnsFalseWithoutCascade() { var userData = new Mock(); userData.Setup(u => u.GetForEditAsync("missing", It.IsAny())).ReturnsAsync((ApplicationUser?)null); var service = NewService(userData, new Mock(), out _); var found = await service.DeleteUserAsync("missing", CancellationToken.None); found.Should().BeFalse(); userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task DeleteUser_Found_PerformsCascade() { var user = IdentityTestHelpers.User(); var userData = new Mock(); userData.Setup(u => u.GetForEditAsync("u1", It.IsAny())).ReturnsAsync(user); var service = NewService(userData, new Mock(), out _); var found = await service.DeleteUserAsync("u1", CancellationToken.None); found.Should().BeTrue(); userData.Verify(u => u.DeleteUserWithCascadeAsync(user, It.IsAny()), Times.Once); } [Fact] public async Task DeleteCurrentUser_SetsIsDeletedFlag() { var user = IdentityTestHelpers.User(); var userData = new Mock(); userData.Setup(u => u.GetForEditAsync("u1", It.IsAny())).ReturnsAsync(user); var service = NewService(userData, new Mock(), out _); await service.DeleteCurrentUserAsync("u1", CancellationToken.None); user.IsDeleted.Should().BeTrue(); userData.Verify(u => u.UpdateUserAsync(user, It.IsAny()), Times.Once); } [Fact] public async Task EditUser_NotFound_ReturnsFalse() { var userData = new Mock(); userData.Setup(u => u.GetForEditAsync("x", It.IsAny())).ReturnsAsync((ApplicationUser?)null); var service = NewService(userData, new Mock(), out _); var succeeded = await service.EditUserAsync(new EditUserRequestDTO { Id = "x", Email = "e@x.com", Name = "n", Role = "Admin" }, CancellationToken.None); succeeded.Should().BeFalse(); } [Fact] public async Task GetUserProfile_MapsCreatedDateToAddedDate() { var user = new UserProfileData { Id = "u1", FirstName = "Alice", CreatedDate = new DateTime(2026, 3, 4) }; var userData = new Mock(); userData.Setup(u => u.GetProfileAsync("u1", It.IsAny())).ReturnsAsync(user); var service = NewService(userData, new Mock(), out _); var rows = await service.GetUserProfileAsync("u1", CancellationToken.None); rows.Should().HaveCount(1); rows[0].AddedDate.Should().Be(new DateTime(2026, 3, 4)); rows[0].Name.Should().Be("Alice"); } [Fact] public async Task AddUser_CreateSuccess_PersistsRoleAndEmailsLoginInformation() { var userData = new Mock(); userData.Setup(u => u.GetByIdAsync(It.IsAny())).ReturnsAsync((ApplicationUser?)null); var email = new Mock(); var service = NewService(userData, email, out var store); store.As>() .Setup(s => s.SetPasswordHashAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); store.Setup(s => s.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Success); store.Setup(s => s.AddToRoleAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var outcome = await service.AddUserAsync(new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "Admin" }, CancellationToken.None); outcome.Success.Should().BeTrue(); store.Verify(s => s.CreateAsync(It.IsAny(), It.IsAny()), Times.Once); store.Verify(s => s.AddToRoleAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); email.Verify( e => e.SendEmailAsync( "n@x.com", "Login Information", It.Is(body => ContainsStrongGeneratedPassword(body))), Times.Once); } [Fact] public async Task AddUser_CreateFails_ReturnsFirstError() { var userData = new Mock(); userData.Setup(u => u.GetByIdAsync(It.IsAny())).ReturnsAsync((ApplicationUser?)null); var service = NewService(userData, new Mock(), out var store); store.As>() .Setup(s => s.SetPasswordHashAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); store.Setup(s => s.CreateAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(IdentityResult.Failed(new IdentityError { Description = "duplicate" })); var outcome = await service.AddUserAsync(new AddUserRequestDTO { Name = "N", Email = "n@x.com", Role = "Admin" }, CancellationToken.None); outcome.Success.Should().BeFalse(); outcome.Error.Should().Be("duplicate"); } private static bool ContainsStrongGeneratedPassword(string body) { const string marker = "Your password is: "; var start = body.IndexOf(marker, StringComparison.Ordinal); if (start < 0) return false; var password = body[(start + marker.Length)..] .Split('\n', StringSplitOptions.RemoveEmptyEntries) .FirstOrDefault(); return password is { Length: 8 } && password.Any(char.IsUpper) && password.Any(char.IsLower) && password.Any(char.IsDigit) && password.Any(character => !char.IsLetterOrDigit(character)); } }