Files
pulse-signage/test/view-as.test.js
T
lzstealth 9c28eca3ef
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m47s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s
Release v2.17.0: add read-only user previews and access fallbacks
2026-10-23 01:55:33 +01:00

153 lines
6.6 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const pages = require('../src/web/pages');
const registerViewAsRoutes = require('../src/web/routes/settings/view-as');
const renderUsersPage = require('../src/web/routes/settings/users/list');
const renderUsersEditPage = require('../src/web/routes/settings/users/edit');
const renderRssFeedEditPage = require('../src/web/routes/data-sources/rss-feeds/edit');
const renderWeatherLocationEditPage = require('../src/web/routes/data-sources/weather/edit');
const { hasAssignedPermission, hasPermission, PERMISSIONS } = require('../src/rbac');
test('preview permission is Impersonate > Allow and read-only execution stays denied', () => {
const permission = PERMISSIONS.find(function (item) { return item.key === 'impersonate.allow'; });
assert.ok(permission);
assert.equal(permission.resourceName, 'Impersonate');
assert.equal(permission.actionName, 'Allow');
assert.ok(!PERMISSIONS.some(function (item) {
return item.key === 'users.allow' || item.key === 'users.impersonate' || item.key === 'dashboard.allow';
}));
const previewUser = { isViewingAs: true, permissionKeys: ['clients.allow'] };
assert.equal(hasAssignedPermission(previewUser, 'clients.allow'), true);
assert.equal(hasPermission(previewUser, 'clients.allow'), false);
assert.equal(hasPermission({ permissionKeys: ['clients.allow'] }, 'clients.allow'), true);
});
test('View as is on the user edit footer, not the Users table', () => {
const user = { id: 19, name: 'Screen operator', username: 'operator', roleNames: 'Operators' };
const administrator = { id: 7, permissionKeys: ['users.read', 'impersonate.allow'] };
const adminPage = renderUsersPage({ users: [user] }, '', administrator);
const editPage = renderUsersEditPage(user, '', administrator, [], []);
const previewPage = renderUsersPage({ users: [user] }, '', Object.assign({}, administrator, { isViewingAs: true }));
const previewEditPage = renderUsersEditPage(user, '', Object.assign({}, administrator, { isViewingAs: true }), [], []);
assert.doesNotMatch(adminPage, /view-as/);
assert.match(editPage, /form="view-as-user-form"/);
assert.match(editPage, /<form id="view-as-user-form" method="post" action="\/settings\/users\/19\/view-as"/);
assert.ok(editPage.indexOf('View as') < editPage.indexOf('Profile actions'));
assert.doesNotMatch(previewPage, /action="\/settings\/users\/19\/view-as"/);
assert.doesNotMatch(previewEditPage, /view-as-user-form/);
});
test('view-as banner identifies both users and keeps an exit control on regular and error pages', () => {
const previewUser = {
id: 19,
name: 'Screen operator',
username: 'operator',
isViewingAs: true,
authenticatedUser: { id: 7, name: 'Administrator', username: 'admin' }
};
const regularPage = pages.renderAboutPage(previewUser);
const errorPage = pages.renderErrorPage({ statusCode: 403 }, previewUser);
[regularPage, errorPage].forEach(function (html) {
assert.match(html, /Read-only preview/);
assert.match(html, /Screen operator/);
assert.match(html, /signed in as Administrator/);
assert.match(html, /action="\/settings\/view-as\/stop"/);
assert.match(html, /class="btn btn-sm btn-dark"/);
assert.doesNotMatch(html, /btn-outline-dark/);
});
});
test('view-as start and stop routes audit the authenticated administrator', async () => {
const handlers = {};
const permissionKeys = [];
const auditEvents = [];
const helperCalls = [];
const app = {
post(path, ...routeHandlers) {
handlers[path] = routeHandlers;
}
};
const administrator = {
id: 7,
name: 'Administrator',
username: 'admin',
permissionKeys: ['users.read', 'impersonate.allow']
};
registerViewAsRoutes(app, {
pool: {
async query(sql, params) {
assert.match(sql, /FROM a_users/);
assert.deepEqual(params, [19]);
return [[{ id: 19, name: 'Screen operator', username: 'operator' }]];
}
},
requirePermission(permissionKey) {
permissionKeys.push(permissionKey);
return function (_req, _res, next) { next(); };
},
async startViewAsUser(_pool, _req, actorId, targetId) {
helperCalls.push(['start', actorId, targetId]);
return true;
},
async stopViewAsUser(_pool, _req, actorId) {
helperCalls.push(['stop', actorId]);
return true;
},
async recordRequestAuditEvent(_pool, _req, event) {
auditEvents.push(event);
}
});
const startResponse = {
redirect(path) { this.redirectedTo = path; },
status(statusCode) { this.statusCode = statusCode; return this; },
send(body) { this.body = body; return this; },
sendStatus(statusCode) { this.statusCode = statusCode; return this; }
};
await handlers['/settings/users/:id/view-as'][2]({
params: { id: '19' },
authenticatedUser: administrator,
currentUser: administrator
}, startResponse, (error) => { throw error; });
const stopResponse = {
redirect(path) { this.redirectedTo = path; },
status(statusCode) { this.statusCode = statusCode; return this; },
send(body) { this.body = body; return this; }
};
await handlers['/settings/view-as/stop'][0]({
authenticatedUser: administrator,
currentUser: { id: 19, username: 'operator', isViewingAs: true }
}, stopResponse, (error) => { throw error; });
assert.deepEqual(permissionKeys, ['users.read', 'impersonate.allow']);
assert.deepEqual(helperCalls, [['start', 7, 19], ['stop', 7]]);
assert.deepEqual(auditEvents.map(function (event) { return event.actorUserId; }), [7, 7]);
assert.deepEqual(auditEvents.map(function (event) { return event.eventType; }), ['user.view_as_started', 'user.view_as_stopped']);
assert.equal(startResponse.redirectedTo, '/dashboard');
assert.equal(stopResponse.redirectedTo, '/settings/users');
});
test('RSS and weather refresh actions stay visible but disabled when authorized previewing', () => {
const rssPage = renderRssFeedEditPage({ id: 31, name: 'Feed', enabled: 1 }, {}, '', {
isViewingAs: true,
permissionKeys: ['rss-feeds.update', 'rss-feeds.allow']
});
const weatherPage = renderWeatherLocationEditPage({ id: 12, name: 'Forecast', enabled: 1 }, '', {
isViewingAs: true,
permissionKeys: ['weather.update', 'weather.allow']
});
assert.match(rssPage, /data-manual-refresh-url="\/data-sources\/rss-feeds\/31\/refresh"[^>]*disabled/);
assert.match(rssPage, /data-async-data-source-toggle[^>]*disabled/);
assert.match(weatherPage, /data-weather-refresh-url="\/data-sources\/weather\/12\/refresh"[^>]*disabled/);
assert.match(weatherPage, /data-async-data-source-toggle[^>]*disabled/);
});