Files
ROLAC/API/ROLAC.API.Tests/Services/PermissionServiceTests.cs
Chris Chen 62592c29ae
ci-cd-vm / ci-cd (push) Successful in 4m2s
Add audit logs.
2026-06-23 12:13:47 -07:00

186 lines
7.2 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,
new ROLAC.API.Services.Logging.SystemLogQueue(),
new Microsoft.AspNetCore.Http.HttpContextAccessor()),
};
}
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 }]));
}
}