OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / static-cache.test.js

126 lines · javascript ref:
/**
 * 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')
})