feat: add Node ACS admin workflows
This commit is contained in:
195
server/routes/auth.test.ts
Normal file
195
server/routes/auth.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import test from 'node:test';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import express from 'express';
|
||||
import { createAuthRouter } from './auth.js';
|
||||
import { errorHandler } from '../http/errors.js';
|
||||
|
||||
type QueryFn = NonNullable<Parameters<typeof createAuthRouter>[0]>['query'];
|
||||
type JsonBody = {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
};
|
||||
|
||||
interface UserRow {
|
||||
id: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
full_name: string;
|
||||
email: string | null;
|
||||
department: string | null;
|
||||
must_change_password: boolean;
|
||||
enabled: boolean;
|
||||
locked: boolean;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
function queryResult<T>(rows: T[]) {
|
||||
return {
|
||||
command: 'SELECT',
|
||||
rowCount: rows.length,
|
||||
oid: 0,
|
||||
fields: [],
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function buildApp(query: QueryFn, sessionData: Record<string, unknown> = {}) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
req.session = {
|
||||
...sessionData,
|
||||
destroy(callback: (error?: Error) => void) {
|
||||
callback();
|
||||
},
|
||||
} as typeof req.session;
|
||||
next();
|
||||
});
|
||||
app.use('/auth', createAuthRouter({ query }));
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
}
|
||||
|
||||
async function requestJson(
|
||||
app: ReturnType<typeof buildApp>,
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
) {
|
||||
const server = app.listen(0);
|
||||
try {
|
||||
const address = server.address() as AddressInfo;
|
||||
const response = await fetch(`http://127.0.0.1:${address.port}${path}`, {
|
||||
method,
|
||||
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
return {
|
||||
status: response.status,
|
||||
body: await response.json() as JsonBody,
|
||||
};
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function makeQuery(user: UserRow, calls: Array<{ text: string; params?: unknown[] }>): QueryFn {
|
||||
return (async (text, params) => {
|
||||
const sql = String(text);
|
||||
calls.push({ text: sql, params });
|
||||
|
||||
if (sql.includes('WHERE u.username = $1')) {
|
||||
return queryResult(params?.[0] === user.username ? [user] : []);
|
||||
}
|
||||
|
||||
if (sql.includes('WHERE u.id = $1')) {
|
||||
return queryResult(Number(params?.[0]) === Number(user.id) ? [user] : []);
|
||||
}
|
||||
|
||||
if (sql.includes('UPDATE users')) {
|
||||
return queryResult([]);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected query: ${sql}`);
|
||||
}) as QueryFn;
|
||||
}
|
||||
|
||||
test('login trims username and returns the current user shape', async () => {
|
||||
const user: UserRow = {
|
||||
id: '1',
|
||||
username: 'a',
|
||||
password_hash: await bcrypt.hash('1', 4),
|
||||
full_name: 'ACS Admin',
|
||||
email: 'admin@example.test',
|
||||
department: 'IT',
|
||||
must_change_password: false,
|
||||
enabled: true,
|
||||
locked: false,
|
||||
roles: ['ADMIN'],
|
||||
};
|
||||
const calls: Array<{ text: string; params?: unknown[] }> = [];
|
||||
const app = buildApp(makeQuery(user, calls));
|
||||
|
||||
const response = await requestJson(app, 'POST', '/auth/login', {
|
||||
username: ' a ',
|
||||
password: '1',
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.body.data.username, 'a');
|
||||
assert.equal(response.body.data.fullName, 'ACS Admin');
|
||||
assert.deepEqual(response.body.data.roles, ['ADMIN']);
|
||||
assert.deepEqual(calls[0]?.params, ['a']);
|
||||
});
|
||||
|
||||
test('login rejects disabled accounts even with a valid password', async () => {
|
||||
const user: UserRow = {
|
||||
id: '2',
|
||||
username: 'disabled',
|
||||
password_hash: await bcrypt.hash('1', 4),
|
||||
full_name: 'Disabled User',
|
||||
email: null,
|
||||
department: null,
|
||||
must_change_password: false,
|
||||
enabled: false,
|
||||
locked: false,
|
||||
roles: ['HOST'],
|
||||
};
|
||||
const app = buildApp(makeQuery(user, []));
|
||||
|
||||
const response = await requestJson(app, 'POST', '/auth/login', {
|
||||
username: 'disabled',
|
||||
password: '1',
|
||||
});
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(response.body.data, null);
|
||||
});
|
||||
|
||||
test('me returns 401 without a session user', async () => {
|
||||
const app = buildApp((async () => {
|
||||
throw new Error('query should not run for unauthenticated /me');
|
||||
}) as QueryFn);
|
||||
|
||||
const response = await requestJson(app, 'GET', '/auth/me');
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(response.body.data, null);
|
||||
});
|
||||
|
||||
test('change-password updates the password hash and clears must_change_password', async () => {
|
||||
const user: UserRow = {
|
||||
id: '3',
|
||||
username: 'host',
|
||||
password_hash: await bcrypt.hash('OldPassword123!', 4),
|
||||
full_name: 'Host User',
|
||||
email: null,
|
||||
department: null,
|
||||
must_change_password: true,
|
||||
enabled: true,
|
||||
locked: false,
|
||||
roles: ['HOST'],
|
||||
};
|
||||
const calls: Array<{ text: string; params?: unknown[] }> = [];
|
||||
const app = buildApp(makeQuery(user, calls), { userId: 3 });
|
||||
|
||||
const response = await requestJson(app, 'POST', '/auth/change-password', {
|
||||
oldPassword: 'OldPassword123!',
|
||||
newPassword: 'NewPassword123!',
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const updateCall = calls.find((call) => call.text.includes('UPDATE users'));
|
||||
assert.ok(updateCall);
|
||||
assert.equal(updateCall.params?.[1], 3);
|
||||
assert.equal(await bcrypt.compare('NewPassword123!', String(updateCall.params?.[0])), true);
|
||||
assert.notEqual(updateCall.params?.[0], user.password_hash);
|
||||
});
|
||||
Reference in New Issue
Block a user