using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentAssertions; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using Xunit; namespace Api.SeaHavenIndustries.Tests; public sealed class TeamMemberServiceTests { [Fact] public async Task Create_DispatcherRequiresAtLeastOneServiceArea() { var userManager = UserManager(); var roleManager = RoleManager(); var service = new TeamMemberService( userManager.Object, roleManager.Object, Mock.Of(), Mock.Of()); var result = await service.CreateAsync( ValidRequest() with { ServiceAreas = Array.Empty() }, Admin(), CancellationToken.None); result.Success.Should().BeFalse(); result.Error.Should().Be("At least one service area is required for a Dispatcher."); userManager.Verify(manager => manager.CreateAsync(It.IsAny()), Times.Never); } [Fact] public async Task Create_RejectsUnknownColorAndPermission() { var service = NewService(out var userManager, out _, out _, out _); var badColor = await service.CreateAsync( ValidRequest() with { Color = "#000000" }, Admin(), CancellationToken.None); var badPermission = await service.CreateAsync( ValidRequest() with { PermissionOverrides = new Dictionary { ["not-a-permission"] = UserPermissionState.Allow } }, Admin(), CancellationToken.None); badColor.Error.Should().Be("Color must be selected from the accessible palette."); badPermission.Error.Should().Be("Unknown permission key: not-a-permission."); userManager.Verify(manager => manager.CreateAsync(It.IsAny()), Times.Never); } [Fact] public async Task Create_PersistsPendingUserWithoutPasswordAndStoresAreasAndOverrides() { var service = NewService(out var userManager, out var roleManager, out var areas, out var overrides); ApplicationUser? created = null; userManager .Setup(manager => manager.CreateAsync(It.IsAny())) .Callback(user => { user.Id = "new-user"; created = user; }) .ReturnsAsync(IdentityResult.Success); userManager .Setup(manager => manager.FindByEmailAsync(It.IsAny())) .ReturnsAsync((ApplicationUser?)null); userManager .Setup(manager => manager.AddToRoleAsync(It.IsAny(), "Dispatcher")) .ReturnsAsync(IdentityResult.Success); roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true); var result = await service.CreateAsync(ValidRequest(), Admin(), CancellationToken.None); result.Success.Should().BeTrue(); created.Should().NotBeNull(); created!.PasswordHash.Should().BeNull(); created.EmailConfirmed.Should().BeFalse(); created.PendingRegistration.Should().BeTrue(); created.PhoneNumber.Should().Be("555-0100"); result.Member!.ServiceAreas.Should().Equal("East", "West"); areas.Verify(data => data.ReplaceAsync("new-user", It.Is>(value => value.SequenceEqual(new[] { "East", "West" })), It.IsAny()), Times.Once); overrides.Verify(data => data.SetOverridesAsync("new-user", It.Is>(value => value["deleteSites"] == UserPermissionState.Allow), It.IsAny()), Times.Once); } private static TeamMemberService NewService( out Mock> userManager, out Mock> roleManager, out Mock areas, out Mock overrides) { userManager = UserManager(); roleManager = RoleManager(); areas = new Mock(); overrides = new Mock(); return new TeamMemberService(userManager.Object, roleManager.Object, areas.Object, overrides.Object); } private static Mock> UserManager() { var store = new Mock>(); return new Mock>( store.Object, Microsoft.Extensions.Options.Options.Create(new IdentityOptions()), Mock.Of>(), Array.Empty>(), Array.Empty>(), Mock.Of(), new IdentityErrorDescriber(), Mock.Of(), Mock.Of>>()); } private static Mock> RoleManager() { var store = new Mock>(); return new Mock>( store.Object, Array.Empty>(), Mock.Of(), new IdentityErrorDescriber(), Mock.Of>>()); } private static CreateTeamMemberRequestDTO ValidRequest() => new() { Name = "Taylor Dispatcher", Role = "dispatcher", Color = "#F59E0B", Email = "taylor@example.com", Phone = "555-0100", ServiceAreas = new[] { "east", "West" }, PermissionOverrides = new Dictionary { ["deleteSites"] = UserPermissionState.Allow } }; private static ClaimsPrincipal Admin() => new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "Admin") }, "test")); }