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

255 lines
7.7 KiB
JavaScript

import argon2 from 'argon2';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createServer } from '../src/app.js';
import { db } from '../src/db/knex.js';
import { renderPdf } from '../src/pdf/render.js';
import { clearOutbox, outbox } from '../src/services/mail.js';
import { truncateTables } from './truncate.js';
vi.mock('../src/pdf/render.js', () => ({
renderPdf: vi.fn(async () => Buffer.from('%PDF-1.4 consent')),
}));
const email = 'consent-doctor@example.com';
const adminEmail = 'consent-admin@example.com';
const adminPassword = 'correct-password';
const answers = {
voluntary_withdraw: true,
mou_required: true,
eligibility_six_months: true,
confidential_research: true,
web_questionnaire: true,
no_intervention: true,
aggregated_reporting: true,
honorarium: true,
agree_participate: true,
};
/**
* @param {import('@hapi/hapi').ServerInjectResponse} response
*/
function userCookie(response) {
const raw = response.headers['set-cookie'];
const headers = Array.isArray(raw) ? raw : raw ? [raw] : [];
const match = headers.find((header) => header.startsWith('user_token='));
return match ? match.split(';')[0] : null;
}
describe('consent', () => {
/** @type {import('@hapi/hapi').Server | undefined} */
let server;
afterEach(async () => {
clearOutbox();
vi.mocked(renderPdf).mockClear();
await server?.stop();
await truncateTables();
});
async function start() {
server = await createServer();
return server;
}
async function login(api) {
await api.inject({
method: 'POST',
url: '/api/auth/otp/request',
payload: { email },
});
const verified = await api.inject({
method: 'POST',
url: '/api/auth/otp/verify',
payload: { email, otp: outbox[0].otp },
});
return userCookie(verified);
}
it('submits consent and advances nextStep to agreement', async () => {
const api = await start();
const cookie = await login(api);
const before = await api.inject({
method: 'GET',
url: '/api/me',
headers: { cookie },
});
expect(before.result.nextStep).toBe('consent');
const submitted = await api.inject({
method: 'POST',
url: '/api/consent',
headers: { cookie },
payload: {
participant_name: 'Dr Consent Test',
consent_date: '2026-10-16',
answers,
},
});
expect(submitted.statusCode).toBe(200);
expect(submitted.result).toMatchObject({
participant_name: 'Dr Consent Test',
consent_date: '2026-10-16',
});
const me = await api.inject({
method: 'GET',
url: '/api/me',
headers: { cookie },
});
expect(me.result.steps.consent.done).toBe(true);
expect(me.result.nextStep).toBe('agreement');
expect(me.result.user.name).toBe('Dr Consent Test');
const again = await api.inject({
method: 'POST',
url: '/api/consent',
headers: { cookie },
payload: {
participant_name: 'Dr Consent Test',
consent_date: '2026-10-16',
answers,
},
});
expect(again.statusCode).toBe(409);
const row = await db('consents').where({ participant_name: 'Dr Consent Test' }).first();
expect(row).toBeTruthy();
});
it('rejects incomplete consent answers', async () => {
const api = await start();
const cookie = await login(api);
const response = await api.inject({
method: 'POST',
url: '/api/consent',
headers: { cookie },
payload: {
participant_name: 'Dr Consent Test',
consent_date: '2026-10-16',
answers: { ...answers, eligibility_six_months: false },
},
});
expect(response.statusCode).toBe(400);
});
async function adminCookie(api) {
await db('admin_users').insert({
name: 'Administrator',
email: adminEmail,
password_hash: await argon2.hash(adminPassword, { type: argon2.argon2id }),
is_active: true,
});
const response = await api.inject({
method: 'POST',
url: '/api/admin/auth/login',
payload: { email: adminEmail, password: adminPassword },
});
const raw = response.headers['set-cookie'];
const header = Array.isArray(raw) ? raw[0] : raw;
return header.split(';')[0];
}
async function submit(api, cookie) {
return api.inject({
method: 'POST',
url: '/api/consent',
headers: { cookie },
payload: { participant_name: 'Dr Consent Test', consent_date: '2026-10-16', answers },
});
}
it('stores a consent PDF on submission and lets the physician download it', async () => {
const api = await start();
const cookie = await login(api);
const missing = await api.inject({
method: 'GET',
url: '/api/consent/me/pdf',
headers: { cookie },
});
expect(missing.statusCode).toBe(404);
expect((await submit(api, cookie)).statusCode).toBe(200);
const row = await db('consents').where({ participant_name: 'Dr Consent Test' }).first();
expect(row.consent_pdf_key).toMatch(/\/consent\.pdf$/);
expect(renderPdf).toHaveBeenCalledTimes(1);
const html = String(vi.mocked(renderPdf).mock.calls[0][0]);
expect(html).toContain('Dr Consent Test');
expect(html).toContain('16/10/2026');
expect(html).toContain('INFORMED CONSENT FORM (ICF)');
expect(html).not.toContain('Electronic Signature');
const download = await api.inject({
method: 'GET',
url: '/api/consent/me/pdf',
headers: { cookie },
});
expect(download.statusCode).toBe(200);
expect(download.headers['content-type']).toContain('application/pdf');
expect(download.headers['content-disposition']).toMatch(/attachment; filename="consent-.+\.pdf"/);
expect(download.rawPayload.toString()).toBe('%PDF-1.4 consent');
expect(renderPdf).toHaveBeenCalledTimes(1);
});
it('requires a signed-in physician to download a consent', async () => {
const api = await start();
const response = await api.inject({ method: 'GET', url: '/api/consent/me/pdf' });
expect(response.statusCode).toBe(401);
});
it('lets an admin download a consent and records it', async () => {
const api = await start();
const cookie = await login(api);
await submit(api, cookie);
const row = await db('consents').where({ participant_name: 'Dr Consent Test' }).first();
await db('consents').where({ id: row.id }).update({ consent_pdf_key: null });
const admin = await adminCookie(api);
const detail = await api.inject({
method: 'GET',
url: `/api/admin/applications/${row.application_id}`,
headers: { cookie: admin },
});
expect(detail.result.consent).toMatchObject({
participantName: 'Dr Consent Test',
consentDate: '2026-10-16',
});
const download = await api.inject({
method: 'GET',
url: `/api/admin/applications/${row.application_id}/consent-pdf`,
headers: { cookie: admin },
});
expect(download.statusCode).toBe(200);
expect(download.headers['content-type']).toContain('application/pdf');
expect((await db('consents').where({ id: row.id }).first()).consent_pdf_key).toBeTruthy();
expect(
await db('audit_log')
.where({ action: 'CONSENT_PDF_DOWNLOADED', entity_id: row.application_id })
.first(),
).toBeTruthy();
});
it('returns 404 to an admin when consent has not been submitted', async () => {
const api = await start();
await login(api);
const application = await db('applications').where({ email }).first();
const admin = await adminCookie(api);
const response = await api.inject({
method: 'GET',
url: `/api/admin/applications/${application.id}/consent-pdf`,
headers: { cookie: admin },
});
expect(response.statusCode).toBe(404);
});
});