import { randomUUID } from 'node:crypto'; import argon2 from 'argon2'; import ExcelJS from 'exceljs'; import { afterEach, describe, expect, it } from 'vitest'; import { CONSENT_QUESTIONS } from '../../frontend/src/consentQuestions.js'; import { createServer } from '../src/app.js'; import { db } from '../src/db/knex.js'; import { CONSENT_STATEMENTS } from '../src/schemas/consent.js'; import { encryptField } from '../src/services/fieldCrypto.js'; import { seedSurveyDefinition } from '../src/survey/seed.js'; import { truncateTables } from './truncate.js'; const email = 'applications-export-admin@example.com'; const password = 'correct-password'; /** * @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('consent statements', () => { it('match the statements shown on the consent page', () => { expect(CONSENT_STATEMENTS).toEqual( CONSENT_QUESTIONS.map(({ id, label }) => ({ id, label })), ); }); }); describe('applications export', () => { /** @type {import('@hapi/hapi').Server | undefined} */ let server; afterEach(async () => { await server?.stop(); await truncateTables(); }); it('writes application, consent and survey columns for the created-date range', async () => { server = await createServer(); await db('admin_users').insert({ name: 'Administrator', email, password_hash: await argon2.hash(password, { type: argon2.argon2id }), is_active: true, }); const login = await server.inject({ method: 'POST', url: '/api/admin/auth/login', payload: { email, password }, }); const [userId] = await db('users').insert({ email: 'asha@example.com', name: 'Asha Rao' }); const [applicationId] = await db('applications').insert({ public_id: randomUUID(), user_id: userId, email: 'asha@example.com', status: 'APPROVED', full_name: 'Asha Rao', registration_no: 'MMC-100', pan: encryptField('ABCDE1234F'), account_number: encryptField('001234567890'), ifsc: 'HDFC0001234', cancelled_cheque_key: 'cheques/1.png', final_pdf_key: 'final/1.pdf', version: 1, created_at: '2026-10-10 09:00:00', }); await db('applications').insert({ public_id: randomUUID(), email: 'outside@example.com', status: 'DRAFT', version: 1, created_at: '2026-09-01 09:00:00', }); await db('signatures').insert({ application_id: applicationId, version: 1, type: 'TYPED', typed_text: 'Asha Rao', signed_at: new Date(), }); await db('consents').insert({ user_id: userId, application_id: applicationId, participant_name: 'Asha Rao', consent_date: '2026-10-10', answers: JSON.stringify( Object.fromEntries(CONSENT_STATEMENTS.map((statement) => [statement.id, true])), ), submitted_at: '2026-10-10 09:05:00', }); const definitionId = await seedSurveyDefinition(); await db('survey_responses').insert({ application_id: applicationId, survey_definition_id: definitionId, answers: JSON.stringify({}), status: 'IN_PROGRESS', started_at: new Date(), }); const response = await server.inject({ method: 'GET', url: '/api/admin/applications/export?from=2026-10-01&to=2026-10-31', headers: { cookie: sessionCookie(login) }, }); expect(response.statusCode).toBe(200); expect(response.headers['content-disposition']).toContain('applications-consent.xlsx'); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(response.rawPayload); const sheet = workbook.getWorksheet('Applications'); const headers = sheet.getRow(1).values; const cell = (/** @type {string} */ header) => sheet.getRow(2).getCell(headers.indexOf(header)).value; expect(sheet.rowCount).toBe(2); expect(sheet.getRow(1).getCell(1).font?.bold).toBe(true); expect(cell('Status')).toBe('Approved'); expect(cell('PAN')).toBe('ABCDE1234F'); expect(cell('Account number')).toBe('001234567890'); expect(cell('Cheque uploaded')).toBe('Yes'); expect(cell('Signed')).toBe('Yes'); expect(cell('Final PDF available')).toBe('Yes'); expect(cell('Consent date')).toBe('2026-10-10'); expect(cell(CONSENT_STATEMENTS[0].label)).toBe('Yes'); expect(cell('I agree and consent to participate in the survey.')).toBe('Yes'); expect(cell('Survey status')).toBe('In progress'); expect(await db('audit_log').where({ action: 'APPLICATIONS_EXPORTED' }).first()).toBeTruthy(); }); it('rejects malformed dates', async () => { server = await createServer(); await db('admin_users').insert({ name: 'Administrator', email, password_hash: await argon2.hash(password, { type: argon2.argon2id }), is_active: true, }); const login = await server.inject({ method: 'POST', url: '/api/admin/auth/login', payload: { email, password }, }); const response = await server.inject({ method: 'GET', url: '/api/admin/applications/export?from=10-10-2026', headers: { cookie: sessionCookie(login) }, }); expect(response.statusCode).toBe(400); }); });