Add per-file asset hashes and a rollback-compatible server manifest
This commit is contained in:
@@ -32,11 +32,11 @@ async function freePort() {
|
||||
});
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
async function startServer(hashing = true) {
|
||||
const port = await freePort();
|
||||
const child = Bun.spawn([process.execPath, serverFile], {
|
||||
cwd: root,
|
||||
env: { ...process.env, PORT: String(port), DB_PATH: join(dataDir, `db-${port}.sqlite`), MEDIA_DIR: join(dataDir, 'media'), UPLOAD_DIR: join(dataDir, 'uploads') },
|
||||
env: { ...process.env, PORT: String(port), ASSET_HASHING: hashing ? '1' : '0', BUILD_TAG: '', DB_PATH: join(dataDir, `db-${port}.sqlite`), MEDIA_DIR: join(dataDir, 'media'), UPLOAD_DIR: join(dataDir, 'uploads') },
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
});
|
||||
const stdout = new Response(child.stdout).text();
|
||||
@@ -97,20 +97,20 @@ describe('static delivery characterization', () => {
|
||||
test('index stamps only relative local css/js and serves expected cache headers', async () => {
|
||||
const index = await fetch(`${server.base}/`);
|
||||
const html = await index.text();
|
||||
const { buildTag } = await (await fetch(`${server.base}/api/version`)).json();
|
||||
const { buildTag, files } = await (await fetch(`${server.base}/api/manifest`)).json();
|
||||
expect(html).toContain(`<meta name="ytp-build" content="${buildTag}">`);
|
||||
expect(html).toContain(`href="app.css?v=${buildTag}"`);
|
||||
expect(html).toContain(`src="app.js?v=${buildTag}"`);
|
||||
expect(html).toContain(`href="app.css?v=${files['/app.css'].h}"`);
|
||||
expect(html).toContain(`src="app.js?v=${files['/app.js'].h}"`);
|
||||
expect(html).toContain('https://cdn.example/x.js');
|
||||
expect(html).toContain('href="/root.css"');
|
||||
expect(html).toContain(`href="/root.css?v=${files['/root.css'].h}"`);
|
||||
expect(index.headers.get('cache-control')).toBe('no-cache');
|
||||
expect((await fetch(`${server.base}/index.html`)).headers.get('cache-control')).toBe('no-cache');
|
||||
|
||||
const current = await fetch(`${server.base}/app.js?v=${buildTag}`, { headers: { 'Accept-Encoding': 'br' } });
|
||||
const current = await fetch(`${server.base}/app.js?v=${files['/app.js'].h}`, { headers: { 'Accept-Encoding': 'br' } });
|
||||
expect(current.headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
|
||||
expect(current.headers.get('content-encoding')).toBe('br');
|
||||
expect(current.headers.get('etag')).toBeTruthy();
|
||||
expect((await fetch(`${server.base}/app.js?v=stale`)).headers.get('cache-control')).toBe('no-cache');
|
||||
expect((await fetch(`${server.base}/app.js?v=stale`)).headers.get('cache-control')).toBe('no-store');
|
||||
expect((await fetch(`${server.base}/app.js`)).headers.get('cache-control')).toBe('no-cache');
|
||||
expect((await fetch(`${server.base}/fonts/font.woff2`)).headers.get('cache-control')).toBe('public, max-age=2592000');
|
||||
expect((await fetch(`${server.base}/icons/icon.png`)).headers.get('cache-control')).toBe('public, max-age=2592000');
|
||||
@@ -118,11 +118,54 @@ describe('static delivery characterization', () => {
|
||||
expect((await fetch(`${server.base}${path}`)).headers.get('cache-control')).toBe('no-store, no-cache, must-revalidate');
|
||||
}
|
||||
const etag = current.headers.get('etag');
|
||||
const notModified = await fetch(`${server.base}/app.js?v=${buildTag}`, { headers: { 'If-None-Match': etag } });
|
||||
const notModified = await fetch(`${server.base}/app.js?v=${files['/app.js'].h}`, { headers: { 'If-None-Match': etag } });
|
||||
expect(notModified.status).toBe(304);
|
||||
expect(await notModified.text()).toBe('');
|
||||
});
|
||||
|
||||
|
||||
test('manifest API, ETag/304 and hashes cover compressed and binary assets', async () => {
|
||||
const response = await fetch(`${server.base}/api/manifest`);
|
||||
expect(response.headers.get('cache-control')).toBe('no-store');
|
||||
const manifest = await response.json();
|
||||
expect(manifest.buildTag).toBe((await (await fetch(`${server.base}/api/version`)).json()).buildTag);
|
||||
expect(manifest.groups.core.eager).toBe(true);
|
||||
expect(manifest.contracts.core).toBe(1);
|
||||
expect(response.headers.get('etag')).toBe(`"${manifest.buildTag}"`);
|
||||
const unchanged = await fetch(`${server.base}/api/manifest`, { headers: { 'If-None-Match': response.headers.get('etag') } });
|
||||
expect(unchanged.status).toBe(304);
|
||||
expect(await unchanged.text()).toBe('');
|
||||
const { createHash } = await import('node:crypto');
|
||||
for (const path of ['/app.js', '/fonts/font.woff2', '/icons/icon.png', '/index.html', '/sw.js']) {
|
||||
const h = manifest.files[path].h;
|
||||
for (const query of ['', `?v=${h}`, '?v=oldTag', '?__ytpfresh=123', '?v=']) {
|
||||
const asset = await fetch(`${server.base}${path}${query}`);
|
||||
expect(asset.status).toBe(200);
|
||||
expect(asset.headers.get('x-asset-hash')).toBe(h);
|
||||
expect(createHash('sha256').update(Buffer.from(await asset.arrayBuffer())).digest('hex').slice(0, 10)).toBe(h);
|
||||
if (path !== '/sw.js') {
|
||||
if (query === `?v=${h}`) expect(asset.headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
|
||||
if (query === '?v=oldTag' || query === '?v=') expect(asset.headers.get('cache-control')).toBe('no-store');
|
||||
}
|
||||
}
|
||||
}
|
||||
const canonical = await (await fetch(`${server.base}/`)).text();
|
||||
for (const path of ['/index.html', '/search', '/playlist/x']) expect(await (await fetch(`${server.base}${path}`)).text()).toBe(canonical);
|
||||
});
|
||||
|
||||
test('ASSET_HASHING=0 restores single-tag stamping and old cache matrix', async () => {
|
||||
await stopServer(server);
|
||||
server = await startServer(false);
|
||||
try {
|
||||
const { buildTag } = await (await fetch(`${server.base}/api/version`)).json();
|
||||
const html = await (await fetch(`${server.base}/`)).text();
|
||||
expect(html).toContain(`src="app.js?v=${buildTag}"`);
|
||||
expect(html).toContain('href="/root.css"');
|
||||
expect((await fetch(`${server.base}/app.js?v=${buildTag}`)).headers.get('cache-control')).toBe('public, max-age=31536000, immutable');
|
||||
expect((await fetch(`${server.base}/app.js?v=stale`)).headers.get('cache-control')).toBe('no-cache');
|
||||
} finally { await stopServer(server); server = await startServer(); }
|
||||
});
|
||||
|
||||
test('service worker injection tracks fallback literal and reports missing placeholder', async () => {
|
||||
let sw = await (await fetch(`${server.base}/sw.js`)).text();
|
||||
const { buildTag } = await (await fetch(`${server.base}/api/version`)).json();
|
||||
|
||||
Reference in New Issue
Block a user