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() { await db('pppaimt_doctors').insert({ email, hcp_name: 'Dr Consent Test' }); 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); }); });