using Data.SeaHavenIndustries.Enums; using FluentAssertions; using SeaHaven.DataServices.Dto; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Constants; using SeaHaven.Services.Implementation; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; using System.Security.Claims; using Xunit; namespace SeaHaven.Services.Tests; public sealed class TeamPermissionServiceTests { [Fact] public async Task GetProfile_ReturnsEveryKeyAndRoleDefaults() { var data = new FakeDataService("u1", "Dispatcher"); var result = await Service(data).GetProfileAsync("u1", Admin(), CancellationToken.None); result.IsSuccess.Should().BeTrue(); result.Value!.Permissions.Should().HaveCount(22); result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.CreateSites).IsGranted.Should().BeTrue(); result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.DeleteSites).IsGranted.Should().BeFalse(); result.Value.Permissions.Should().OnlyContain(p => p.OverrideState == UserPermissionState.Unset); } [Fact] public async Task SetOverride_ChangesOnlyOnePermissionAndPersistsCanonicalKey() { var data = new FakeDataService("u1", "Dispatcher"); var result = await Service(data).SetOverrideAsync( "u1", "DELETEsites", UserPermissionState.Allow, Admin(), CancellationToken.None); result.IsSuccess.Should().BeTrue(); result.Value!.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.DeleteSites).IsGranted.Should().BeTrue(); result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.DeleteSites).OverrideState.Should().Be(UserPermissionState.Allow); result.Value.Permissions.Single(p => p.PermissionKey == TeamPermissionKeys.CreateSites).IsGranted.Should().BeTrue(); data.LastSet.Should().Be(("u1", TeamPermissionKeys.DeleteSites, UserPermissionState.Allow)); } [Fact] public async Task SetOverride_RejectsUnknownKeyBeforeDataAccess() { var data = new FakeDataService("u1", "Dispatcher"); var result = await Service(data).SetOverrideAsync( "u1", "not-a-permission", UserPermissionState.Allow, Admin(), CancellationToken.None); result.Status.Should().Be(TeamPermissionResultStatus.InvalidPermissionKey); data.GetCalls.Should().Be(0); } [Fact] public async Task NonAdminCannotReadOrWritePermissions() { var data = new FakeDataService("u1", "Dispatcher"); var service = Service(data); var read = await service.GetProfileAsync("u1", User("Dispatcher"), CancellationToken.None); var write = await service.SetOverrideAsync("u1", TeamPermissionKeys.CreateSites, UserPermissionState.Deny, User("Dispatcher"), CancellationToken.None); read.Status.Should().Be(TeamPermissionResultStatus.Forbidden); write.Status.Should().Be(TeamPermissionResultStatus.Forbidden); data.GetCalls.Should().Be(0); } [Fact] public async Task UnknownRoleReceivesNoGrantEvenWhenAnOverrideIsSet() { var data = new FakeDataService("u1", "Legacy"); var result = await Service(data).SetOverrideAsync( "u1", TeamPermissionKeys.CreateSites, UserPermissionState.Allow, Admin(), CancellationToken.None); result.Value!.Permissions.Should().OnlyContain(p => !p.IsGranted); } [Fact] public async Task MissingUserReturnsNotFound() { var data = new FakeDataService(null, null); var result = await Service(data).GetProfileAsync("missing", Admin(), CancellationToken.None); result.Status.Should().Be(TeamPermissionResultStatus.NotFound); } private static TeamPermissionService Service(FakeDataService data) => new(data, new TeamPermissionPolicy()); private static ClaimsPrincipal Admin() => User("Admin"); private static ClaimsPrincipal User(string role) => new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, role) }, "test")); private sealed class FakeDataService : ITeamPermissionOverrideDataService { private readonly string? _userId; private readonly string? _roleName; private readonly Dictionary _overrides = new(StringComparer.OrdinalIgnoreCase); public FakeDataService(string? userId, string? roleName) { _userId = userId; _roleName = roleName; } public int GetCalls { get; private set; } public (string UserId, string PermissionKey, UserPermissionState State)? LastSet { get; private set; } public Task GetUserAsync(string userId, CancellationToken cancellationToken) { GetCalls++; return Task.FromResult(_userId != userId ? null : new TeamPermissionUserData { UserId = userId, RoleName = _roleName, Overrides = new Dictionary(_overrides, StringComparer.OrdinalIgnoreCase) }); } public Task SetOverrideAsync(string userId, string permissionKey, UserPermissionState state, CancellationToken cancellationToken) { _overrides[permissionKey] = state; LastSet = (userId, permissionKey, state); return Task.CompletedTask; } public Task SetOverridesAsync( string userId, IReadOnlyDictionary overrides, CancellationToken cancellationToken) { foreach (var (permissionKey, state) in overrides) { _overrides[permissionKey] = state; LastSet = (userId, permissionKey, state); } return Task.CompletedTask; } public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken) { _overrides.Clear(); return Task.CompletedTask; } } }