Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/frontend/src/lib/utils/permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { describe, expect, it } from 'vitest';
import type { User } from '$lib/types';
import { hasAnyPermission, hasPermission, isSuperAdmin, Permissions } from './permissions';

/** Creates a minimal User object for testing. */
function makeUser(overrides: Partial<User> = {}): User {
return {
id: '00000000-0000-0000-0000-000000000001',
username: 'test@example.com',
email: 'test@example.com',
roles: [],
permissions: [],
...overrides
};
}

// ── isSuperAdmin ────────────────────────────────────────────────────

describe('isSuperAdmin', () => {
it('returns true when user has SuperAdmin role', () => {
const user = makeUser({ roles: ['SuperAdmin'] });
expect(isSuperAdmin(user)).toBe(true);
});

it('returns true when SuperAdmin is among multiple roles', () => {
const user = makeUser({ roles: ['User', 'SuperAdmin', 'Admin'] });
expect(isSuperAdmin(user)).toBe(true);
});

it('returns false when user has Admin but not SuperAdmin', () => {
const user = makeUser({ roles: ['Admin'] });
expect(isSuperAdmin(user)).toBe(false);
});

it('returns false when user has no roles', () => {
const user = makeUser({ roles: [] });
expect(isSuperAdmin(user)).toBe(false);
});

it('returns false for null user', () => {
expect(isSuperAdmin(null)).toBe(false);
});

it('returns false for undefined user', () => {
expect(isSuperAdmin(undefined)).toBe(false);
});

it('returns false when roles property is undefined', () => {
const user = makeUser();
delete user.roles;
expect(isSuperAdmin(user)).toBe(false);
});
});

// ── hasPermission ───────────────────────────────────────────────────

describe('hasPermission', () => {
it('returns true when user has the exact permission', () => {
const user = makeUser({ permissions: [Permissions.Users.View] });
expect(hasPermission(user, Permissions.Users.View)).toBe(true);
});

it('returns false when user lacks the permission', () => {
const user = makeUser({ permissions: [Permissions.Users.View] });
expect(hasPermission(user, Permissions.Users.Manage)).toBe(false);
});

it('returns false when user has no permissions', () => {
const user = makeUser({ permissions: [] });
expect(hasPermission(user, Permissions.Users.View)).toBe(false);
});

it('SuperAdmin implicitly has any permission', () => {
const user = makeUser({ roles: ['SuperAdmin'], permissions: [] });
expect(hasPermission(user, Permissions.Users.Manage)).toBe(true);
expect(hasPermission(user, Permissions.Roles.Manage)).toBe(true);
expect(hasPermission(user, Permissions.Jobs.Manage)).toBe(true);
});

it('SuperAdmin implicitly has permissions even for unknown permission strings', () => {
const user = makeUser({ roles: ['SuperAdmin'], permissions: [] });
expect(hasPermission(user, 'some.custom.permission')).toBe(true);
});

it('returns false for null user', () => {
expect(hasPermission(null, Permissions.Users.View)).toBe(false);
});

it('returns false for undefined user', () => {
expect(hasPermission(undefined, Permissions.Users.View)).toBe(false);
});

it('returns false when permissions property is undefined', () => {
const user = makeUser();
delete user.permissions;
expect(hasPermission(user, Permissions.Users.View)).toBe(false);
});

it('non-SuperAdmin with explicit permission returns true', () => {
const user = makeUser({
roles: ['Admin'],
permissions: [Permissions.Users.View, Permissions.Users.Manage]
});
expect(hasPermission(user, Permissions.Users.Manage)).toBe(true);
});

it('does not grant permissions from a different permission string', () => {
const user = makeUser({ permissions: ['users.view'] });
expect(hasPermission(user, 'users.view_pii')).toBe(false);
});
});

// ── hasAnyPermission ────────────────────────────────────────────────

describe('hasAnyPermission', () => {
it('returns true when user has one of the requested permissions', () => {
const user = makeUser({ permissions: [Permissions.Users.View] });
expect(hasAnyPermission(user, [Permissions.Users.View, Permissions.Users.Manage])).toBe(true);
});

it('returns true when user has all of the requested permissions', () => {
const user = makeUser({
permissions: [Permissions.Users.View, Permissions.Users.Manage]
});
expect(hasAnyPermission(user, [Permissions.Users.View, Permissions.Users.Manage])).toBe(true);
});

it('returns false when user has none of the requested permissions', () => {
const user = makeUser({ permissions: [Permissions.Jobs.View] });
expect(hasAnyPermission(user, [Permissions.Users.View, Permissions.Users.Manage])).toBe(false);
});

it('returns false for empty permissions list', () => {
const user = makeUser({ permissions: [Permissions.Users.View] });
expect(hasAnyPermission(user, [])).toBe(false);
});

it('SuperAdmin implicitly satisfies any permission check', () => {
const user = makeUser({ roles: ['SuperAdmin'], permissions: [] });
expect(hasAnyPermission(user, [Permissions.Users.Manage, Permissions.Roles.Manage])).toBe(true);
});

it('returns false for null user', () => {
expect(hasAnyPermission(null, [Permissions.Users.View])).toBe(false);
});

it('returns false for undefined user', () => {
expect(hasAnyPermission(undefined, [Permissions.Users.View])).toBe(false);
});

it('returns false for null user even with empty permissions list', () => {
expect(hasAnyPermission(null, [])).toBe(false);
});
});

// ── Permissions constant ────────────────────────────────────────────

describe('Permissions constant', () => {
it('exposes Users permissions', () => {
expect(Permissions.Users.View).toBe('users.view');
expect(Permissions.Users.ViewPii).toBe('users.view_pii');
expect(Permissions.Users.Manage).toBe('users.manage');
expect(Permissions.Users.AssignRoles).toBe('users.assign_roles');
});

it('exposes Roles permissions', () => {
expect(Permissions.Roles.View).toBe('roles.view');
expect(Permissions.Roles.Manage).toBe('roles.manage');
});

it('exposes Jobs permissions', () => {
expect(Permissions.Jobs.View).toBe('jobs.view');
expect(Permissions.Jobs.Manage).toBe('jobs.manage');
});
});
173 changes: 173 additions & 0 deletions src/frontend/src/lib/utils/roles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { describe, expect, it } from 'vitest';
import { canManageUser, getAssignableRoles, getHighestRank, getRoleRank } from './roles';

describe('getRoleRank', () => {
it('returns 3 for SuperAdmin', () => {
expect(getRoleRank('SuperAdmin')).toBe(3);
});

it('returns 2 for Admin', () => {
expect(getRoleRank('Admin')).toBe(2);
});

it('returns 1 for User', () => {
expect(getRoleRank('User')).toBe(1);
});

it('returns 0 for an unknown role name', () => {
expect(getRoleRank('Moderator')).toBe(0);
});

it('returns 0 for an empty string', () => {
expect(getRoleRank('')).toBe(0);
});

it('is case-sensitive — lowercase variants return 0', () => {
expect(getRoleRank('superadmin')).toBe(0);
expect(getRoleRank('admin')).toBe(0);
expect(getRoleRank('user')).toBe(0);
});
});

describe('getHighestRank', () => {
it('returns the highest rank from mixed roles', () => {
expect(getHighestRank(['User', 'Admin'])).toBe(2);
});

it('returns 3 when SuperAdmin is present', () => {
expect(getHighestRank(['User', 'SuperAdmin', 'Admin'])).toBe(3);
});

it('returns 0 for an empty array', () => {
expect(getHighestRank([])).toBe(0);
});

it('returns 0 when all roles are unknown', () => {
expect(getHighestRank(['Moderator', 'Viewer'])).toBe(0);
});

it('returns correct rank for a single known role', () => {
expect(getHighestRank(['Admin'])).toBe(2);
});

it('ignores unknown roles and returns highest known rank', () => {
expect(getHighestRank(['Unknown', 'User', 'AnotherUnknown'])).toBe(1);
});
});

describe('canManageUser', () => {
it('SuperAdmin can manage Admin', () => {
expect(canManageUser(['SuperAdmin'], ['Admin'])).toBe(true);
});

it('SuperAdmin can manage User', () => {
expect(canManageUser(['SuperAdmin'], ['User'])).toBe(true);
});

it('Admin can manage User', () => {
expect(canManageUser(['Admin'], ['User'])).toBe(true);
});

it('Admin cannot manage SuperAdmin', () => {
expect(canManageUser(['Admin'], ['SuperAdmin'])).toBe(false);
});

it('User cannot manage Admin', () => {
expect(canManageUser(['User'], ['Admin'])).toBe(false);
});

it('User cannot manage User — equal rank is not sufficient', () => {
expect(canManageUser(['User'], ['User'])).toBe(false);
});

it('Admin cannot manage Admin — equal rank is not sufficient', () => {
expect(canManageUser(['Admin'], ['Admin'])).toBe(false);
});

it('SuperAdmin cannot manage SuperAdmin — equal rank is not sufficient', () => {
expect(canManageUser(['SuperAdmin'], ['SuperAdmin'])).toBe(false);
});

it('empty caller roles cannot manage anyone', () => {
expect(canManageUser([], ['User'])).toBe(false);
});

it('any role can manage a target with empty roles', () => {
expect(canManageUser(['User'], [])).toBe(true);
});

it('empty caller cannot manage empty target — both rank 0', () => {
expect(canManageUser([], [])).toBe(false);
});

it('uses highest role when caller has multiple roles', () => {
expect(canManageUser(['User', 'Admin'], ['User'])).toBe(true);
});

it('uses highest role when target has multiple roles', () => {
// Admin (rank 2) cannot manage target whose highest is SuperAdmin (rank 3)
expect(canManageUser(['Admin'], ['User', 'SuperAdmin'])).toBe(false);
});

it('unknown caller roles cannot manage known target roles', () => {
expect(canManageUser(['Moderator'], ['User'])).toBe(false);
});

it('known caller roles can manage unknown target roles', () => {
// User (rank 1) > unknown (rank 0)
expect(canManageUser(['User'], ['Moderator'])).toBe(true);
});
});

describe('getAssignableRoles', () => {
const allRoles = ['SuperAdmin', 'Admin', 'User'];

it('SuperAdmin can assign Admin and User', () => {
const result = getAssignableRoles(['SuperAdmin'], allRoles);
expect(result).toEqual(['Admin', 'User']);
});

it('Admin can assign only User', () => {
const result = getAssignableRoles(['Admin'], allRoles);
expect(result).toEqual(['User']);
});

it('User cannot assign any role', () => {
const result = getAssignableRoles(['User'], allRoles);
expect(result).toEqual([]);
});

it('empty caller roles yield no assignable roles', () => {
const result = getAssignableRoles([], allRoles);
expect(result).toEqual([]);
});

it('unknown caller role yields no assignable roles from standard set', () => {
const result = getAssignableRoles(['Moderator'], allRoles);
expect(result).toEqual([]);
});

it('filters from arbitrary allRoles list', () => {
// Admin (rank 2) can assign anything with rank < 2
const result = getAssignableRoles(['Admin'], ['User', 'Viewer', 'SuperAdmin']);
// User has rank 1 (< 2), Viewer has rank 0 (< 2), SuperAdmin has rank 3 (not < 2)
expect(result).toEqual(['User', 'Viewer']);
});

it('returns empty when allRoles is empty', () => {
const result = getAssignableRoles(['SuperAdmin'], []);
expect(result).toEqual([]);
});

it('never includes the caller own rank level', () => {
// SuperAdmin (rank 3) should not be able to assign SuperAdmin (rank 3)
const result = getAssignableRoles(['SuperAdmin'], ['SuperAdmin']);
expect(result).toEqual([]);
});

it('multi-role caller uses highest rank for filtering', () => {
// Caller has User + Admin, highest is Admin (rank 2)
const result = getAssignableRoles(['User', 'Admin'], allRoles);
expect(result).toEqual(['User']);
});
});