Files
acs/server/routes/auth.test.ts
2026-07-16 11:00:09 +09:00

196 lines
5.3 KiB
TypeScript

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