e7b1f270a5
- Implemented VendorOptionalFields component to handle optional vendor details (mobile, email, address) with validation. - Created useCustomerConfig hook to load optional customer field settings from Sales configuration. - Developed useProcurementConfig hook to manage Procurement configuration flags. - Introduced accessConfigApi for managing access configuration settings. - Added openingBalancesApi for handling opening balances operations. - Created procurementConfigApi for managing procurement configuration settings. - Implemented nav-routes for permission-based routing. - Developed PDF generation for purchase orders with dynamic vendor and item details. - Added purchase-order tax helpers for tax calculations and formatting. - Defined types for opening balances and related operations.
513 lines
24 KiB
C#
513 lines
24 KiB
C#
using System.Security.Claims;
|
|
using ERPCore.Domain.Entities;
|
|
using ERPCore.Domain.Enums;
|
|
using ERPCore.Dtos.Config;
|
|
using ERPCore.Dtos.Users;
|
|
using ERPCore.Infra.Auth;
|
|
using ERPCore.Infra.Persistence;
|
|
using ERPCore.Repositories;
|
|
using ERPCore.Services;
|
|
using ERPCore.System.Errors;
|
|
using ERPCore.Tests.TestSupport;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Configuration;
|
|
using static ERPCore.Tests.TestSupport.BankDepositTestSupport;
|
|
|
|
namespace ERPCore.Tests;
|
|
|
|
/// <summary>
|
|
/// The ERP-owned role model: AuthHex proves who someone is; the role, its permissions and "is admin" come from the
|
|
/// ERP's own users/roles tables, are read on every request, and a change applies on the very next one.
|
|
/// </summary>
|
|
public class AccessControlTests
|
|
{
|
|
private const int AdminRole = 1, ApproverRole = 2, ViewerRole = 3, NoAccessRole = 4, InactiveRole = 5;
|
|
|
|
// ---------------------------------------------------------------- helpers
|
|
|
|
private sealed class FakeUser(int userId = 10, int? roleId = null, bool isAdmin = false) : ICurrentUser
|
|
{
|
|
public int AuditUserId { get; set; } = userId;
|
|
public int? RoleId { get; set; } = roleId;
|
|
public string? RoleCode { get; set; }
|
|
public bool IsAdmin { get; set; } = isAdmin;
|
|
public string UserId => AuditUserId.ToString();
|
|
public bool IsAuthenticated => true;
|
|
public int? BranchId => null;
|
|
}
|
|
|
|
private static async Task<ErpDbContext> SeedAsync(bool withDefault = true)
|
|
{
|
|
var db = CreateContext();
|
|
db.Roles.AddRange(
|
|
new Role { RoleId = AdminRole, Code = "Admin", Name = "Administrator", IsAdmin = true, IsSystemRole = true, CreatedAt = DateTime.UtcNow },
|
|
new Role { RoleId = ApproverRole, Code = "Approver", Name = "Approver", CreatedAt = DateTime.UtcNow },
|
|
new Role { RoleId = ViewerRole, Code = "Viewer", Name = "Viewer", CreatedAt = DateTime.UtcNow },
|
|
new Role { RoleId = NoAccessRole, Code = "NoAccess", Name = "No access", IsSystemRole = true, CreatedAt = DateTime.UtcNow },
|
|
new Role { RoleId = InactiveRole, Code = "Retired", Name = "Retired", Status = EntityStatus.Inactive, CreatedAt = DateTime.UtcNow });
|
|
|
|
db.NavItems.Add(new NavItem { NavItemId = 100, Code = "procurement", Label = "Procurement" });
|
|
db.SubNavItems.Add(new SubNavItem { SubNavItemId = 101, NavItemId = 100, Code = "procurement.vendors", Label = "Vendors" });
|
|
db.NavItems.Add(new NavItem { NavItemId = 102, Code = "stock", Label = "Stock" });
|
|
db.Permissions.AddRange(
|
|
new Permission { PermissionId = 200, Code = "NAV:procurement", NavItemId = 100 },
|
|
new Permission { PermissionId = 201, Code = "NAV:procurement.vendors", SubNavItemId = 101 },
|
|
new Permission { PermissionId = 202, Code = "NAV:stock", NavItemId = 102 });
|
|
db.RolePermissions.AddRange(
|
|
new RolePermission { RoleId = ApproverRole, PermissionId = 200, Actions = RolePermissionAction.View | RolePermissionAction.Approve },
|
|
new RolePermission { RoleId = ViewerRole, PermissionId = 201, Actions = RolePermissionAction.View },
|
|
new RolePermission { RoleId = InactiveRole, PermissionId = 202, Actions = RolePermissionAction.All });
|
|
|
|
if (withDefault) db.AccessConfig.Add(new AccessConfig { ConfigId = AccessConfig.SingletonId, DefaultRoleId = NoAccessRole });
|
|
await db.SaveChangesAsync();
|
|
return db;
|
|
}
|
|
|
|
private static ClaimsPrincipal Principal(Guid authUserId, string? tokenRoleCode = null)
|
|
{
|
|
var claims = new List<Claim> { new("UserId", authUserId.ToString()), new("NIC", "N-1") };
|
|
if (tokenRoleCode is not null) claims.Add(new Claim("RoleCode", tokenRoleCode));
|
|
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
|
}
|
|
|
|
private static ShadowUserClaimsTransformation Transformer(ErpDbContext db, bool legacyFallback = true) =>
|
|
new(db, new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?> { ["Auth:LegacyRoleClaimFallback"] = legacyFallback.ToString() })
|
|
.Build());
|
|
|
|
private static string? Claim(ClaimsPrincipal p, string type) => p.FindFirstValue(type);
|
|
|
|
private static User NewUser(Guid authId, int? roleId, EntityStatus status = EntityStatus.Active, int id = 0) => new()
|
|
{
|
|
UserId = id == 0 ? Random.Shared.Next(100, 100000) : id,
|
|
AuthUserId = authId, Username = $"u-{authId:N}", DisplayName = "Someone", RoleId = roleId, Status = status
|
|
};
|
|
|
|
private static NavPermissionService NavService(ErpDbContext db) =>
|
|
new(new Repository<Role>(db), new Repository<RolePermission>(db), new MemoryCache(new MemoryCacheOptions()));
|
|
|
|
private static RoleService RoleSvc(ErpDbContext db, ICurrentUser user, NavPermissionService nav) =>
|
|
new(new Repository<Role>(db), new Repository<RolePermission>(db), new Repository<Permission>(db), new Repository<Employee>(db),
|
|
new Repository<User>(db), new Repository<AccessConfig>(db), user, nav, new FakeBranchConfig(), new PassThroughUnitOfWork(db));
|
|
|
|
// The AuthHex client, link and employee services are not touched by role/status changes (users here have no AuthUserId).
|
|
private static UserManagementService UserSvc(ErpDbContext db, ICurrentUser user) =>
|
|
new(new Repository<User>(db), new Repository<Role>(db), new Repository<Employee>(db), null!, null!, null!, user, new PassThroughUnitOfWork(db));
|
|
|
|
// ---------------------------------------------------------------- claims: the role comes from the ERP
|
|
|
|
[Fact]
|
|
public async Task Role_IsReadFromTheErpTable_NotFromTheTokensRoleClaim()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, ApproverRole));
|
|
await db.SaveChangesAsync();
|
|
|
|
// The token still says "Admin" (an old AuthHex token). The ERP says Approver, and the ERP wins.
|
|
var principal = await Transformer(db).TransformAsync(Principal(auth, tokenRoleCode: "Admin"));
|
|
|
|
Assert.Equal(ApproverRole.ToString(), Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Equal("Approver", Claim(principal, AuthHexClaims.ErpRoleCode));
|
|
Assert.Equal("false", Claim(principal, AuthHexClaims.ErpIsAdmin));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Admin_IsTheRoleFlag_NotTheRoleName()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var boss = Guid.NewGuid();
|
|
var impostor = Guid.NewGuid();
|
|
db.Roles.Add(new Role { RoleId = 20, Code = "Boss", Name = "The boss", IsAdmin = true, CreatedAt = DateTime.UtcNow });
|
|
db.Roles.Add(new Role { RoleId = 21, Code = "ADMIN2", Name = "admin", CreatedAt = DateTime.UtcNow }); // named like an admin, is not
|
|
db.Users.AddRange(NewUser(boss, 20), NewUser(impostor, 21));
|
|
await db.SaveChangesAsync();
|
|
|
|
Assert.Equal("true", Claim(await Transformer(db).TransformAsync(Principal(boss)), AuthHexClaims.ErpIsAdmin));
|
|
Assert.Equal("false", Claim(await Transformer(db).TransformAsync(Principal(impostor)), AuthHexClaims.ErpIsAdmin));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RenamingTheAdminRole_DoesNotCostAnyoneTheirAdminRights()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, AdminRole));
|
|
await db.SaveChangesAsync();
|
|
|
|
db.Roles.Single(r => r.RoleId == AdminRole).Code = "Owners";
|
|
await db.SaveChangesAsync();
|
|
|
|
var principal = await Transformer(db).TransformAsync(Principal(auth));
|
|
Assert.Equal("true", Claim(principal, AuthHexClaims.ErpIsAdmin));
|
|
Assert.Equal("Owners", Claim(principal, AuthHexClaims.ErpRoleCode));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FirstSignIn_CreatesTheUser_AndGivesItTheDefaultRole()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
|
|
var principal = await Transformer(db).TransformAsync(Principal(auth));
|
|
|
|
var user = Assert.Single(db.Users.Where(u => u.AuthUserId == auth));
|
|
Assert.Equal(NoAccessRole, user.RoleId); // saved, so the Users screen shows it
|
|
Assert.Equal(NoAccessRole.ToString(), Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Equal("false", Claim(principal, AuthHexClaims.ErpIsAdmin));
|
|
Assert.Equal(user.UserId.ToString(), Claim(principal, ClaimTypes.NameIdentifier));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WithNoDefaultRole_APersonWithoutARoleHasNoRoleClaims()
|
|
{
|
|
await using var db = await SeedAsync(withDefault: false);
|
|
var principal = await Transformer(db).TransformAsync(Principal(Guid.NewGuid()));
|
|
|
|
Assert.Null(Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Null(Claim(principal, AuthHexClaims.ErpIsAdmin));
|
|
Assert.NotNull(Claim(principal, ClaimTypes.NameIdentifier)); // still a known user
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LegacyFallback_UsesTheTokensRole_ForARolelessUser_AndSavesIt()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, roleId: null));
|
|
await db.SaveChangesAsync();
|
|
|
|
var principal = await Transformer(db, legacyFallback: true).TransformAsync(Principal(auth, "Approver"));
|
|
|
|
Assert.Equal(ApproverRole.ToString(), Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Equal(ApproverRole, db.Users.Single(u => u.AuthUserId == auth).RoleId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LegacyFallbackOff_IgnoresTheTokenAndUsesTheDefault()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, roleId: null));
|
|
await db.SaveChangesAsync();
|
|
|
|
var principal = await Transformer(db, legacyFallback: false).TransformAsync(Principal(auth, "Admin"));
|
|
|
|
Assert.Equal(NoAccessRole.ToString(), Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Equal("false", Claim(principal, AuthHexClaims.ErpIsAdmin));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnAssignedRole_IsNeverOverwrittenByTheFallbackOrTheDefault()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, ViewerRole));
|
|
await db.SaveChangesAsync();
|
|
|
|
var principal = await Transformer(db).TransformAsync(Principal(auth, "Admin"));
|
|
|
|
Assert.Equal(ViewerRole.ToString(), Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Equal(ViewerRole, db.Users.Single(u => u.AuthUserId == auth).RoleId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InactiveUser_IsBlocked_AndGetsNoRole()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, AdminRole, EntityStatus.Inactive));
|
|
await db.SaveChangesAsync();
|
|
|
|
var principal = await Transformer(db).TransformAsync(Principal(auth));
|
|
|
|
Assert.Equal("1", Claim(principal, AuthHexClaims.ErpBlocked));
|
|
Assert.Null(Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Null(Claim(principal, AuthHexClaims.ErpIsAdmin));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InactiveRole_GrantsNothing()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
db.Users.Add(NewUser(auth, InactiveRole));
|
|
await db.SaveChangesAsync();
|
|
|
|
var principal = await Transformer(db).TransformAsync(Principal(auth));
|
|
|
|
Assert.Null(Claim(principal, AuthHexClaims.ErpRoleId));
|
|
Assert.Null(Claim(principal, AuthHexClaims.ErpBlocked)); // not blocked — just no permissions
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Branch_ComesFromTheLinkedEmployee_AndTransformingTwiceAddsNothingNew()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
var user = NewUser(auth, ViewerRole, id: 500);
|
|
db.Users.Add(user);
|
|
db.Employees.Add(new Employee { EmployeeId = 1, EmployeeCode = "E1", FullName = "Emp", UserId = 500, BranchId = Branch2 });
|
|
await db.SaveChangesAsync();
|
|
|
|
var transformer = Transformer(db);
|
|
var principal = await transformer.TransformAsync(Principal(auth));
|
|
var claimsAfterFirst = principal.Claims.Count();
|
|
principal = await transformer.TransformAsync(principal);
|
|
|
|
Assert.Equal(Branch2.ToString(), Claim(principal, AuthHexClaims.BranchId));
|
|
Assert.Equal(claimsAfterFirst, principal.Claims.Count());
|
|
}
|
|
|
|
// ---------------------------------------------------------------- permissions by role id
|
|
|
|
[Fact]
|
|
public async Task Nav_AccessAndActions_AreDecidedByTheRolesPermissions()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var nav = NavService(db);
|
|
|
|
Assert.True(await nav.HasAccessAsync(ApproverRole, "procurement"));
|
|
Assert.True(await nav.HasActionAsync(ApproverRole, "procurement", RolePermissionAction.Approve));
|
|
Assert.False(await nav.HasActionAsync(ApproverRole, "procurement", RolePermissionAction.Delete));
|
|
Assert.False(await nav.HasAccessAsync(ApproverRole, "stock"));
|
|
|
|
// A parent module is reachable through a child grant; an action is exact.
|
|
Assert.True(await nav.HasAccessAsync(ViewerRole, "procurement"));
|
|
Assert.False(await nav.HasActionAsync(ViewerRole, "procurement", RolePermissionAction.View));
|
|
Assert.True(await nav.HasActionAsync(ViewerRole, "procurement.vendors", RolePermissionAction.View));
|
|
|
|
Assert.False(await nav.HasAccessAsync(null, "procurement"));
|
|
Assert.False(await nav.HasAccessAsync(999, "procurement"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Nav_InactiveRole_HasNoAccess()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
Assert.False(await NavService(db).HasAccessAsync(InactiveRole, "stock"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditingARolesPermissions_AppliesImmediately_NotAfterACacheTimeout()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var nav = NavService(db);
|
|
var roles = RoleSvc(db, new FakeUser(roleId: AdminRole, isAdmin: true), nav);
|
|
|
|
Assert.False(await nav.HasAccessAsync(ViewerRole, "stock")); // cached as "no"
|
|
await roles.AssignPermissionsAsync(ViewerRole, new Dtos.Rbac.AssignRolePermissionsRequest { NavItemIds = [102] });
|
|
|
|
Assert.True(await nav.HasAccessAsync(ViewerRole, "stock")); // visible straight away
|
|
}
|
|
|
|
// ---------------------------------------------------------------- /auth/me
|
|
|
|
[Fact]
|
|
public async Task Me_ForAnAdministrator_ListsEveryPermission_EvenOnesNeverGranted()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var me = await RoleSvc(db, new FakeUser(roleId: AdminRole, isAdmin: true), NavService(db)).GetMeAsync();
|
|
|
|
Assert.True(me.HasRole);
|
|
Assert.True(me.IsAdmin);
|
|
Assert.Equal(AdminRole, me.RoleId);
|
|
Assert.Contains("procurement.vendors", me.NavCodes);
|
|
Assert.Contains("stock", me.NavCodes);
|
|
Assert.Equal((int)RolePermissionAction.All, me.Permissions!["stock"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Me_ForAnOrdinaryRole_ListsOnlyItsGrants_WithTheirActions()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var me = await RoleSvc(db, new FakeUser(roleId: ApproverRole), NavService(db)).GetMeAsync();
|
|
|
|
Assert.True(me.HasRole);
|
|
Assert.False(me.IsAdmin);
|
|
Assert.Equal(["procurement"], me.NavCodes);
|
|
Assert.Equal((int)(RolePermissionAction.View | RolePermissionAction.Approve), me.Permissions!["procurement"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Me_WithoutARole_SaysSo_InsteadOfReturningAnEmptyMenu()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var me = await RoleSvc(db, new FakeUser(roleId: null), NavService(db)).GetMeAsync();
|
|
|
|
Assert.False(me.HasRole);
|
|
Assert.Empty(me.NavCodes);
|
|
Assert.Empty(me.Permissions!);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- role management guards
|
|
|
|
[Fact]
|
|
public async Task TheAdministratorSystemAndDefaultRoles_CannotBeDeactivatedOrDeleted()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var roles = RoleSvc(db, new FakeUser(roleId: AdminRole, isAdmin: true), NavService(db));
|
|
|
|
await Assert.ThrowsAsync<DomainException>(() => roles.SetStatusAsync(AdminRole, EntityStatus.Inactive));
|
|
await Assert.ThrowsAsync<DomainException>(() => roles.DeleteAsync(AdminRole));
|
|
await Assert.ThrowsAsync<DomainException>(() => roles.DeleteAsync(NoAccessRole)); // system role and the default
|
|
await Assert.ThrowsAsync<DomainException>(() => roles.SetStatusAsync(NoAccessRole, EntityStatus.Inactive));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ARoleInUse_CannotBeDeactivated_ButAnUnusedOneCanBeDeleted()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
db.Users.Add(NewUser(Guid.NewGuid(), ViewerRole));
|
|
await db.SaveChangesAsync();
|
|
var roles = RoleSvc(db, new FakeUser(roleId: AdminRole, isAdmin: true), NavService(db));
|
|
|
|
await Assert.ThrowsAsync<DomainException>(() => roles.SetStatusAsync(ViewerRole, EntityStatus.Inactive));
|
|
await roles.DeleteAsync(ApproverRole);
|
|
Assert.False(db.Roles.Any(r => r.RoleId == ApproverRole));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreatedRoles_NeverCarryTheAdminFlag()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var roles = RoleSvc(db, new FakeUser(roleId: AdminRole, isAdmin: true), NavService(db));
|
|
|
|
var created = (await roles.CreateAsync(new Dtos.Rbac.CreateRoleRequest { Code = "Clerk", Name = "Clerk" })).Value;
|
|
|
|
Assert.False(created.IsAdmin);
|
|
Assert.False(created.IsSystemRole);
|
|
Assert.Null(db.Roles.Single(r => r.RoleId == created.RoleId).AuthRoleId);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- user management guards
|
|
|
|
[Fact]
|
|
public async Task ChangingAUsersRole_TakesEffectOnTheirNextRequest_WithNoRelogin()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var auth = Guid.NewGuid();
|
|
var user = NewUser(auth, ViewerRole, id: 700);
|
|
db.Users.Add(user);
|
|
await db.SaveChangesAsync();
|
|
Assert.Equal(ViewerRole.ToString(), Claim(await Transformer(db).TransformAsync(Principal(auth)), AuthHexClaims.ErpRoleId));
|
|
|
|
await UserSvc(db, new FakeUser(1, AdminRole, true)).UpdateRoleAsync(700, new UpdateUserRoleRequest { RoleId = ApproverRole });
|
|
|
|
// A fresh request for the same login now carries the new role.
|
|
Assert.Equal(ApproverRole.ToString(), Claim(await Transformer(db).TransformAsync(Principal(auth)), AuthHexClaims.ErpRoleId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheLastActiveAdministrator_CannotBeDemotedOrDeactivated()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
db.Users.Add(NewUser(Guid.NewGuid(), AdminRole, id: 801));
|
|
await db.SaveChangesAsync();
|
|
var svc = UserSvc(db, new FakeUser(userId: 1, roleId: AdminRole, isAdmin: true));
|
|
|
|
var demote = await Assert.ThrowsAsync<DomainException>(() => svc.UpdateRoleAsync(801, new UpdateUserRoleRequest { RoleId = ViewerRole }));
|
|
var deactivate = await Assert.ThrowsAsync<DomainException>(() => svc.SetStatusAsync(801, EntityStatus.Inactive));
|
|
|
|
Assert.Equal(409, demote.StatusCode);
|
|
Assert.Equal(409, deactivate.StatusCode);
|
|
Assert.Equal(AdminRole, db.Users.Single(u => u.UserId == 801).RoleId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnAdministrator_CanBeDemoted_WhenAnotherActiveAdminRemains()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
db.Users.AddRange(NewUser(Guid.NewGuid(), AdminRole, id: 801), NewUser(Guid.NewGuid(), AdminRole, id: 802));
|
|
await db.SaveChangesAsync();
|
|
|
|
await UserSvc(db, new FakeUser(userId: 802, roleId: AdminRole, isAdmin: true))
|
|
.UpdateRoleAsync(801, new UpdateUserRoleRequest { RoleId = ViewerRole });
|
|
|
|
Assert.Equal(ViewerRole, db.Users.Single(u => u.UserId == 801).RoleId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Nobody_CanRemoveTheirOwnAdminAccess_OrDeactivateThemselves()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
db.Users.AddRange(NewUser(Guid.NewGuid(), AdminRole, id: 801), NewUser(Guid.NewGuid(), AdminRole, id: 802));
|
|
await db.SaveChangesAsync();
|
|
var svc = UserSvc(db, new FakeUser(userId: 801, roleId: AdminRole, isAdmin: true));
|
|
|
|
await Assert.ThrowsAsync<DomainException>(() => svc.UpdateRoleAsync(801, new UpdateUserRoleRequest { RoleId = ViewerRole }));
|
|
await Assert.ThrowsAsync<DomainException>(() => svc.SetStatusAsync(801, EntityStatus.Inactive));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AUser_CannotBeGivenAnInactiveRole()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
db.Users.Add(NewUser(Guid.NewGuid(), ViewerRole, id: 900));
|
|
await db.SaveChangesAsync();
|
|
|
|
await Assert.ThrowsAsync<DomainException>(
|
|
() => UserSvc(db, new FakeUser(1, AdminRole, true)).UpdateRoleAsync(900, new UpdateUserRoleRequest { RoleId = InactiveRole }));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- access config (default role)
|
|
|
|
private static AccessConfigService AccessSvc(ErpDbContext db) =>
|
|
new(new Repository<AccessConfig>(db), new Repository<Role>(db), new FakeUser(), new PassThroughUnitOfWork(db));
|
|
|
|
[Fact]
|
|
public async Task TheDefaultRole_CanNeverBeAnAdministratorOrAnInactiveRole()
|
|
{
|
|
await using var db = await SeedAsync();
|
|
var svc = AccessSvc(db);
|
|
var etag = db.AccessConfig.Single().RowVersion;
|
|
|
|
await Assert.ThrowsAsync<DomainException>(() => svc.UpdateAsync(new UpdateAccessConfigRequest { DefaultRoleId = AdminRole }, etag));
|
|
await Assert.ThrowsAsync<DomainException>(() => svc.UpdateAsync(new UpdateAccessConfigRequest { DefaultRoleId = InactiveRole }, etag));
|
|
await Assert.ThrowsAsync<NotFoundException>(() => svc.UpdateAsync(new UpdateAccessConfigRequest { DefaultRoleId = 999 }, etag));
|
|
Assert.Equal(NoAccessRole, db.AccessConfig.Single().DefaultRoleId);
|
|
|
|
var changed = (await svc.UpdateAsync(new UpdateAccessConfigRequest { DefaultRoleId = ViewerRole }, etag)).Value;
|
|
Assert.Equal(("Viewer", ViewerRole), (changed.DefaultRoleName, changed.DefaultRoleId));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- seeding
|
|
|
|
[Fact]
|
|
public async Task Seeder_EnsuresAnAdminRoleAndALeastPrivilegeDefault_AndIsIdempotent()
|
|
{
|
|
await using var db = CreateContext();
|
|
db.Database.EnsureCreated();
|
|
|
|
await DataSeeder.SeedAsync(db);
|
|
await DataSeeder.SeedAsync(db);
|
|
|
|
var admin = Assert.Single(db.Roles.Where(r => r.IsAdmin));
|
|
Assert.True(admin.IsSystemRole);
|
|
Assert.Equal(db.Permissions.Count(), db.RolePermissions.Count(rp => rp.RoleId == admin.RoleId));
|
|
var noAccess = Assert.Single(db.Roles.Where(r => r.Code == DataSeeder.DefaultNoAccessRoleCode));
|
|
Assert.Empty(db.RolePermissions.Where(rp => rp.RoleId == noAccess.RoleId));
|
|
Assert.Equal(noAccess.RoleId, Assert.Single(db.AccessConfig).DefaultRoleId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Seeder_FlagsAnExistingRoleCalledAdmin_InsteadOfAddingASecond_AndKeepsAChosenDefault()
|
|
{
|
|
await using var db = CreateContext();
|
|
db.Database.EnsureCreated();
|
|
db.Roles.Add(new Role { RoleId = 50, Code = "ADMIN", Name = "Old admin", CreatedAt = DateTime.UtcNow });
|
|
db.Roles.Add(new Role { RoleId = 51, Code = "Reader", Name = "Reader", CreatedAt = DateTime.UtcNow });
|
|
await db.SaveChangesAsync();
|
|
|
|
await DataSeeder.SeedAsync(db);
|
|
db.AccessConfig.Single().DefaultRoleId = 51; // an administrator's own choice
|
|
await db.SaveChangesAsync();
|
|
await DataSeeder.SeedAsync(db);
|
|
|
|
Assert.True(db.Roles.Single(r => r.RoleId == 50).IsAdmin);
|
|
Assert.Equal(1, db.Roles.Count(r => r.IsAdmin));
|
|
Assert.Equal(51, db.AccessConfig.Single().DefaultRoleId);
|
|
}
|
|
}
|