You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/test/pdf-download.test.ts

82 lines
4.2 KiB
TypeScript

// 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> => 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<ReturnType<typeof setup>>
beforeAll(async () => { ctx = await setup() })
afterAll(() => ctx.server.close())
const login = async (): Promise<string> =>
(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"')
})
})