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