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