PPPAIMT-Crest/backend/test/pdfCache.test.js

118 lines
3.9 KiB
JavaScript

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 () => '<p>v2</p>');
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, '<p>v1</p>');
expect(await refreshStoredPdf(KEY, async () => '<p>v1</p>')).toBeNull();
expect(renderPdf).not.toHaveBeenCalled();
const rebuilt = await refreshStoredPdf(KEY, async () => '<p>v2</p>');
expect(String(rebuilt)).toBe('pdf:<p>v2</p>');
expect(String(mocks.files.get(KEY))).toBe('pdf:<p>v2</p>');
expect(await refreshStoredPdf(KEY, async () => '<p>v2</p>')).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 () => '<p>v1</p>'))).toBe('pdf:<p>v1</p>');
});
it('rebuilds on every call in always mode', async () => {
mocks.env.PDF_REGENERATE = 'always';
await refreshStoredPdf(KEY, async () => '<p>v1</p>');
await refreshStoredPdf(KEY, async () => '<p>v1</p>');
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 () => '<p>v1</p>');
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<string, string>} 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);
});
});