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