import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { afterEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ env: { PDF_REGENERATE: 'never' }, files: new Map(), })); vi.mock('../src/config/env.js', () => ({ env: mocks.env })); vi.mock('../src/pdf/render.js', () => ({ renderPdf: vi.fn(async (html) => Buffer.from(`pdf:${html}`)), })); vi.mock('../src/services/storage.js', () => ({ getObject: vi.fn(async (key) => { if (!mocks.files.has(key)) { throw new Error('missing'); } return mocks.files.get(key); }), putObject: vi.fn(async (key, body) => { mocks.files.set(key, Buffer.from(body)); }), })); const { refreshStoredPdf, rememberPdfSource } = await import('../src/services/pdfCache.js'); const { renderPdf } = await import('../src/pdf/render.js'); const KEY = 'applications/abc/consent.pdf'; describe('PDF_REGENERATE', () => { afterEach(() => { mocks.files.clear(); vi.mocked(renderPdf).mockClear(); }); it('never rebuilds a stored PDF by default', async () => { mocks.env.PDF_REGENERATE = 'never'; const build = vi.fn(async () => '

v2

'); expect(await refreshStoredPdf(KEY, build)).toBeNull(); expect(build).not.toHaveBeenCalled(); expect(renderPdf).not.toHaveBeenCalled(); }); it('rebuilds only when the html changed in changed mode', async () => { mocks.env.PDF_REGENERATE = 'changed'; await rememberPdfSource(KEY, '

v1

'); expect(await refreshStoredPdf(KEY, async () => '

v1

')).toBeNull(); expect(renderPdf).not.toHaveBeenCalled(); const rebuilt = await refreshStoredPdf(KEY, async () => '

v2

'); expect(String(rebuilt)).toBe('pdf:

v2

'); expect(String(mocks.files.get(KEY))).toBe('pdf:

v2

'); expect(await refreshStoredPdf(KEY, async () => '

v2

')).toBeNull(); expect(renderPdf).toHaveBeenCalledTimes(1); }); it('rebuilds a PDF with no recorded source in changed mode', async () => { mocks.env.PDF_REGENERATE = 'changed'; mocks.files.set(KEY, Buffer.from('old pdf')); expect(String(await refreshStoredPdf(KEY, async () => '

v1

'))).toBe('pdf:

v1

'); }); it('rebuilds on every call in always mode', async () => { mocks.env.PDF_REGENERATE = 'always'; await refreshStoredPdf(KEY, async () => '

v1

'); await refreshStoredPdf(KEY, async () => '

v1

'); expect(renderPdf).toHaveBeenCalledTimes(2); }); it('shares one render between simultaneous requests for the same file', async () => { mocks.env.PDF_REGENERATE = 'always'; const build = vi.fn(async () => '

v1

'); await Promise.all([refreshStoredPdf(KEY, build), refreshStoredPdf(KEY, build)]); expect(build).toHaveBeenCalledTimes(1); expect(renderPdf).toHaveBeenCalledTimes(1); }); }); describe('PDF_REGENERATE in production', () => { const envModule = pathToFileURL( path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/config/env.js'), ).href; /** * Runs outside backend/ so a developer's backend/.env is not loaded. * @param {Record} extra */ function loadEnv(extra) { return spawnSync( process.execPath, ['--input-type=module', '-e', `await import(${JSON.stringify(envModule)})`], { cwd: tmpdir(), env: { PATH: process.env.PATH, ...extra }, encoding: 'utf8' }, ); } it('refuses to start with regeneration enabled', () => { const result = loadEnv({ NODE_ENV: 'production', PDF_REGENERATE: 'changed' }); expect(result.status).toBe(1); expect(result.stderr).toContain('PDF_REGENERATE=changed is not allowed in production'); }); it('starts with regeneration off', () => { expect(loadEnv({ NODE_ENV: 'production', PDF_REGENERATE: 'never' }).status).toBe(0); expect(loadEnv({ NODE_ENV: 'production' }).status).toBe(0); }); });