Add role control
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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 }]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user