hoppscotch-backend-git/tests/webapp-bundle-cache.test.ts
tlemesle c4a46b7af9 Cache webapp bundle & unblock listen on startup
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 17:29:09 +02:00

81 lines
2.9 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { mkdtemp, rm, utimes, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { loadOrBuildWebappBundle } from '../src/webapp/bundle.ts';
import { resolveWebappKeys } from '../src/webapp/keys.ts';
import { buildStoreZip } from '../src/webapp/zip-store.ts';
describe('webapp bundle cache', () => {
let frontendPath = '';
let cacheDir = '';
let stateParent = '';
beforeAll(async () => {
stateParent = await mkdtemp(join(tmpdir(), 'hbg-bundle-'));
frontendPath = join(stateParent, 'frontend');
cacheDir = join(stateParent, 'cache');
await Bun.$`mkdir -p ${frontendPath}`.quiet();
await writeFile(join(frontendPath, 'index.html'), '<html>ok</html>\n');
});
afterAll(async () => {
await rm(stateParent, { recursive: true, force: true });
});
test('loadOrBuildWebappBundle reuses disk cache on second call', async () => {
const keys = await resolveWebappKeys('test-webapp-signing-secret', join(stateParent, 'signing.key'));
const first = await loadOrBuildWebappBundle(
frontendPath,
keys,
'v1',
cacheDir,
);
expect(first.content.byteLength).toBeGreaterThan(0);
expect(await Bun.file(join(cacheDir, 'bundle.zip')).exists()).toBe(true);
const second = await loadOrBuildWebappBundle(
frontendPath,
keys,
'v1',
cacheDir,
);
expect(Buffer.from(second.content).equals(Buffer.from(first.content))).toBe(true);
expect(second.metadata.signature).toBe(first.metadata.signature);
});
test('cache invalidates when a frontend file changes', async () => {
const keys = await resolveWebappKeys('test-webapp-signing-secret', join(stateParent, 'signing.key'));
const before = await loadOrBuildWebappBundle(
frontendPath,
keys,
'v1',
cacheDir,
);
await writeFile(join(frontendPath, 'index.html'), '<html>changed</html>\n');
// Ensure mtime advances even on fast filesystems
const now = new Date(Date.now() + 2000);
await utimes(join(frontendPath, 'index.html'), now, now);
const after = await loadOrBuildWebappBundle(
frontendPath,
keys,
'v1',
cacheDir,
);
expect(Buffer.from(after.content).equals(Buffer.from(before.content))).toBe(false);
});
test('buildStoreZip produces a valid zip local header', () => {
const zip = buildStoreZip([
{ path: 'a.txt', content: new TextEncoder().encode('hello') },
]);
expect(zip[0]).toBe(0x50);
expect(zip[1]).toBe(0x4b);
expect(zip[2]).toBe(0x03);
expect(zip[3]).toBe(0x04);
});
});