import { PhotonImage } from '@cf-wasm/photon' import { createExecutionContext, env, SELF, waitOnExecutionContext } from 'cloudflare:test' import { beforeAll, describe, expect, it } from 'vitest' import app from '../../img.app' import type { Env } from '../../context' declare module 'cloudflare:test' { interface ProvidedEnv extends Env {} } const ORIGIN = 'https://example.com' /** Decode a JPEG and read back its pixel dimensions. */ function jpegSize(bytes: Uint8Array): { width: number; height: number } { const img = PhotonImage.new_from_byteslice(bytes) try { return { width: img.get_width(), height: img.get_height() } } finally { img.free() } } // A tiny valid JPEG magic-number blob — enough to assert round-tripping. const IMAGE_BYTES = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46]) // Public half (SPKI DER, base64) of the dev IMG_SIGNING_KEY in wrangler.jsonc — // used to verify the Content-Signature header. const PUBLIC_SPKI_B64 = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1EIcBzPCvOFRy3WYuG8ICaRyr/OpotABJBpiMq2zcZHsSPXQw7NC+N082JDqYLy627oB9qJ+wC3idtbzFTANLkIYIEWMWJC9hjWl56vBVXOIroji2+lOpR4hV9JRdgmJfBYXmJPtHRP4GAl8np9xcnZpbMJdauR+HIJiQT3QHc2RomLXWCUfOb564cW8Ks7CLlmXPWf4M77DufHhY+788uWq6bI0+QSJ1qrUi3gaou0HPj7YPTl7pUTwX4VOmHKN5Nw+/jB9f2JNpRKp9niylCVUgdHnmHz5iqMW86HRf7EJcalSyYn7cC6b1ng9GPYryybipZ7QuTgl52qu2GQDaQIDAQAB' // An R2-only key that has no matching file in `static/`, so it exercises the // bucket path rather than a static asset. const R2_KEY = 'user-photo.jpg' // An extensionless name, as returned by the `storage` worker for a FileType 3 // upload — served from `recflare-cdn` under `image/`, not `recflare-img`. const CDN_NAME = '2028-06-01/12345-67890-12345' /** * Fetch with real signing turned OFF — the deployed default. `vitest.config.ts` * binds `IMG_SIGNING_ENABLED` on so the RSA path stays covered, so the placeholder * path has to drive the app directly with an overridden env. */ async function unsignedFetch(url: string): Promise { const ctx = createExecutionContext() const res = await app.fetch(new Request(url), { ...env, IMG_SIGNING_ENABLED: false }, ctx) await waitOnExecutionContext(ctx) return res } beforeAll(async () => { await env.IMAGES.put(R2_KEY, IMAGE_BYTES, { httpMetadata: { contentType: 'image/jpeg' }, }) await env.CDN_ASSETS.put(`image/${CDN_NAME}`, IMAGE_BYTES, { httpMetadata: { contentType: 'image/jpeg' }, }) // Seed R2 with a key that ALSO exists in `static/` to prove static wins. await env.IMAGES.put('3DCharades.jpg', IMAGE_BYTES, { httpMetadata: { contentType: 'image/jpeg' }, }) }) describe('img endpoints', () => { it('GET / reports service status', async () => { const res = await SELF.fetch(`${ORIGIN}/`) expect(res.status).toBe(200) expect(await res.json()).toEqual({ service: 'img', status: 'ok' }) }) // An upload keeps whatever content-type the uploader claimed, so a stored HTML // document would otherwise render — and run — on this origin. Every response says // `nosniff` and carries a sandboxing CSP so the body is inert whatever it is. it('serves every response with headers that stop a stored HTML upload executing', async () => { const HTML_KEY = 'not-an-image.jpg' await env.IMAGES.put(HTML_KEY, '', { httpMetadata: { contentType: 'text/html' }, }) const html = await SELF.fetch(`${ORIGIN}/${HTML_KEY}`) expect(html.status).toBe(200) expect(html.headers.get('x-content-type-options')).toBe('nosniff') expect(html.headers.get('content-security-policy')).toBe("default-src 'none'; sandbox") // The stored object, the static asset, the fallback, a resize, and a 304. const paths = [ `/${R2_KEY}`, `/${CDN_NAME}`, '/does-not-exist-anywhere.jpg', '/3DCharades.jpg?width=128', '/', ] for (const path of paths) { const res = await SELF.fetch(`${ORIGIN}${path}`) expect(res.status, path).toBe(200) expect(res.headers.get('x-content-type-options'), path).toBe('nosniff') expect(res.headers.get('content-security-policy'), path).toBe("default-src 'none'; sandbox") } const etag = (await SELF.fetch(`${ORIGIN}/${R2_KEY}`)).headers.get('etag') const notModified = await SELF.fetch(`${ORIGIN}/${R2_KEY}`, { headers: { 'if-none-match': etag! }, }) expect(notModified.status).toBe(304) expect(notModified.headers.get('x-content-type-options')).toBe('nosniff') }) it('streams an image stored in R2 with its content type', async () => { const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/jpeg') expect(new Uint8Array(await res.arrayBuffer())).toEqual(IMAGE_BYTES) }) // Images are served with the same 30-day window `cdn` uses for its blobs, plus // `immutable` — an image never changes under its key, a new one takes a new key. it('every served image carries the 30-day Cache-Control', async () => { const CACHE_CONTROL = `public, max-age=${86400 * 30}, immutable` // The stored object, a static asset, and the fallback for a key in neither. for (const path of [`/${R2_KEY}`, `/${CDN_NAME}`, '/does-not-exist-anywhere.jpg']) { const res = await SELF.fetch(`${ORIGIN}${path}`) expect(res.status, path).toBe(200) expect(res.headers.get('cache-control'), path).toBe(CACHE_CONTROL) } // And the transformed variant, which is rebuilt rather than streamed. Uses a real // (decodable) JPEG, since a resize has to actually run for this path to be reached. const resized = await SELF.fetch(`${ORIGIN}/3DCharades.jpg?width=128`) expect(resized.status).toBe(200) expect(resized.headers.get('cache-control')).toBe(CACHE_CONTROL) }) it('serves an extensionless key from the cdn bucket under image/', async () => { const res = await SELF.fetch(`${ORIGIN}/${CDN_NAME}`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/jpeg') expect(new Uint8Array(await res.arrayBuffer())).toEqual(IMAGE_BYTES) }) it('does not look for an extensionless key in the image bucket', async () => { // Same bare name seeded into `recflare-img` instead: extensionless keys only // ever resolve against `recflare-cdn`, so this falls through to the default. await env.IMAGES.put('2028-06-02/only-in-img', IMAGE_BYTES) const res = await SELF.fetch(`${ORIGIN}/2028-06-02/only-in-img`) expect(res.status).toBe(200) const body = new Uint8Array(await res.arrayBuffer()) expect(body.length).toBeGreaterThan(IMAGE_BYTES.length) }) it('resizes an extensionless cdn image', async () => { // Exercises the transform path against the cdn bucket, not just the stream-through. // Needs a decodable JPEG, so reuse a bundled static asset's bytes. const real = await (await SELF.fetch(`${ORIGIN}/3DCharades.jpg`)).arrayBuffer() await env.CDN_ASSETS.put('image/2028-06-03/real-photo', real, { httpMetadata: { contentType: 'image/jpeg' }, }) const res = await SELF.fetch(`${ORIGIN}/2028-06-03/real-photo?width=128`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/jpeg') expect(res.headers.get('etag')).toBeNull() expect(jpegSize(new Uint8Array(await res.arrayBuffer())).width).toBe(128) }) it('honours the 64 width some newer endpoints request', async () => { const res = await SELF.fetch(`${ORIGIN}/3DCharades.jpg?width=64`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/jpeg') expect(jpegSize(new Uint8Array(await res.arrayBuffer())).width).toBe(64) }) it('serves a static asset in preference to an R2 object of the same key', async () => { const res = await SELF.fetch(`${ORIGIN}/3DCharades.jpg`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toMatch(/^image\/jpeg/) // The bundled static JPEG, not the tiny IMAGE_BYTES stub seeded into R2. const body = new Uint8Array(await res.arrayBuffer()) expect(body.length).toBeGreaterThan(IMAGE_BYTES.length) expect(body[0]).toBe(0xff) expect(body[1]).toBe(0xd8) }) it('serves a nested static asset', async () => { const res = await SELF.fetch(`${ORIGIN}/Base/Clearcut.jpg`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toMatch(/^image\/jpeg/) const body = new Uint8Array(await res.arrayBuffer()) expect(body.length).toBeGreaterThan(0) expect(body[0]).toBe(0xff) expect(body[1]).toBe(0xd8) }) it('returns 304 when If-None-Match matches the etag', async () => { const first = await SELF.fetch(`${ORIGIN}/${R2_KEY}`) const etag = first.headers.get('etag') expect(etag).toBeTruthy() const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`, { headers: { 'If-None-Match': etag! }, }) expect(res.status).toBe(304) }) it('honors a Range request on the stored image with a 206', async () => { const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`, { headers: { Range: 'bytes=2-4' } }) expect(res.status).toBe(206) expect(res.headers.get('content-range')).toBe('bytes 2-4/8') expect(res.headers.get('accept-ranges')).toBe('bytes') expect(new Uint8Array(await res.arrayBuffer())).toEqual(IMAGE_BYTES.slice(2, 5)) }) // R2 resolves a range it cannot parse or satisfy to the WHOLE object rather than // failing. Handing that back as a bare 200 is the shape that corrupts a chunked // download — the client wrote a whole file where it expected a slice — so every one // of these still states what the body holds. it('never answers a bytes range with a whole-object 200', async () => { for (const range of ['bytes=100-200', 'bytes=abc', 'bytes=0-1,3-4', 'bytes=0-7']) { const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`, { headers: { Range: range } }) expect(res.status, range).toBe(206) expect(res.headers.get('content-range'), range).toBe('bytes 0-7/8') } // A unit other than bytes must be ignored outright (RFC 9110), not answered with // a byte-denominated Content-Range. const other = await SELF.fetch(`${ORIGIN}/${R2_KEY}`, { headers: { Range: 'items=0-1' } }) expect(other.status).toBe(200) expect(other.headers.get('content-range')).toBeNull() }) // A resize decodes the whole image, so there is no meaningful slice of the source to // read — the range is ignored and the whole transformed result served, which is the // legal answer. What it must NOT do is claim a 206 over bytes it rebuilt. Runs against // the R2 path (a decodable JPEG borrowed from `static/`), since that is the one that // has a range to suppress; the static-asset path is never handed one at all. it('ignores a Range when a transform rebuilds the body', async () => { const real = await (await SELF.fetch(`${ORIGIN}/3DCharades.jpg`)).arrayBuffer() await env.IMAGES.put('ranged-transform.jpg', real, { httpMetadata: { contentType: 'image/jpeg' }, }) const res = await SELF.fetch(`${ORIGIN}/ranged-transform.jpg?width=128`, { headers: { Range: 'bytes=0-9' }, }) expect(res.status).toBe(200) expect(res.headers.get('content-range')).toBeNull() expect(res.headers.get('accept-ranges')).toBeNull() expect(jpegSize(new Uint8Array(await res.arrayBuffer())).width).toBe(128) // Same for a real RSA signature, which covers the whole body (the test env binds // IMG_SIGNING_ENABLED on, so `?sig=p1` takes the signing path rather than the stub). const signed = await SELF.fetch(`${ORIGIN}/ranged-transform.jpg?sig=p1`, { headers: { Range: 'bytes=0-9' }, }) expect(signed.status).toBe(200) expect(signed.headers.get('content-range')).toBeNull() expect(signed.headers.get('content-signature')).toContain('key-id=KEY:RSA:p1.rec.net') expect(new Uint8Array(await signed.arrayBuffer()).byteLength).toBe(real.byteLength) }) it('serves the DefaultProfileImage.jpg fallback for a missing image', async () => { const res = await SELF.fetch(`${ORIGIN}/missing.png`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toMatch(/^image\/jpeg/) const body = new Uint8Array(await res.arrayBuffer()) // Real JPEG static asset: SOI marker + non-empty body. expect(body.length).toBeGreaterThan(0) expect(body[0]).toBe(0xff) expect(body[1]).toBe(0xd8) }) it('signs the DefaultProfileImage.jpg fallback with ?sig=p1', async () => { const res = await SELF.fetch(`${ORIGIN}/missing.png?sig=p1`) expect(res.status).toBe(200) const header = res.headers.get('content-signature') expect(header).toMatch(/^key-id=KEY:RSA:p1\.rec\.net; data=/) const signatureB64 = header!.split('data=')[1] const signature = Uint8Array.from(atob(signatureB64), (ch) => ch.charCodeAt(0)) const body = new Uint8Array(await res.arrayBuffer()) const publicKey = await crypto.subtle.importKey( 'spki', Uint8Array.from(atob(PUBLIC_SPKI_B64), (ch) => ch.charCodeAt(0)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-1' }, false, ['verify'] ) const ok = await crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, body) expect(ok).toBe(true) }) it('signs the response with ?sig=p1 and the signature verifies', async () => { const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}?sig=p1`) expect(res.status).toBe(200) const header = res.headers.get('content-signature') expect(header).toMatch(/^key-id=KEY:RSA:p1\.rec\.net; data=/) const signatureB64 = header!.split('data=')[1] const signature = Uint8Array.from(atob(signatureB64), (ch) => ch.charCodeAt(0)) const body = new Uint8Array(await res.arrayBuffer()) const publicKey = await crypto.subtle.importKey( 'spki', Uint8Array.from(atob(PUBLIC_SPKI_B64), (ch) => ch.charCodeAt(0)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-1' }, false, ['verify'] ) const ok = await crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, body) expect(ok).toBe(true) }) it('does not sign without ?sig=p1', async () => { const res = await SELF.fetch(`${ORIGIN}/${R2_KEY}`) expect(res.headers.get('content-signature')).toBeNull() }) it('returns a placeholder signature when IMG_SIGNING_ENABLED is off', async () => { // The deployed default (see wrangler.jsonc). The header must still be there — // the client requires it — but the value is derived from the key, so the body // is neither buffered nor hashed and streams straight out of R2. const res = await unsignedFetch(`${ORIGIN}/${R2_KEY}?sig=p1`) expect(res.status).toBe(200) const header = res.headers.get('content-signature') expect(header).toMatch(/^key-id=KEY:RSA:p1\.rec\.net; data=/) // Same shape as a real RSA-2048 signature, so the client's parser sees no // difference between the two modes. const signature = Uint8Array.from(atob(header!.split('data=')[1]), (ch) => ch.charCodeAt(0)) expect(signature.length).toBe(256) expect(signature.some((b) => b !== 0)).toBe(true) // Still on the streaming path: the source etag survives and the bytes are the // stored object, untouched. expect(res.headers.get('etag')).toBeTruthy() expect(new Uint8Array(await res.arrayBuffer())).toEqual(IMAGE_BYTES) }) it('derives the placeholder signature from the key, stably', async () => { const sigFor = async (path: string) => (await unsignedFetch(`${ORIGIN}/${path}?sig=p1`)).headers.get('content-signature') // Stable for a key, so a cached response and a fresh one agree... expect(await sigFor(R2_KEY)).toBe(await sigFor(R2_KEY)) // ...and distinct across keys, so it isn't a single hardcoded constant. expect(await sigFor(R2_KEY)).not.toBe(await sigFor(CDN_NAME)) }) it('signs the fallback and resized bodies with a placeholder too', async () => { // The fallback (missing key) and the transform path both go through // serveStaticAsset/finalizeImage — the header must survive both. const fallback = await unsignedFetch(`${ORIGIN}/missing.png?sig=p1`) expect(fallback.headers.get('content-signature')).toMatch(/^key-id=KEY:RSA:p1\.rec\.net; /) const resized = await unsignedFetch(`${ORIGIN}/RecCenter.jpg?width=512&sig=p1`) expect(resized.headers.get('content-signature')).toMatch(/^key-id=KEY:RSA:p1\.rec\.net; /) expect(jpegSize(new Uint8Array(await resized.arrayBuffer())).width).toBe(512) }) it('resizes a static asset to ?width, preserving aspect ratio', async () => { const full = new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer()) const original = jpegSize(full) const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=512`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/jpeg') // Resized responses carry no etag (the source etag no longer describes them). expect(res.headers.get('etag')).toBeNull() const body = new Uint8Array(await res.arrayBuffer()) const resized = jpegSize(body) expect(resized.width).toBe(512) // Height scales with the source aspect ratio (allow 1px rounding). expect(resized.height).toBeCloseTo(Math.round((original.height / original.width) * 512), -0.5) }) it('center-crops to a square and resizes with ?cropSquare=1&width', async () => { const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=256&cropSquare=1`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/jpeg') const size = jpegSize(new Uint8Array(await res.arrayBuffer())) expect(size.width).toBe(256) expect(size.height).toBe(256) }) it('keeps a PNG source as PNG when resizing, alpha intact', async () => { // A 64×64 RGBA PNG: left half opaque red, right half fully transparent. Built // through Photon so the bytes are a real PNG with an alpha channel. const side = 64 const rgba = new Uint8Array(side * side * 4) for (let y = 0; y < side; y++) { for (let x = 0; x < side; x++) { const i = (y * side + x) * 4 rgba[i] = 255 rgba[i + 3] = x < side / 2 ? 255 : 0 } } const src = new PhotonImage(rgba, side, side) const pngBytes = src.get_bytes() src.free() // Stored under a `.bin` name with a claimed PNG type, like a custom avatar item's // design; the codec must go by the bytes, not the name. await env.IMAGES.put('avatar-item/2026-08-26/abc-design.bin', pngBytes, { httpMetadata: { contentType: 'image/png' }, }) const res = await SELF.fetch(`${ORIGIN}/avatar-item/2026-08-26/abc-design.bin?width=128`) expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/png') expect(res.headers.get('etag')).toBeNull() const out = PhotonImage.new_from_byteslice(new Uint8Array(await res.arrayBuffer())) try { expect(out.get_width()).toBe(128) const px = out.get_raw_pixels() const alphaAt = (x: number, y: number) => px[(y * 128 + x) * 4 + 3] // Deep inside each half, away from the Lanczos edge: opaque left, transparent right. expect(alphaAt(16, 64)).toBe(255) expect(alphaAt(112, 64)).toBe(0) } finally { out.free() } }) it('crops to a square at native size with ?cropSquare=1 alone', async () => { const full = jpegSize( new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer()) ) const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?cropSquare=1`) expect(res.status).toBe(200) const size = jpegSize(new Uint8Array(await res.arrayBuffer())) expect(size.width).toBe(size.height) // The square's side is the shorter source dimension. expect(size.width).toBe(Math.min(full.width, full.height)) }) it('ignores an invalid ?width and serves the original', async () => { const full = new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer()) const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=0`) expect(res.status).toBe(200) expect(new Uint8Array(await res.arrayBuffer())).toEqual(full) }) it('ignores a ?width outside the allowed sizes and serves the original', async () => { const full = new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer()) // 300 isn't one of 64/128/256/512/1024, so it's rejected and the source served. const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=300`) expect(res.status).toBe(200) expect(new Uint8Array(await res.arrayBuffer())).toEqual(full) }) it('signs the resized body with ?width and ?sig=p1', async () => { const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=512&sig=p1`) expect(res.status).toBe(200) expect(jpegSize(new Uint8Array(await res.clone().arrayBuffer())).width).toBe(512) const header = res.headers.get('content-signature') expect(header).toMatch(/^key-id=KEY:RSA:p1\.rec\.net; data=/) const signature = Uint8Array.from(atob(header!.split('data=')[1]), (ch) => ch.charCodeAt(0)) const body = new Uint8Array(await res.arrayBuffer()) const publicKey = await crypto.subtle.importKey( 'spki', Uint8Array.from(atob(PUBLIC_SPKI_B64), (ch) => ch.charCodeAt(0)), { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-1' }, false, ['verify'] ) // The signature must verify over the RESIZED bytes the client receives. const ok = await crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signature, body) expect(ok).toBe(true) }) it('GET /openapi.json documents every route', async () => { const res = await SELF.fetch(`${ORIGIN}/openapi.json`) expect(res.status).toBe(200) const spec = (await res.json()) as { openapi: string paths: Record> } expect(spec.openapi).toMatch(/^3\.1/) // The spec route hides itself. expect(spec.paths['/openapi.json']).toBeUndefined() // Every route the worker serves is described. This is the drift guard: adding a // route without a describeRoute() block fails here rather than silently shipping // an incomplete spec. Hono's `:param` syntax becomes OpenAPI's `{param}`. const documented = new Set( Object.entries(spec.paths).flatMap(([path, ops]) => Object.keys(ops).map((method) => `${method.toUpperCase()} ${path}`) ) ) expect([...documented].sort()).toEqual(['GET /', 'GET /{key}']) // Every operation carries a summary — a path present but undescribed is not // documentation. for (const ops of Object.values(spec.paths)) { for (const op of Object.values(ops)) expect(op.summary).toBeTruthy() } // Schemas must inline: a `$ref` here is a dangling reference (see openapi.ts). expect(JSON.stringify(spec).includes('"$ref"')).toBe(false) }) })