using Data.SeaHavenIndustries.Enums; using FluentAssertions; using SeaHaven.DataServices.Dto; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Constants; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using System.Security.Claims; using Xunit; namespace SeaHaven.Services.Tests; public sealed class TeamEffectivePermissionsTests { [Fact] public async Task Scheduler_Receives_Role_Defaults() { var data = new FakeUsers().Add("u1", "Scheduler"); var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None); result.IsSuccess.Should().BeTrue(); result.Value!.Permissions.Should().Contain(new[] { TeamPermissionKeys.CreateCompletionDocTemplates, TeamPermissionKeys.EditCompletionDocTemplates }); result.Value.Permissions.Should().NotContain(TeamPermissionKeys.DeleteCompletionDocTemplates); } [Fact] public async Task Grant_Override_Adds_A_Key_Outside_The_Role_Defaults() { var data = new FakeUsers().Add( "u1", "Dispatcher", (TeamPermissionKeys.CreateCompletionDocTemplates, UserPermissionState.Allow)); var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None); result.Value!.Permissions.Should().Contain(TeamPermissionKeys.CreateCompletionDocTemplates); result.Value.Permissions.Should().NotContain(TeamPermissionKeys.EditCompletionDocTemplates); } [Fact] public async Task Revoke_Override_Removes_A_Role_Default() { var data = new FakeUsers().Add( "u1", "Scheduler", (TeamPermissionKeys.EditCompletionDocTemplates, UserPermissionState.Deny)); var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None); result.Value!.Permissions.Should().NotContain(TeamPermissionKeys.EditCompletionDocTemplates); result.Value.Permissions.Should().Contain(TeamPermissionKeys.CreateCompletionDocTemplates); } [Fact] public async Task Admin_Holds_Every_Key_Even_With_A_Revoke_Override() { var data = new FakeUsers().Add( "u1", "Admin", (TeamPermissionKeys.DeleteCompletionDocTemplates, UserPermissionState.Deny)); var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None); result.Value!.Permissions.Should().Equal(TeamPermissionKeys.All); } [Fact] public async Task Keys_Match_What_The_Write_Path_Enforces() { var data = new FakeUsers().Add( "u1", "Dispatcher", (TeamPermissionKeys.CreateCompletionDocTemplates, UserPermissionState.Allow), (TeamPermissionKeys.CreateSites, UserPermissionState.Deny)); var policy = new TeamPermissionPolicy(); var user = await data.GetUserAsync("u1", CancellationToken.None); var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), CancellationToken.None); result.Value!.Permissions.Should().Equal( TeamPermissionKeys.All.Where(key => policy.IsAllowed(user!.RoleName, key, user.Overrides))); } [Fact] public async Task Reads_Only_The_Caller_Identified_By_The_Token() { var data = new FakeUsers() .Add("u1", "Dispatcher") .Add("u2", "Dispatcher", (TeamPermissionKeys.DeleteWorkOrders, UserPermissionState.Allow)); using var cancellation = new CancellationTokenSource(); var result = await Service(data).GetEffectivePermissionsAsync(Caller("u1"), cancellation.Token); result.Value!.Permissions.Should().NotContain(TeamPermissionKeys.DeleteWorkOrders); data.RequestedUserIds.Should().Equal("u1"); data.LastToken.Should().Be(cancellation.Token); } [Fact] public async Task Unauthenticated_Caller_Is_Rejected_Before_Data_Access() { var data = new FakeUsers().Add("u1", "Admin"); var anonymous = new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.NameIdentifier, "u1") })); var result = await Service(data).GetEffectivePermissionsAsync(anonymous, CancellationToken.None); result.Status.Should().Be(TeamPermissionResultStatus.Unauthorized); data.RequestedUserIds.Should().BeEmpty(); } [Fact] public async Task Token_Without_A_User_Id_Is_Rejected_Before_Data_Access() { var data = new FakeUsers().Add("u1", "Admin"); var noId = new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.Role, "Admin") }, "test")); var result = await Service(data).GetEffectivePermissionsAsync(noId, CancellationToken.None); result.Status.Should().Be(TeamPermissionResultStatus.Unauthorized); data.RequestedUserIds.Should().BeEmpty(); } [Fact] public async Task Token_For_A_Removed_User_Is_Rejected() { var data = new FakeUsers(); var result = await Service(data).GetEffectivePermissionsAsync(Caller("gone"), CancellationToken.None); result.Status.Should().Be(TeamPermissionResultStatus.Unauthorized); result.Value.Should().BeNull(); } private static TeamPermissionService Service(FakeUsers data) => new(data, new TeamPermissionPolicy()); private static ClaimsPrincipal Caller(string userId) => new(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")); private sealed class FakeUsers : ITeamPermissionOverrideDataService { private readonly Dictionary _users = new(); public List RequestedUserIds { get; } = new(); public CancellationToken LastToken { get; private set; } public FakeUsers Add(string userId, string role, params (string Key, UserPermissionState State)[] overrides) { _users[userId] = new TeamPermissionUserData { UserId = userId, RoleName = role, Overrides = overrides.ToDictionary(o => o.Key, o => o.State, StringComparer.OrdinalIgnoreCase) }; return this; } public Task GetUserAsync(string userId, CancellationToken cancellationToken) { RequestedUserIds.Add(userId); LastToken = cancellationToken; return Task.FromResult(_users.GetValueOrDefault(userId)); } public Task SetOverrideAsync(string userId, string permissionKey, UserPermissionState state, CancellationToken cancellationToken) => throw new InvalidOperationException("Reading permissions must not write."); public Task SetOverridesAsync(string userId, IReadOnlyDictionary overrides, CancellationToken cancellationToken) => throw new InvalidOperationException("Reading permissions must not write."); public Task ClearOverridesAsync(string userId, CancellationToken cancellationToken) => throw new InvalidOperationException("Reading permissions must not write."); } }