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.'); });