|
|
|
@@ -1,3 +1,8 @@
|
|
|
|
|
import * as fs from 'node:fs';
|
|
|
|
|
import * as os from 'node:os';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
import Fastify, { FastifyInstance } from 'fastify';
|
|
|
|
|
import fastifyStatic from '@fastify/static';
|
|
|
|
|
import { resolveStaticAssetHeaders } from './static.module';
|
|
|
|
|
|
|
|
|
|
// Unit tests for the static-asset cache classifier extracted from the
|
|
|
|
@@ -33,3 +38,69 @@ describe('resolveStaticAssetHeaders', () => {
|
|
|
|
|
expect(headers['vary']).toBe('Accept-Encoding');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Integration test proving the ACTUAL response header emitted by @fastify/static
|
|
|
|
|
// with the exact registration options StaticModule uses. This is the regression
|
|
|
|
|
// guard for #452: without `cacheControl: false`, @fastify/static writes its own
|
|
|
|
|
// `Cache-Control: public, max-age=0` AFTER the setHeaders callback, overwriting
|
|
|
|
|
// the immutable header — the /assets/ assertion below would then fail.
|
|
|
|
|
describe('static.module @fastify/static registration (integration)', () => {
|
|
|
|
|
let app: FastifyInstance;
|
|
|
|
|
let tmpDir: string;
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
|
tmpDir = fs.mkdtempSync(join(os.tmpdir(), 'static-module-spec-'));
|
|
|
|
|
fs.mkdirSync(join(tmpDir, 'assets'), { recursive: true });
|
|
|
|
|
fs.mkdirSync(join(tmpDir, 'locales'), { recursive: true });
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
join(tmpDir, 'assets', 'index-a1b2c3.js'),
|
|
|
|
|
'console.log(1);',
|
|
|
|
|
);
|
|
|
|
|
fs.writeFileSync(join(tmpDir, 'locales', 'en.json'), '{"hello":"world"}');
|
|
|
|
|
|
|
|
|
|
app = Fastify();
|
|
|
|
|
// Mirror StaticModule.onModuleInit's registration options exactly.
|
|
|
|
|
await app.register(fastifyStatic, {
|
|
|
|
|
root: tmpDir,
|
|
|
|
|
wildcard: false,
|
|
|
|
|
preCompressed: true,
|
|
|
|
|
cacheControl: false,
|
|
|
|
|
setHeaders: (res, filePath) => {
|
|
|
|
|
for (const [name, value] of Object.entries(
|
|
|
|
|
resolveStaticAssetHeaders(filePath),
|
|
|
|
|
)) {
|
|
|
|
|
res.setHeader(name, value);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
await app.ready();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
|
await app.close();
|
|
|
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('serves a hashed /assets/ file with an immutable, 1-year cache-control', async () => {
|
|
|
|
|
const res = await app.inject({
|
|
|
|
|
method: 'GET',
|
|
|
|
|
url: '/assets/index-a1b2c3.js',
|
|
|
|
|
});
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
|
const cacheControl = res.headers['cache-control'];
|
|
|
|
|
expect(cacheControl).toContain('immutable');
|
|
|
|
|
expect(cacheControl).toContain('max-age=31536000');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('serves a non-hashed /locales/ file WITHOUT an immutable cache-control', async () => {
|
|
|
|
|
const res = await app.inject({ method: 'GET', url: '/locales/en.json' });
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
|
// resolveStaticAssetHeaders sets no cache-control here and cacheControl:false
|
|
|
|
|
// stops @fastify/static from adding one, so the browser revalidates by
|
|
|
|
|
// etag/last-modified — either an absent header or one without `immutable`.
|
|
|
|
|
const cacheControl = res.headers['cache-control'];
|
|
|
|
|
if (cacheControl !== undefined) {
|
|
|
|
|
expect(cacheControl).not.toContain('immutable');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|