PPPAIMT-Crest/backend/test/adminAuth.test.js

187 lines
5.3 KiB
JavaScript

import argon2 from 'argon2';
import { afterEach, describe, expect, it } from 'vitest';
import { createServer } from '../src/app.js';
import { db } from '../src/db/knex.js';
import { truncateTables } from './truncate.js';
const email = 'admin@example.com';
const password = 'correct-password';
/**
* MySQL may return a JSON column as a parsed value or as text.
* @param {unknown} value
*/
function parseJson(value) {
if (typeof value !== 'string') {
return value;
}
try {
return JSON.parse(value);
} catch {
return value;
}
}
/**
* @param {import('@hapi/hapi').ServerInjectResponse} response
*/
function sessionCookie(response) {
const raw = response.headers['set-cookie'];
const header = Array.isArray(raw) ? raw[0] : raw;
return header.split(';')[0];
}
describe('admin auth', () => {
/** @type {import('@hapi/hapi').Server | undefined} */
let server;
afterEach(async () => {
await server?.stop();
await truncateTables();
});
async function start() {
server = await createServer();
return server;
}
async function createAdmin(overrides = {}) {
await db('admin_users').insert({
name: 'Administrator',
email,
password_hash: await argon2.hash(password, { type: argon2.argon2id }),
is_active: true,
...overrides,
});
}
it('seeds signatory settings', async () => {
const rows = await db('settings').select('key', 'value');
const settings = Object.fromEntries(rows.map((row) => [row.key, parseJson(row.value)]));
expect(settings.signatory_name).toBe('Dr Sudeep Salvi');
expect(settings.signatory_designation).toBe('');
expect(settings.signatory_signature_key).toBe('');
expect(settings.mail_from_name).toBe('');
});
it('logs in, returns the admin, and logs out', async () => {
await createAdmin();
const api = await start();
const login = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password },
});
expect(login.statusCode).toBe(200);
expect(login.result).toMatchObject({ name: 'Administrator', email });
const cookie = sessionCookie(login);
expect(cookie.startsWith('admin_token=')).toBe(true);
expect(login.headers['set-cookie'][0]).toMatch(/HttpOnly/i);
expect(login.headers['set-cookie'][0]).toMatch(/SameSite=Lax/i);
const me = await api.inject({
method: 'GET',
url: '/api/admin/auth/me',
headers: { cookie },
});
expect(me.statusCode).toBe(200);
expect(me.result).toMatchObject({ email });
const logout = await api.inject({
method: 'POST',
url: '/api/admin/auth/logout',
headers: { cookie },
});
expect(logout.statusCode).toBe(200);
const audits = await db('audit_log').select('action').orderBy('id');
expect(audits.map((row) => row.action)).toEqual(['ADMIN_LOGIN', 'ADMIN_LOGOUT']);
});
it('rejects a missing session', async () => {
const api = await start();
const response = await api.inject({ method: 'GET', url: '/api/admin/auth/me' });
expect(response.statusCode).toBe(401);
expect(response.result).toEqual({
error: { code: 'UNAUTHORIZED', message: 'Authentication required' },
});
});
it('uses the same error for an unknown email and a wrong password', async () => {
await createAdmin();
const api = await start();
const unknown = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email: 'missing@example.com', password },
});
const wrong = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password: 'wrong-password' },
});
expect(unknown.statusCode).toBe(401);
expect(wrong.statusCode).toBe(401);
expect(unknown.result).toEqual(wrong.result);
});
it('locks the account on the fifth failure', async () => {
await createAdmin();
const api = await start();
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password: 'wrong-password' },
});
expect(response.statusCode).toBe(401);
}
const locked = await db('admin_users').where({ email }).first();
expect(locked.failed_login_count).toBe(0);
expect(new Date(locked.locked_until).getTime()).toBeGreaterThan(Date.now());
const duringLock = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password },
});
expect(duringLock.statusCode).toBe(401);
await db('admin_users')
.where({ email })
.update({ locked_until: new Date(Date.now() - 1000) });
const afterLock = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password },
});
expect(afterLock.statusCode).toBe(200);
const cleared = await db('admin_users').where({ email }).first();
expect(cleared.failed_login_count).toBe(0);
expect(cleared.locked_until).toBeNull();
});
it('rejects an inactive admin', async () => {
await createAdmin({ is_active: false });
const api = await start();
const response = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password },
});
expect(response.statusCode).toBe(401);
});
});