/**
* Static-asset caching (lib/web/static.js + the layout in helpers.js).
*
* Every asset used to go out as `no-cache, must-revalidate` with no
* validator, so all ten scripts/stylesheets in the layout were re-fetched
* in full on every page view — ~0.45s each through the 7rnx proxy.
*
* The contract these tests pin:
* - the layout stamps ?v=<ASSET_VERSION> onto every asset URL
* - a stamped request may be cached immutable for a year
* - an UNSTAMPED request may not (prism-autoloader builds its own URLs)
* - If-None-Match short-circuits to 304
*/
import test from 'brittle'
import { serveStatic, ASSET_VERSION } from '../lib/web/static.js'
import { layout } from '../lib/web/helpers.js'
// Minimal ServerResponse stand-in — serveStatic only uses writeHead/end.
function mockRes () {
return {
status: null,
headers: null,
body: null,
ended: false,
writeHead (status, headers) {
this.status = status
this.headers = headers
},
end (body) {
this.body = body
this.ended = true
},
header (name) {
const key = Object.keys(this.headers || {})
.find((k) => k.toLowerCase() === name.toLowerCase())
return key ? this.headers[key] : undefined
}
}
}
const ASSET = 'pgv3-flash.js'
const stampedUrl = `/static/${ASSET}?v=${encodeURIComponent(ASSET_VERSION)}`
test('ASSET_VERSION is a real release version, not the dev fallback', (t) => {
t.not(ASSET_VERSION, 'dev',
'package.json should be readable from lib/web/static.js')
t.ok(/^\d+\.\d+\.\d+/.test(ASSET_VERSION),
`expected a semver-ish version, got ${ASSET_VERSION}`)
})
test('the layout stamps every asset URL', (t) => {
const html = layout('t', '/repos', '<p>body</p>', { prefix: '/git' })
const assetUrls = [...html.matchAll(/(?:href|src)="([^"]*\/static\/[^"]+)"/g)]
.map((m) => m[1])
t.ok(assetUrls.length >= 10, `expected >=10 asset urls, got ${assetUrls.length}`)
for (const u of assetUrls) {
t.ok(u.includes('?v='), `carries a cache-buster: ${u}`)
}
})
test('a stamped request is cacheable immutable for a year', async (t) => {
const res = mockRes()
const handled = await serveStatic(ASSET, res, { url: stampedUrl, headers: {} })
t.ok(handled, 'asset served')
t.is(res.status, 200)
const cc = res.header('cache-control')
t.ok(cc.includes('immutable'), `expected immutable, got: ${cc}`)
t.ok(cc.includes('max-age=31536000'), `expected a year, got: ${cc}`)
t.ok(res.header('etag'), 'an ETag is present')
})
test('an UNSTAMPED request is NOT immutable', async (t) => {
// prism-autoloader appends filenames to languages_path itself, so those
// requests arrive with no ?v= and must stay bustable.
const res = mockRes()
await serveStatic(ASSET, res, { url: `/static/${ASSET}`, headers: {} })
const cc = res.header('cache-control')
t.absent(cc.includes('immutable'),
`unstamped assets must not be immutable, got: ${cc}`)
t.ok(cc.includes('max-age=3600'), `expected a 1h max-age, got: ${cc}`)
})
test('If-None-Match returns 304 with no body', async (t) => {
const first = mockRes()
await serveStatic(ASSET, first, { url: stampedUrl, headers: {} })
const etag = first.header('etag')
t.ok(etag, 'got an etag from the first response')
t.ok(first.body, 'first response carried a body')
const second = mockRes()
const handled = await serveStatic(ASSET, second, {
url: stampedUrl,
headers: { 'if-none-match': etag }
})
t.ok(handled, 'still handled')
t.is(second.status, 304, 'not modified')
t.absent(second.body, '304 carries no body')
t.is(second.header('etag'), etag, 'echoes the validator')
})
test('a stale validator still gets a full 200', async (t) => {
const res = mockRes()
await serveStatic(ASSET, res, {
url: stampedUrl,
headers: { 'if-none-match': '"0.0.0-1"' }
})
t.is(res.status, 200, 'mismatched etag must re-send the body')
t.ok(res.body, 'body present')
})
test('path traversal is still refused', async (t) => {
const res = mockRes()
const handled = await serveStatic('../../package.json', res, {
url: '/static/../../package.json',
headers: {}
})
t.absent(handled, 'traversal must not be served')
})