From dd0ee35085c0cc6b232fdc4f25504ba6a297cf65 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 15 Oct 2026 19:56:16 +0200 Subject: [PATCH] test(19.3-05): site purge route handler specs and edge cases --- tests/sitePurge.test.ts | 120 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tests/sitePurge.test.ts b/tests/sitePurge.test.ts index e66c68f..733f057 100644 --- a/tests/sitePurge.test.ts +++ b/tests/sitePurge.test.ts @@ -95,3 +95,123 @@ test('sitePurge: purge event kinds and slugs', () => { assert.deepEqual(parseSitePurgeEvent(body), { kind: 'unknown', slug: '' }, JSON.stringify(body)) } }) + +test('sitePurge: ok in constant time over sha256 digests, whatever the bearer length', () => { + // timingSafeEqual on raw strings throws on a length mismatch; over digests + // every length compares in constant time and answers unauthorized. + for (const length of [1, 31, 33, 64, 4096]) { + assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${'a'.repeat(length)}`, proxied: false }), 'unauthorized', `length ${length}`) + } + const long = 'k'.repeat(200) + assert.equal(authorizeSitePurge({ token: long, authorization: `Bearer ${long}`, proxied: false }), 'ok') + assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer\t${TOKEN}\t`, proxied: false }), 'ok') + assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `BEARER ${TOKEN} `, proxied: false }), 'ok') + assert.equal(authorizeSitePurge({ token: TOKEN, authorization: ` Bearer ${TOKEN}`, proxied: false }), 'unauthorized') + assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}\n`, proxied: false }), 'unauthorized') +}) + +test('sitePurge: purge works with a synchronous storage and skips the bare base key', async () => { + const live = new Set([`${SITE_ROUTE_CACHE_BASE}:_:a.json`, SITE_ROUTE_CACHE_BASE, 'nitro:other:_:b.json']) + const removed: string[] = [] + const storage = { + getKeys: (base?: string) => [...live].filter((k) => !base || k === base || k.startsWith(base + ':')), + removeItem: (key: string) => { + live.delete(key) + removed.push(key) + }, + } + assert.equal(await purgeSiteRouteCache(storage), 1) + assert.deepEqual(removed, [`${SITE_ROUTE_CACHE_BASE}:_:a.json`]) + assert.ok(live.has(SITE_ROUTE_CACHE_BASE)) + assert.equal(await purgeSiteRouteCache({ getKeys: () => [], removeItem: () => assert.fail('nothing to remove') }), 0) +}) + +test('sitePurge: purge event edge cases', () => { + assert.deepEqual(parseSitePurgeEvent({ kind: 'a'.repeat(40), slug: '' }), { kind: 'a'.repeat(40), slug: '' }) + assert.deepEqual(parseSitePurgeEvent({ kind: 'faq' }), { kind: 'faq', slug: '' }) + assert.deepEqual(parseSitePurgeEvent({ kind: 'faq', slug: 42 }), { kind: 'faq', slug: '' }) + assert.deepEqual(parseSitePurgeEvent({ kind: 'post', slug: 'x', extra: 'ignored' }), { kind: 'post', slug: 'x' }) + assert.equal(parseSitePurgeEvent({ kind: 'faq', slug: 'y'.repeat(200) }).slug.length, 200) + // Control characters are stripped before the cut. + const slug = '\u0001'.repeat(50) + 'z'.repeat(200) + assert.equal(parseSitePurgeEvent({ kind: 'faq', slug }).slug, 'z'.repeat(200)) + assert.deepEqual(parseSitePurgeEvent({}), { kind: 'unknown', slug: '' }) +}) + +/** One call of the shared route with Nitro's auto-imports stubbed. */ +async function callRoute(opts: { token?: unknown; headers?: Record; body?: unknown; bodyError?: boolean; keys?: string[] }) { + const responseHeaders: Record = {} + const logs: string[] = [] + const live = new Set(opts.keys ?? []) + const g = globalThis as Record + g.defineEventHandler = (fn: unknown) => fn + g.setResponseHeader = (_e: unknown, name: string, value: string) => { + responseHeaders[name] = value + } + g.useRuntimeConfig = () => ({ siteCache: opts.token === undefined ? undefined : { purgeToken: opts.token } }) + g.getRequestHeader = (_e: unknown, name: string) => opts.headers?.[name] + g.createError = (input: { statusCode: number; statusMessage: string }) => Object.assign(new Error(input.statusMessage), input) + g.readBody = async () => { + if (opts.bodyError) throw new Error('invalid JSON') + return opts.body + } + g.useStorage = (base: string) => { + assert.equal(base, 'cache') + return { + getKeys: async (prefix?: string) => [...live].filter((k) => !prefix || k.startsWith(prefix + ':')), + removeItem: async (key: string) => { + live.delete(key) + }, + } + } + const info = console.info + console.info = (line: string) => { + logs.push(line) + } + try { + const route = (await import('../server/api/__site__/cache/purge.post.ts')).default as (e: unknown) => Promise + try { + return { result: await route({}), status: 200, responseHeaders, logs, live } + } catch (err) { + return { result: undefined, status: (err as { statusCode?: number }).statusCode, responseHeaders, logs, live } + } + } finally { + console.info = info + } +} + +test('sitePurge: route answers 404 when disabled or proxied and 401 for a wrong bearer', async () => { + const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`] + for (const token of [undefined, '', 'short']) { + const r = await callRoute({ token, headers: { authorization: `Bearer ${String(token)}` }, keys }) + assert.equal(r.status, 404, `token ${JSON.stringify(token)}`) + assert.equal(r.live.size, 1) + } + for (const name of ['x-forwarded-for', 'x-real-ip', 'forwarded']) { + const r = await callRoute({ token: TOKEN, headers: { authorization: `Bearer ${TOKEN}`, [name]: '203.0.113.9' }, keys }) + assert.equal(r.status, 404, name) + assert.equal(r.live.size, 1) + } + for (const authorization of [undefined, 'Bearer wrong', `Basic ${TOKEN}`]) { + const r = await callRoute({ token: TOKEN, headers: authorization ? { authorization } : {}, keys }) + assert.equal(r.status, 401, String(authorization)) + assert.equal(r.live.size, 1) + assert.equal(r.responseHeaders['cache-control'], 'no-store') + } +}) + +test('sitePurge: route purges, answers the count and logs kind and slug only', async () => { + const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`, `${SITE_ROUTE_CACHE_BASE}:_:faq.json`, 'nitro:functions:_:x.json'] + const r = await callRoute({ token: TOKEN, headers: { authorization: `Bearer ${TOKEN}` }, body: { kind: 'faq', slug: 'ship\nping' }, keys }) + assert.equal(r.status, 200) + assert.deepEqual(r.result, { purged: 2 }) + assert.deepEqual([...r.live], ['nitro:functions:_:x.json']) + assert.equal(r.responseHeaders['cache-control'], 'no-store') + assert.deepEqual(r.logs, ['[site-cache] purge: removed 2 route cache entries (faq shipping)']) + assert.ok(!r.logs.join('').includes(TOKEN)) + + const bad = await callRoute({ token: TOKEN, headers: { authorization: `Bearer ${TOKEN}` }, bodyError: true, keys: [] }) + assert.equal(bad.status, 200) + assert.deepEqual(bad.result, { purged: 0 }) + assert.deepEqual(bad.logs, ['[site-cache] purge: removed 0 route cache entries (unknown )']) +})