Files
pulse-signage/test/rbac-permissions.test.js

195 lines
9.5 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const rbacPermissionsScript = fs.readFileSync(require.resolve('../src/web/public/js/rbac/permissions.js'), 'utf8');
const rbacFormTemplate = fs.readFileSync(require.resolve('../src/web/views/settings/rbac/form.hbs'), 'utf8');
const rbacRoutesSource = fs.readFileSync(require.resolve('../src/web/routes/admin/rbac.js'), 'utf8');
const { buildPermissionSections, buildRbacEditViewModel } = require('../src/web/routes/settings/rbac/form-view-model');
const rbacFormViewModel = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/form-view-model.js'), 'utf8');
const rbacSource = fs.readFileSync(require.resolve('../src/rbac.js'), 'utf8');
for (const create of [true, false]) {
for (const enabled of [true, false]) {
test((create ? 'create' : 'update') + ' role persists two-factor requirement ' + enabled, async () => {
require('../src/common');
const registerRbacRoutes = require('../src/web/routes/admin/rbac');
const handlers = {};
const writes = [];
let auditEvent;
const connection = {
async beginTransaction() {},
async commit() {},
async rollback() {},
release() {},
async query(sql, params) { writes.push({ sql, params }); return [{ insertId: 9 }]; }
};
registerRbacRoutes({
get() {},
post(path, ...routeHandlers) { handlers[path] = routeHandlers; }
}, {
pool: { async query() { return [[]]; }, async getConnection() { return connection; } },
common: {
validateMaxLength: value => value,
getSearchQuery: () => '',
getSortQuery: () => '',
getSortDirectionQuery: () => '',
fetchDuplicateName: async () => null
},
pages: {},
requirePermission: () => (_req, _res, next) => next(),
getAuditUserId: () => 3,
recordRequestAuditEvent: async (_pool, _req, event) => { auditEvent = event; },
readArrayField: () => [],
normalizePermissionKeys: value => value,
permissions: [],
rbacData: {
fetchRoleById: async () => ({ id: 9, name: 'Secure', description: '', require_two_factor: enabled ? 0 : 1 }),
fetchPermissions: async () => [],
fetchUsersWithRolesPage: async () => ({ users: [], currentPage: 1, totalItems: 0 }),
fetchUsersWithRoles: async () => [],
syncRoleUsers: async () => {}
}
});
const response = { redirect(url) { this.redirectedTo = url; } };
await handlers[create ? '/settings/roles' : '/settings/roles/:id'][1]({
params: { id: '9' }, query: {}, currentUser: { id: 3 },
body: { name: 'Secure', description: '', ...(enabled ? { require_two_factor: '1' } : {}) }
}, response, error => { throw error; });
assert.equal(writes.length, 1);
assert.match(writes[0].sql, /require_two_factor/);
assert.equal(writes[0].params[create ? 3 : 2], enabled ? 1 : 0);
assert.match(response.redirectedTo, /^\/settings\/roles\/9\/edit/);
if (create) assert.equal(auditEvent.details.requireTwoFactor, enabled);
else assert.deepEqual(auditEvent.details.changes.requireTwoFactor, { from: !enabled, to: enabled });
});
}
}
test('duplicating a role retains its two-factor requirement and the editor renders the toggle', () => {
require('../src/common');
const { buildDuplicateRole } = require('../src/web/routes/settings/rbac/duplicate');
const pages = require('../src/web/pages');
for (const enabled of [true, false]) {
const role = { id: 9, name: 'Secure', require_two_factor: enabled };
assert.equal(buildDuplicateRole(role).require_two_factor, enabled);
const html = pages.renderRbacEditPage(role, '', null, [], [], null);
const toggle = html.match(/<input id="role-require-two-factor"[^>]+>/)[0];
assert.equal(toggle.includes('checked'), enabled);
}
});
test('rbac form exposes bulk permission controls', () => {
assert.match(rbacFormTemplate, /name="permissions_present" value="1"/);
assert.match(rbacFormTemplate, /name="user_ids_present\[\]" value="\{\{id\}\}"/);
assert.match(rbacFormTemplate, /rbac-permissions-table-head/);
assert.ok(!rbacFormTemplate.includes('table-light'));
assert.match(rbacFormTemplate, /<h3 class="card-title">Permissions<\/h3>[\s\S]*<div class="card-body p-0">[\s\S]*accordion accordion-flush/);
assert.match(rbacFormTemplate, /card card-outline card-secondary admin-form-card overflow-hidden[\s\S]*<h3 class="card-title">Permissions<\/h3>/);
assert.ok(!rbacFormTemplate.includes('card card-outline card-secondary overflow-hidden'));
assert.match(rbacFormTemplate, /accordion-item overflow-hidden" data-permission-section/);
assert.match(rbacFormTemplate, /accordion-body p-0/);
assert.match(rbacFormTemplate, /data-bs-parent="#role-permissions-accordion"/);
assert.match(rbacFormTemplate, /table-layout: fixed; width: 100%;/);
assert.match(rbacFormTemplate, /<col style="width: 5\.5rem;" \/>/);
assert.match(rbacFormTemplate, /class="text-center"/);
assert.match(rbacFormTemplate, /data-permission-row-toggle/);
assert.ok(rbacFormTemplate.includes('text-reset'));
assert.ok(rbacFormTemplate.includes('color: inherit;'));
assert.ok(rbacFormTemplate.includes('text-decoration-style: dotted'));
assert.ok(rbacFormTemplate.includes('aria-label="Role actions"'));
assert.ok(!rbacFormTemplate.includes('aria-label="Permission actions"'));
assert.ok(!rbacFormTemplate.includes('showSaveAndClose=false'));
assert.ok(!rbacFormTemplate.includes('showSaveAndNew=false'));
assert.ok(!rbacFormTemplate.includes('Select none'));
assert.ok(!rbacFormTemplate.includes('permission categories'));
assert.ok(!rbacFormTemplate.includes('btn-link'));
assert.ok(!rbacFormTemplate.includes('rounded-bottom-3 overflow-hidden'));
assert.ok(rbacFormViewModel.includes("label: 'Read'"));
assert.ok(rbacFormViewModel.includes("label: 'Create'"));
assert.ok(rbacFormViewModel.includes("label: 'Update'"));
assert.ok(rbacFormViewModel.includes("label: 'Delete'"));
assert.ok(rbacFormViewModel.includes("label: 'Allow'"));
assert.ok(rbacRoutesSource.includes('resourceOrder: Number(permission.resourceOrder) || 999'));
assert.ok(rbacFormViewModel.includes('section.actions = actionDefinitions.map'));
assert.ok(rbacFormViewModel.includes('data-async-save-close-url="/settings/roles" data-async-save-new-url="/settings/roles/new"'));
assert.ok(rbacFormViewModel.includes('cancelConfirmMessage: "You\'ve made changes. Are you sure you want to leave this page?"'));
assert.match(rbacFormTemplate, /cancelConfirmMessage=cancelConfirmMessage/);
assert.ok(rbacSource.includes("sectionName: 'Main Navigation'"));
assert.ok(rbacSource.includes("permissions: ["));
assert.ok(rbacSource.includes("resourceOrder: resource.order"));
});
test('rbac permissions script supports bulk permission selection', () => {
assert.ok(rbacPermissionsScript.includes('function setRowCheckboxes(row, checked)'));
assert.ok(rbacPermissionsScript.includes('function toggleRowCheckboxes(row)'));
assert.ok(rbacPermissionsScript.includes('data-permission-row-toggle'));
assert.match(rbacPermissionsScript, /toggleRowCheckboxes\(findPermissionRow\(targetId\)\);\s*markPermissionFormDirty\(control\);/);
assert.ok(!rbacPermissionsScript.includes('data-permission-select-none'));
assert.ok(!rbacPermissionsScript.includes('data-permission-section-select-none'));
});
test('rbac accordion sections follow configured order', () => {
const sections = buildPermissionSections([
{
categoryName: 'Settings',
sectionIndex: 0,
sectionOrder: 40,
resourceIndex: 1,
resourceOrder: 20,
title: 'Later resource',
permissions: []
},
{
categoryName: 'Main navigation',
sectionIndex: 3,
sectionOrder: 10,
resourceIndex: 0,
resourceOrder: 20,
title: 'Clients',
permissions: []
},
{
categoryName: 'Main navigation',
sectionIndex: 0,
sectionOrder: 10,
resourceIndex: 1,
resourceOrder: 10,
title: 'Dashboard',
permissions: []
}
]);
assert.deepEqual(sections.map(function (section) {
return section.title;
}), ['Main navigation', 'Settings']);
assert.deepEqual(sections[0].groups.map(function (group) {
return group.title;
}), ['Dashboard', 'Clients']);
});
test('rbac duplicate route exists', () => {
const rbacRoutes = fs.readFileSync(require.resolve('../src/web/routes/admin/rbac.js'), 'utf8');
const duplicateHelpers = fs.readFileSync(require.resolve('../src/web/routes/settings/rbac/duplicate.js'), 'utf8');
assert.match(rbacRoutes, /\/settings\/roles\/:id\/duplicate/);
assert.ok(rbacRoutes.includes("requirePermission('rbac.create')"));
assert.ok(duplicateHelpers.includes('buildDuplicateRoleName'));
assert.ok(duplicateHelpers.includes('buildDuplicateRole'));
});
test('built-in Super Admin role cannot be deleted but can be renamed', () => {
const model = buildRbacEditViewModel({
id: 1,
role_key: 'super-admin',
name: 'Super Admin',
inUse: false
}, '', null, [], [], null);
assert.equal(model.footerDeleteDisabled, true);
assert.equal(model.role.isProtected, true);
assert.equal(model.footerDeleteTitle, 'The built-in Super Admin role cannot be deleted.');
assert.equal(model.footerDeleteConfirmMessage, 'Delete Super Admin?');
assert.ok(!rbacFormTemplate.includes('The built-in role name cannot be changed.'));
assert.ok(!rbacFormTemplate.includes('The built-in role description cannot be changed.'));
});