184 lines
7.1 KiB
C#
184 lines
7.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using ROLAC.API.Authorization;
|
|
using ROLAC.API.Data;
|
|
using ROLAC.API.DTOs.Permissions;
|
|
using ROLAC.API.Entities;
|
|
using ROLAC.API.Services;
|
|
using Xunit;
|
|
|
|
namespace ROLAC.API.Tests.Services;
|
|
|
|
public class PermissionServiceTests
|
|
{
|
|
// -----------------------------------------------------------------------
|
|
// Harness: a real PermissionService backed by an in-memory EF database.
|
|
// -----------------------------------------------------------------------
|
|
|
|
private sealed class Harness
|
|
{
|
|
public required ServiceProvider Provider { get; init; }
|
|
public required PermissionService Service { get; init; }
|
|
|
|
public async Task SeedRoleAsync(string roleName, params RolePermission[] permissions)
|
|
{
|
|
using var scope = Provider.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
|
|
var role = new AppRole { Id = $"role-{roleName}", Name = roleName, NormalizedName = roleName.ToUpperInvariant() };
|
|
db.Roles.Add(role);
|
|
foreach (var permission in permissions)
|
|
{
|
|
permission.RoleId = role.Id;
|
|
db.RolePermissions.Add(permission);
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
private static Harness BuildHarness()
|
|
{
|
|
var dbName = Guid.NewGuid().ToString();
|
|
var services = new ServiceCollection();
|
|
services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase(dbName));
|
|
var provider = services.BuildServiceProvider();
|
|
|
|
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
|
|
var cache = new MemoryCache(new MemoryCacheOptions());
|
|
return new Harness
|
|
{
|
|
Provider = provider,
|
|
Service = new PermissionService(scopeFactory, cache),
|
|
};
|
|
}
|
|
|
|
private static RolePermission Perm(string module, bool r = false, bool w = false, bool d = false, bool a = false)
|
|
=> new() { Module = module, CanRead = r, CanWrite = w, CanDelete = d, CanApprove = a };
|
|
|
|
// -----------------------------------------------------------------------
|
|
// HasPermissionAsync
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task HasPermission_RoleGrantsAction_ReturnsTrue()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true, w: true));
|
|
|
|
Assert.True(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Read));
|
|
Assert.True(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Write));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasPermission_RoleLacksAction_ReturnsFalse()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true)); // read only
|
|
|
|
Assert.False(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Delete));
|
|
Assert.False(await h.Service.HasPermissionAsync(["finance"], Modules.Members, PermissionActions.Read));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasPermission_UnionAcrossRoles_ReturnsTrueIfAnyRoleGrants()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("pastor", Perm(Modules.Members, r: true));
|
|
await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true, w: true));
|
|
|
|
// User holds both roles — should get the union.
|
|
Assert.True(await h.Service.HasPermissionAsync(["pastor", "finance"], Modules.Members, PermissionActions.Read));
|
|
Assert.True(await h.Service.HasPermissionAsync(["pastor", "finance"], Modules.Givings, PermissionActions.Write));
|
|
Assert.False(await h.Service.HasPermissionAsync(["pastor", "finance"], Modules.Members, PermissionActions.Delete));
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// GetEffectivePermissionsAsync
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task GetEffectivePermissions_SuperAdmin_ReturnsAllModulesFull()
|
|
{
|
|
var h = BuildHarness(); // no rows seeded at all
|
|
|
|
var effective = await h.Service.GetEffectivePermissionsAsync(["super_admin"]);
|
|
|
|
Assert.Equal(Modules.All.Count, effective.Count);
|
|
foreach (var module in Modules.All)
|
|
{
|
|
Assert.True(effective[module].Read);
|
|
Assert.True(effective[module].Write);
|
|
Assert.True(effective[module].Delete);
|
|
Assert.True(effective[module].Approve);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetEffectivePermissions_MergesFlagsAcrossRoles()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("a", Perm(Modules.Expenses, r: true));
|
|
await h.SeedRoleAsync("b", Perm(Modules.Expenses, w: true, a: true));
|
|
|
|
var effective = await h.Service.GetEffectivePermissionsAsync(["a", "b"]);
|
|
|
|
Assert.True(effective[Modules.Expenses].Read);
|
|
Assert.True(effective[Modules.Expenses].Write);
|
|
Assert.True(effective[Modules.Expenses].Approve);
|
|
Assert.False(effective[Modules.Expenses].Delete);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetEffectivePermissions_OmitsModulesWithNoGrant()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("member"); // role exists but no grants
|
|
|
|
var effective = await h.Service.GetEffectivePermissionsAsync(["member"]);
|
|
|
|
Assert.Empty(effective);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Caching / invalidation via UpsertRoleAsync
|
|
// -----------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public async Task UpsertRole_InvalidatesCache_SoNextCheckReflectsNewState()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("finance", Perm(Modules.Givings, r: true)); // read only
|
|
|
|
// Prime the cache with the original snapshot.
|
|
Assert.False(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Write));
|
|
|
|
// Grant write; UpsertRoleAsync must invalidate the cache.
|
|
await h.Service.UpsertRoleAsync("finance", [new ModulePermissionDto
|
|
{
|
|
Module = Modules.Givings, CanRead = true, CanWrite = true,
|
|
}]);
|
|
|
|
Assert.True(await h.Service.HasPermissionAsync(["finance"], Modules.Givings, PermissionActions.Write));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpsertRole_SuperAdmin_Throws()
|
|
{
|
|
var h = BuildHarness();
|
|
await h.SeedRoleAsync("super_admin");
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => h.Service.UpsertRoleAsync("super_admin", [new ModulePermissionDto { Module = Modules.Members, CanRead = true }]));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpsertRole_UnknownRole_Throws()
|
|
{
|
|
var h = BuildHarness();
|
|
|
|
await Assert.ThrowsAsync<KeyNotFoundException>(
|
|
() => h.Service.UpsertRoleAsync("ghost", [new ModulePermissionDto { Module = Modules.Members, CanRead = true }]));
|
|
}
|
|
}
|