Files
pulse-signage/test/admin-users-invitations-route.test.js
lzstealth 98f969ca0f
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 31s
Release v2.11.1
2026-09-04 15:43:55 +01:00

193 lines
5.9 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const registerUsersRoutes = require('../src/web/routes/admin/users');
test('pending invitations route loads active invitations with display fields', async () => {
const handlers = {};
let invitationQuery = '';
const rendered = {};
const createdAt = new Date('2026-09-03T10:00:00Z');
const expiresAt = new Date('2026-09-04T10:00:00Z');
const app = {
get(path, ...routeHandlers) {
handlers[path] = routeHandlers;
},
post() {}
};
const requiredPermissions = [];
const deps = {
pool: { async query() { return [[]]; } },
common: {
getSearchQuery: () => '',
getSortQuery: () => '',
getSortDirectionQuery: () => 'asc'
},
pages: {
renderInvitationsPage(data, currentUser) {
rendered.invitations = data.invitations;
rendered.pagination = data.pagination;
rendered.currentUser = currentUser;
return '';
}
},
formatDashboardDate(value) {
return value === createdAt ? 'sent label' : 'expiry label';
},
rbacData: {
fetchRoles: async () => [{ id: 2, name: 'Viewer' }],
fetchInvitationsPage: async (_pool, page, pageSize, search, sort, direction) => {
invitationQuery = { page, pageSize, search, sort, direction };
return {
invitations: [{
id: 7,
email: 'person@example.com',
name: 'Person',
role_ids_json: '[2]',
created_at: createdAt,
expires_at: expiresAt,
created_by_username: 'admin'
}],
totalItems: 26,
currentPage: 2
};
}
},
requirePermission(permission) {
requiredPermissions.push(permission);
return function (_req, _res, next) {
next();
};
}
};
registerUsersRoutes(app, deps);
const routeHandlers = handlers['/settings/invitations'];
assert.ok(requiredPermissions.includes('invitations.read'));
const currentUser = { id: 1 };
const res = {
send(value) {
this.body = value;
}
};
await routeHandlers[1]({ currentUser, query: {} }, res, function (error) {
throw error;
});
assert.deepEqual(invitationQuery, { page: 1, pageSize: 25, search: '', sort: '', direction: 'asc' });
assert.equal(rendered.currentUser, currentUser);
assert.equal(rendered.pagination.totalItems, 26);
assert.deepEqual(rendered.invitations, [{
id: 7,
email: 'person@example.com',
name: 'Person',
role_ids_json: '[2]',
created_at: createdAt,
expires_at: expiresAt,
created_by_username: 'admin',
roleNames: 'Viewer',
createdAtLabel: 'sent label',
expiresAtLabel: 'expiry label',
createdByLabel: 'admin'
}]);
});
test('user invite route stores the invitation and sends an acceptance link', async () => {
const handlers = {};
const queries = [];
let sentEmail = null;
const app = {
get() {},
post(path, ...routeHandlers) {
handlers[path] = routeHandlers;
}
};
const deps = {
pool: {
async query(sql, args) {
queries.push({ sql, args });
if (sql.includes('o_app_settings')) return [[{ setting_key: 'email.smtp_enabled', setting_value: 'true' }]];
if (sql.includes('SELECT id FROM a_users WHERE email')) return [[]];
if (sql.includes('INSERT INTO a_user_invitations')) return [{ insertId: 4 }];
if (sql.includes('COUNT(*) AS invite_count')) return [[{ invite_count: 0 }]];
return [[]];
}
},
common: {
validateMaxLength(value) { return String(value || '').trim(); }
},
pages: {
renderUsersInvitePage() { return ''; }
},
formatDashboardDate: () => '',
getAuditUserId: () => 9,
sendAccountEmail: async (_settings, message) => { sentEmail = message; },
createOneTimeToken: () => 'invite-token',
hashSessionToken: (value) => 'hash:' + value,
getRequestOrigin: () => 'https://signage.example.com',
readArrayField: () => ['2'],
rbacData: {
fetchRoles: async () => [{ id: 2, name: 'Viewer' }]
},
requirePermission() {
return function (_req, _res, next) { next(); };
}
};
registerUsersRoutes(app, deps);
const res = {
redirect(url) { this.redirectedTo = url; },
status() { return this; },
send() { return this; }
};
let routeError = null;
await handlers['/settings/users/invite'][1]({
body: { email: ' Person@Example.COM ', name: 'Person' },
currentUser: { id: 9 }
}, res, (error) => { routeError = error; });
assert.equal(routeError, null);
const insert = queries.find((query) => query.sql.includes('INSERT INTO a_user_invitations'));
assert.deepEqual(insert.args, ['person@example.com', 'Person', '[2]', 'hash:invite-token', 9]);
assert.equal(sentEmail.to, 'person@example.com');
assert.match(sentEmail.html, /accept-invite\?token=invite-token/);
assert.equal(res.redirectedTo, '/settings/users?message=Invitation%20sent.');
});
test('user invite route rejects invalid email before sending', async () => {
const handlers = {};
const app = {
get() {},
post(path, ...routeHandlers) { handlers[path] = routeHandlers; }
};
let renderedMessage = '';
const deps = {
pool: { async query() { return [[]]; } },
common: {
validateMaxLength(value) { return String(value || '').trim(); }
},
pages: {
renderUsersInvitePage(message) { renderedMessage = message; return ''; }
},
formatDashboardDate: () => '',
getAuditUserId: () => 1,
readArrayField: () => [],
rbacData: { fetchRoles: async () => [] },
requirePermission() {
return function (_req, _res, next) { next(); };
}
};
registerUsersRoutes(app, deps);
const res = { status() { return this; }, send() {} };
await handlers['/settings/users/invite'][1]({ body: { email: 'invalid', name: 'Person' }, currentUser: { id: 1 } }, res, () => {});
assert.equal(renderedMessage, 'Email address is invalid.');
});