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

214 lines
6.8 KiB
JavaScript

import { randomBytes, randomUUID } from 'node:crypto';
import argon2 from 'argon2';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createServer } from '../src/app.js';
import { db } from '../src/db/knex.js';
import { clearOutbox, outbox } from '../src/services/mail.js';
import { putObject } from '../src/services/storage.js';
import { truncateTables } from './truncate.js';
vi.mock('../src/pdf/render.js', () => ({
renderPdf: vi.fn(async () => Buffer.from('%PDF-1.4')),
}));
const email = 'admin@example.com';
const password = 'correct-password';
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
'base64',
);
/**
* @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 applications', () => {
/** @type {import('@hapi/hapi').Server | undefined} */
let server;
beforeEach(async () => {
await db('settings')
.where({ key: 'signatory_signature_key' })
.update({ value: JSON.stringify('') });
});
afterEach(async () => {
await server?.stop();
clearOutbox();
await truncateTables();
await db('settings')
.where({ key: 'signatory_signature_key' })
.update({ value: JSON.stringify('') });
});
async function start() {
server = await createServer();
return server;
}
async function login(api) {
await db('admin_users').insert({
name: 'Administrator',
email,
password_hash: await argon2.hash(password, { type: argon2.argon2id }),
is_active: true,
});
const response = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email, password },
});
return { cookie: sessionCookie(response), admin: response.result };
}
async function insertApplication(status) {
const [id] = await db('applications').insert({
public_id: randomUUID(),
email: 'physician@example.com',
status,
full_name: 'Asha Rao',
agreement_date: '2026-09-30',
submitted_at: status === 'DRAFT' ? null : new Date(),
version: 1,
});
await db('resume_tokens').insert({
application_id: id,
token_hash: randomBytes(32).toString('hex'),
created_at: new Date(),
});
await db('signatures').insert({
application_id: id,
version: 1,
type: 'TYPED',
typed_text: 'Asha Rao',
signed_at: new Date(),
});
return id;
}
it('rejects approval unless the application is submitted and the signature is configured', async () => {
const api = await start();
const { cookie } = await login(api);
const draftId = await insertApplication('DRAFT');
const headers = { cookie };
const draft = await api.inject({
method: 'POST',
url: `/api/admin/applications/${draftId}/approve`,
headers,
});
expect(draft.statusCode).toBe(409);
const submittedId = await insertApplication('SUBMITTED');
const missing = await api.inject({
method: 'POST',
url: `/api/admin/applications/${submittedId}/approve`,
headers,
});
expect(missing.statusCode).toBe(400);
expect(missing.result).toEqual({
error: {
code: 'BAD_REQUEST',
message: 'Upload the CREST signatory signature in Settings before approving.',
},
});
const shortReason = await api.inject({
method: 'POST',
url: `/api/admin/applications/${submittedId}/reject`,
headers,
payload: { reason: 'too short' },
});
expect(shortReason.statusCode).toBe(400);
});
it('approves a submitted application and records the decision', async () => {
const api = await start();
const { cookie } = await login(api);
const id = await insertApplication('SUBMITTED');
await putObject('settings/signatory-signature.png', png, 'image/png');
await db('settings')
.where({ key: 'signatory_signature_key' })
.update({ value: JSON.stringify('settings/signatory-signature.png') });
const response = await api.inject({
method: 'POST',
url: `/api/admin/applications/${id}/approve`,
headers: { cookie },
});
expect(response.statusCode).toBe(200);
expect(response.result).toMatchObject({ status: 'APPROVED' });
expect(JSON.stringify(response.result)).not.toContain('/survey/');
const saved = await db('applications').where({ id }).first();
expect(saved.final_pdf_key).toContain('/final.pdf');
expect(saved.signed_pdf_sha256).toHaveLength(64);
expect(await db('survey_tokens').where({ application_id: id }).first()).toBeTruthy();
expect(
await db('email_log').where({ application_id: id, template: 'approved' }).first(),
).toBeTruthy();
expect(
await db('audit_log').where({ action: 'APPLICATION_APPROVED', entity_id: id }).first(),
).toBeTruthy();
expect(outbox[0].url).toContain('/survey/');
});
it('rejects with a reason, audits the decision, and rotates the resume token', async () => {
const api = await start();
const { cookie } = await login(api);
const id = await insertApplication('SUBMITTED');
const response = await api.inject({
method: 'POST',
url: `/api/admin/applications/${id}/reject`,
headers: { cookie },
payload: { reason: 'The registration number does not match' },
});
expect(response.statusCode).toBe(200);
expect(response.result).toMatchObject({
status: 'REJECTED',
rejection_reason: 'The registration number does not match',
});
expect(
await db('audit_log').where({ action: 'APPLICATION_REJECTED', entity_id: id }).first(),
).toBeTruthy();
expect(JSON.stringify(response.result)).not.toContain('/resume/');
const tokens = await db('resume_tokens').where({ application_id: id });
expect(tokens.filter((row) => row.revoked_at)).toHaveLength(1);
expect(tokens.filter((row) => !row.revoked_at)).toHaveLength(1);
const listed = await api.inject({
method: 'GET',
url: '/api/admin/applications?status=REJECTED&q=Asha',
headers: { cookie },
});
expect(listed.statusCode).toBe(200);
expect(listed.result.total).toBe(1);
const dashboard = await api.inject({
method: 'GET',
url: '/api/admin/dashboard',
headers: { cookie },
});
expect(dashboard.statusCode).toBe(200);
expect(dashboard.result.counts.REJECTED).toBe(1);
});
it('stops an admin from deactivating their own account', async () => {
const api = await start();
const { cookie, admin } = await login(api);
const response = await api.inject({
method: 'POST',
url: `/api/admin/users/${admin.id}/deactivate`,
headers: { cookie },
});
expect(response.statusCode).toBe(409);
});
});