Release v2.11.1
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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.');
|
||||
});
|
||||
Reference in New Issue
Block a user