195 lines
9.5 KiB
JavaScript
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.'));
|
|
}); |