- exact names: purge auth disabled, proxied before the token, missing or wrong bearer; purge removes every route cache entry and only those - parsePurgeEvent kinds, control characters, 200-character cap, non-objects - privateRevalidate with and without max-age, the 304 form, static values
130 lines
6.2 KiB
TypeScript
130 lines
6.2 KiB
TypeScript
// Site cache purge: route authorization, the route-cache purge, the purge
|
|
// body parsing and browser revalidation of route-cached responses.
|
|
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {
|
|
MIN_PURGE_TOKEN_LENGTH,
|
|
ROUTE_CACHE_BASE,
|
|
authorizePurge,
|
|
parsePurgeEvent,
|
|
privateRevalidate,
|
|
purgeRouteCache,
|
|
} from '../app/utils/siteCache.ts'
|
|
|
|
const TOKEN = 'a'.repeat(MIN_PURGE_TOKEN_LENGTH)
|
|
|
|
test('purge auth: a missing or short token disables the route', () => {
|
|
for (const token of [undefined, null, '', 'short', 'b'.repeat(MIN_PURGE_TOKEN_LENGTH - 1), 12345678901234567890, {}]) {
|
|
assert.equal(authorizePurge({ token, authorization: `Bearer ${String(token)}`, proxied: false }), 'disabled', `token ${JSON.stringify(token)}`)
|
|
}
|
|
// Disabled wins over proxied: an app without a token has no route at all.
|
|
assert.equal(authorizePurge({ token: '', authorization: 'Bearer x', proxied: true }), 'disabled')
|
|
assert.equal(MIN_PURGE_TOKEN_LENGTH, 32)
|
|
})
|
|
|
|
test('purge auth: a proxied request is refused before the token is checked', () => {
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: true }), 'proxied')
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization: 'Bearer wrong', proxied: true }), 'proxied')
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization: null, proxied: true }), 'proxied')
|
|
})
|
|
|
|
test('purge auth: a missing or wrong bearer token is unauthorized', () => {
|
|
for (const authorization of [undefined, null, '', 'Bearer', 'Bearer ', `Basic ${TOKEN}`, `${TOKEN}`, `Bearer ${TOKEN}x`, `Bearer ${TOKEN.slice(1)}`, `Bearer ${TOKEN} extra`, 'Bearer ' + 'b'.repeat(32)]) {
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization, proxied: false }), 'unauthorized', `authorization ${JSON.stringify(authorization)}`)
|
|
}
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: false }), 'ok')
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization: `bearer ${TOKEN}`, proxied: false }), 'ok')
|
|
assert.equal(authorizePurge({ token: TOKEN, authorization: `Bearer ${TOKEN} `, proxied: false }), 'ok')
|
|
})
|
|
|
|
/** A fake unstorage prefix storage over a plain key list. */
|
|
function fakeStorage(keys: string[]) {
|
|
const live = new Set(keys)
|
|
const removed: string[] = []
|
|
return {
|
|
removed,
|
|
live,
|
|
async getKeys(base?: string) {
|
|
return [...live].filter((k) => !base || k === base || k.startsWith(base + ':'))
|
|
},
|
|
async removeItem(key: string) {
|
|
live.delete(key)
|
|
removed.push(key)
|
|
},
|
|
}
|
|
}
|
|
|
|
test('purge removes every route cache entry and only those', async () => {
|
|
const routes = [
|
|
`${ROUTE_CACHE_BASE}:_:cordyceps.abc123.json`,
|
|
`${ROUTE_CACHE_BASE}:_:cordyceps_payloadjson.def456.json`,
|
|
`${ROUTE_CACHE_BASE}:_:index.0a1b2c.json`,
|
|
`${ROUTE_CACHE_BASE}:_:rssxml.99.json`,
|
|
]
|
|
const others = ['nitro:functions:_:journal.1.json', 'nitro:handlers:_:api.2.json', 'nitro:routesx:_:near.3.json', 'other:routes:_:x.json']
|
|
const storage = fakeStorage([...routes, ...others])
|
|
assert.equal(ROUTE_CACHE_BASE, 'nitro:routes')
|
|
assert.equal(await purgeRouteCache(storage), routes.length)
|
|
assert.deepEqual([...storage.removed].sort(), [...routes].sort())
|
|
assert.deepEqual([...storage.live].sort(), [...others].sort())
|
|
assert.equal(await purgeRouteCache(storage), 0)
|
|
})
|
|
|
|
test('purge ignores keys a storage returns outside the route cache', async () => {
|
|
const storage = {
|
|
removed: [] as string[],
|
|
getKeys: () => ['nitro:routes:_:a.json', 'nitro:functions:_:b.json'],
|
|
removeItem(key: string) {
|
|
this.removed.push(key)
|
|
},
|
|
}
|
|
assert.equal(await purgeRouteCache(storage), 1)
|
|
assert.deepEqual(storage.removed, ['nitro:routes:_:a.json'])
|
|
})
|
|
|
|
test('purge event: kinds', () => {
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'post', slug: 'hello' }), { kind: 'post', slug: 'hello' })
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'category', slug: 'cordyceps' }), { kind: 'category', slug: 'cordyceps' })
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'tag', slug: 'x' }), { kind: 'unknown', slug: 'x' })
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'POST', slug: 'x' }), { kind: 'unknown', slug: 'x' })
|
|
assert.deepEqual(parsePurgeEvent({ slug: 'x' }), { kind: 'unknown', slug: 'x' })
|
|
})
|
|
|
|
test('purge event: control characters are removed from the slug', () => {
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'post', slug: 'a\nb\r\tc\u0000d\u001be\u007ff\u0085g' }), { kind: 'post', slug: 'abcdefg' })
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'post', slug: 'zażółć-gęślą' }), { kind: 'post', slug: 'zażółć-gęślą' })
|
|
})
|
|
|
|
test('purge event: the slug is cut to 200 characters', () => {
|
|
assert.equal(parsePurgeEvent({ kind: 'post', slug: 'x'.repeat(500) }).slug.length, 200)
|
|
assert.equal(parsePurgeEvent({ kind: 'post', slug: 'x'.repeat(200) }).slug.length, 200)
|
|
})
|
|
|
|
test('purge event: a body that is not an object', () => {
|
|
for (const body of [null, undefined, 'post', 42, true, ['post', 'x']]) {
|
|
assert.deepEqual(parsePurgeEvent(body), { kind: 'unknown', slug: '' }, JSON.stringify(body))
|
|
}
|
|
assert.deepEqual(parsePurgeEvent({ kind: 'post', slug: 7 }), { kind: 'post', slug: '' })
|
|
})
|
|
|
|
test('revalidate: s-maxage without max-age gets max-age=0', () => {
|
|
assert.equal(privateRevalidate('s-maxage=300, stale-while-revalidate'), 'max-age=0, s-maxage=300, stale-while-revalidate')
|
|
assert.equal(privateRevalidate('s-maxage=120'), 'max-age=0, s-maxage=120')
|
|
})
|
|
|
|
test('revalidate: s-maxage with max-age replaces max-age', () => {
|
|
assert.equal(privateRevalidate('max-age=300, s-maxage=300, stale-while-revalidate'), 'max-age=0, s-maxage=300, stale-while-revalidate')
|
|
assert.equal(privateRevalidate('s-maxage=300,max-age=60'), 's-maxage=300,max-age=0')
|
|
})
|
|
|
|
test('revalidate: the 304 form of the route cache', () => {
|
|
assert.equal(privateRevalidate('public, max-age=300, s-maxage=300'), 'public, max-age=0, s-maxage=300')
|
|
})
|
|
|
|
test('revalidate: values without s-maxage are unchanged', () => {
|
|
assert.equal(privateRevalidate('public, max-age=31536000, immutable'), 'public, max-age=31536000, immutable')
|
|
assert.equal(privateRevalidate('no-store'), 'no-store')
|
|
assert.equal(privateRevalidate('no-cache'), 'no-cache')
|
|
assert.equal(privateRevalidate(''), '')
|
|
})
|