// apps/hq/test/pdf-download.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest' import express from 'express' import { openDb } from '../src/db' import { seedIfEmpty } from '../src/seed' import { createStaff } from '../src/auth' import { createClient } from '../src/repos-clients' import { createModule, setPrice } from '../src/repos-modules' import { createDraft, issueDocument } from '../src/repos-documents' import { apiRouter } from '../src/api' /** * GET /api/documents/:id/pdf serves the PDF INLINE by default (so the composer iframe * preview keeps working). With ?download=1 it switches to an attachment with a filename * derived from the doc no ("QT/26-27-0001" → "QT-26-27-0001.pdf"; a null docNo draft → * "draft.pdf"). renderPdf is injected (a fake %PDF- buffer) so the header logic is * exercised without launching Chrome — the house pattern for PDF-touching tests. */ async function setup() { const db = openDb(':memory:'); await seedIfEmpty(db) await createStaff(db, { email: 'owner@test.in', displayName: 'Owner', role: 'owner', password: 'owner-password' }) const c = await createClient(db, 'u1', { name: 'Acme Traders', code: 'ACME', stateCode: '32' }) const m = await createModule(db, 'u1', { code: 'POS', name: 'POS Billing' }) await setPrice(db, 'u1', { moduleId: m.id, kind: 'yearly', pricePaise: 10_000_00, effectiveFrom: '2026-04-01' }) // An issued quotation carries a docNo with a '/' (QT/26-27-0001)… const issued = await issueDocument(db, 'u1', (await createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] })).id) // …a separate, un-issued draft keeps docNo=null. const draftDoc = await createDraft(db, 'u1', { docType: 'QUOTATION', clientId: c.id, lines: [{ moduleId: m.id, qty: 1, kind: 'yearly' }] }) const fakePdf = async (): Promise => Buffer.from('%PDF-1.4 fake') const app = express(); app.use(express.json()); app.locals['db'] = db app.use('/api', apiRouter(db, undefined, fakePdf)) const server = app.listen(0) const base = `http://localhost:${(server.address() as { port: number }).port}/api` return { db, server, base, issued, draftDoc } } describe('GET /documents/:id/pdf — download filename (Content-Disposition)', () => { let ctx: Awaited> beforeAll(async () => { ctx = await setup() }) afterAll(() => ctx.server.close()) const login = async (): Promise => (await (await fetch(`${ctx.base}/auth/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'owner@test.in', password: 'owner-password' }), })).json() as any).token const getPdf = async (path: string, token: string) => fetch(`${ctx.base}${path}`, { headers: { authorization: `Bearer ${token}` } }) it('serves inline by default (no ?download) so the composer iframe preview still works', async () => { const token = await login() const res = await getPdf(`/documents/${ctx.issued.id}/pdf`, token) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toContain('application/pdf') expect(res.headers.get('content-disposition')).toMatch(/^inline/) // Not an attachment. expect(res.headers.get('content-disposition')).not.toMatch(/^attachment/) }) it('serves an attachment with the slugged doc-no filename on ?download=1', async () => { const token = await login() const res = await getPdf(`/documents/${ctx.issued.id}/pdf?download=1`, token) expect(res.status).toBe(200) const cd = res.headers.get('content-disposition') ?? '' expect(cd).toMatch(/^attachment/) // QT/26-27-0001 → QT-26-27-0001.pdf (the '/' is not a legal filename char). const slug = ctx.issued.docNo!.replaceAll('/', '-') expect(cd).toContain(`filename="${slug}.pdf"`) expect(slug).toMatch(/^QT-\d{2}-\d{2}-\d{4}$/) }) it('names a null-docNo draft "draft.pdf" on download', async () => { const token = await login() const res = await getPdf(`/documents/${ctx.draftDoc.id}/pdf?download=1`, token) expect(res.status).toBe(200) const cd = res.headers.get('content-disposition') ?? '' expect(cd).toMatch(/^attachment/) expect(cd).toContain('filename="draft.pdf"') }) })