Files
pulse-signage/test/admin-users-password-route.test.js
T

203 lines
7.5 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const registerUsersRoutes = require('../src/web/routes/admin/users');
const { validatePasswordStrength } = require('../src/auth');
test('user password reset redirects back to the selected user edit page', async () => {
const handlers = {};
let savedMustChangePassword = null;
let credentialPassword = null;
const app = {
post(path, ...routeHandlers) {
handlers[path] = routeHandlers;
},
get() {}
};
const deps = {
pool: {
async query(sql, params) {
if (sql.includes('SELECT id, username, must_change_password FROM a_users WHERE id = ? LIMIT 1')) {
return [[{ id: 7, username: 'user', must_change_password: 0 }]];
}
if (sql.includes('SELECT id, created_at, modified_at FROM a_users')) {
return [[{ id: 7, created_at: new Date(), modified_at: new Date() }]];
}
if (sql.includes('INSERT INTO a_accounts')) {
credentialPassword = params[3];
return [{ affectedRows: 1 }];
}
if (sql.includes('UPDATE a_users SET must_change_password = ?, modified_by = ? WHERE id = ?')) {
savedMustChangePassword = params[0];
return [{ affectedRows: 1 }];
}
if (sql.includes('DELETE FROM a_sessions WHERE user_id = ?')) {
return [{ affectedRows: 1 }];
}
return [[]];
}
},
common: {
fetchDuplicateName: async () => null
},
pages: {},
formatDashboardDate: () => '',
getAuditUserId: () => 3,
hashPassword: () => ({ hash: 'hash', salt: 'salt', iterations: 1 }),
validatePasswordStrength: validatePasswordStrength,
readArrayField: () => [],
rbacData: {
fetchRoles: async () => [],
fetchUsersWithRolesPage: async () => ({ users: [], totalItems: 0, currentPage: 1 }),
fetchUserWithRoles: async () => ({ id: 7, name: 'User', username: 'user', roleIds: [], inUse: false }),
syncUserRoles: async () => {}
},
requirePermission() {
return function (_req, _res, next) {
next();
};
}
};
registerUsersRoutes(app, deps);
const routeHandlers = handlers['/settings/users/:id/password'];
assert.equal(Array.isArray(routeHandlers), true);
const req = {
params: { id: '7' },
body: { password: 'NewPassword!1', confirm_password: 'NewPassword!1', must_change_password: '1' },
currentUser: { id: 1 }
};
const res = {
redirect(url) {
this.redirectedTo = url;
},
status() {
return this;
},
send() {
return this;
}
};
await routeHandlers[1](req, res, () => {});
assert.equal(res.redirectedTo, '/settings/users/7/edit?message=Password%20updated.');
assert.equal(savedMustChangePassword, 1);
assert.match(credentialPassword, /^[a-f0-9]{32}:[a-f0-9]{128}$/);
});
test('admin unlinks another user two-factor data and revokes their sessions', async () => {
const handlers = {};
const queries = [];
let committed = false;
let auditedEvent = null;
const app = {
post(path, ...routeHandlers) { handlers[path] = routeHandlers; },
get() {}
};
const connection = {
async beginTransaction() {},
async query(sql, params) {
const text = String(sql);
queries.push([text, params]);
if (text.includes('SELECT id FROM a_users')) return [[{ id: 7 }]];
if (text.includes('SELECT id FROM a_user_two_factors')) return [[{ id: 'factor-7' }]];
return [{ affectedRows: 1 }];
},
async commit() { committed = true; },
async rollback() {},
release() {}
};
const deps = {
pool: {
async query() { return [[]]; },
async getConnection() { return connection; }
},
common: {},
pages: {},
formatDashboardDate: () => '',
getAuditUserId: () => 3,
recordRequestAuditEvent: async (_pool, _req, event) => { auditedEvent = event; },
validatePasswordStrength: () => '',
readArrayField: () => [],
rbacData: {
fetchRoles: async () => [],
fetchUsersWithRolesPage: async () => ({ users: [], totalItems: 0, currentPage: 1 }),
fetchUserWithRoles: async () => null,
fetchActiveUserSessions: async () => [],
syncUserRoles: async () => {}
},
requirePermission() { return (_req, _res, next) => next(); }
};
registerUsersRoutes(app, deps);
const response = { redirect(url) { this.redirectedTo = url; }, status() { return this; }, send(value) { this.body = value; } };
await handlers['/settings/users/:id/two-factor/unlink'][1]({ params: { id: '7' }, currentUser: { id: 3 } }, response, error => { throw error; });
assert.equal(committed, true);
assert.ok(queries.some(([sql, params]) => sql.includes('DELETE FROM a_user_two_factors WHERE user_id = ?') && params[0] === 7));
assert.ok(queries.some(([sql, params]) => sql.includes('UPDATE a_users SET two_factor_enabled = 0') && params[1] === 7));
assert.ok(queries.some(([sql, params]) => sql.includes('DELETE FROM a_sessions WHERE user_id = ?') && params[0] === 7));
assert.equal(auditedEvent.eventType, 'user.two_factor_unlinked');
assert.equal(response.redirectedTo, '/settings/users/7/edit?message=Two-step%20verification%20removed.%20The%20user%20must%20sign%20in%20again.');
});
test('admin unlinks a passkey only from the selected user and revokes their sessions', async () => {
const handlers = {};
const queries = [];
let committed = false;
let auditedEvent = null;
const app = {
post(path, ...routeHandlers) { handlers[path] = routeHandlers; },
get() {}
};
const connection = {
async beginTransaction() {},
async query(sql, params) {
const text = String(sql);
queries.push([text, params]);
if (text.includes('SELECT id FROM a_users')) return [[{ id: 7 }]];
if (text.includes('FROM a_user_passkeys')) return [[{ id: 'passkey-7' }]];
return [{ affectedRows: 1 }];
},
async commit() { committed = true; },
async rollback() {},
release() {}
};
const deps = {
pool: {
async query() { return [[]]; },
async getConnection() { return connection; }
},
common: {},
pages: {},
formatDashboardDate: () => '',
getAuditUserId: () => 3,
recordRequestAuditEvent: async (_pool, _req, event) => { auditedEvent = event; },
validatePasswordStrength: () => '',
readArrayField: () => [],
rbacData: {
fetchRoles: async () => [],
fetchUsersWithRolesPage: async () => ({ users: [], totalItems: 0, currentPage: 1 }),
fetchUserWithRoles: async () => null,
fetchActiveUserSessions: async () => [],
syncUserRoles: async () => {}
},
requirePermission() { return (_req, _res, next) => next(); }
};
registerUsersRoutes(app, deps);
const response = { redirect(url) { this.redirectedTo = url; }, status() { return this; }, send(value) { this.body = value; } };
await handlers['/settings/users/:id/passkeys/:passkeyId/unlink'][1]({ params: { id: '7', passkeyId: 'passkey-7' }, currentUser: { id: 3 } }, response, error => { throw error; });
assert.equal(committed, true);
assert.ok(queries.some(([sql, params]) => sql.includes('DELETE FROM a_user_passkeys WHERE id = ? AND user_id = ?') && params[0] === 'passkey-7' && params[1] === 7));
assert.ok(queries.some(([sql, params]) => sql.includes('DELETE FROM a_sessions WHERE user_id = ?') && params[0] === 7));
assert.equal(auditedEvent.eventType, 'user.passkey_unlinked');
assert.equal(response.redirectedTo, '/settings/users/7/edit?message=Passkey%20removed.%20The%20user%20must%20sign%20in%20again.');
});