import { describe, expect, it, vi } from "vitest"; import type { ImageProbeRequest, } from "../../src/application/ports/browser-transfer/image-cdn.ts"; import { createBrowserImageProbe } from "../../src/adapters/browser-transfer/image-cdn/browser-image-probe.ts"; import { avifBytes, jpegBytes, manualImageProbeScheduler, pngBytes, publicImageHeaders, responseAt, webpBytes, } from "./image-cdn-test-fixture.ts"; describe("browser image probe", () => { const imageUrl = "https://images.example.test/v1/assets/a/rev?format=png"; const request = ( overrides: Partial = {}, ): ImageProbeRequest => ({ absoluteUrl: imageUrl, expectedMediaType: "image/png", expectedWidth: 640, expectedHeight: 360, maxEncodedBytes: 1_024, maxDecodedPixels: 230_400, maxDecodedBytes: 921_600, delivery: "PUBLIC_IMMUTABLE", minimumPublicMaxAgeSeconds: 31_536_000, referrerPolicy: "no-referrer", signal: new AbortController().signal, ...overrides, }); it("parses all supported static headers before decode and closes each bitmap", async () => { const samples = [ { mediaType: "image/png" as const, bytes: pngBytes(640, 360), }, { mediaType: "image/jpeg" as const, bytes: jpegBytes(640, 360), }, { mediaType: "image/webp" as const, bytes: webpBytes(640, 360), }, { mediaType: "image/avif" as const, bytes: avifBytes(640, 360), }, ]; const close = vi.fn(); for (const sample of samples) { const exactUrl = imageUrl.replace( "format=png", `format=${sample.mediaType.slice("image/".length)}`, ); const fetcher = vi.fn(async () => responseAt(exactUrl, sample.bytes, { status: 200, headers: publicImageHeaders( sample.mediaType, sample.bytes.byteLength, ), }), ); const probe = createBrowserImageProbe({ fetcher: fetcher as typeof fetch, createBitmap: vi.fn(async () => ({ width: 640, height: 360, close, })), }); await expect( probe.probe( request({ absoluteUrl: exactUrl, expectedMediaType: sample.mediaType, }), ), ).resolves.toMatchObject({ ok: true, value: { absoluteUrl: exactUrl, mediaType: sample.mediaType, encodedBytes: sample.bytes.byteLength, decodedWidth: 640, decodedHeight: 360, }, }); expect(fetcher).toHaveBeenCalledWith( exactUrl, expect.objectContaining({ credentials: "omit", redirect: "error", mode: "cors", cache: "no-store", referrerPolicy: "no-referrer", }), ); } expect(close).toHaveBeenCalledTimes(samples.length); }); it("fails closed on duplicate/conflicting cache directives and oversized bodies", async () => { const png = pngBytes(640, 360); const createBitmap = vi.fn(async () => ({ width: 640, height: 360, close: vi.fn(), })); for (const cacheControl of [ // BT-IMG-02. Unmatched quotes must not be unwrapped into a bare number. 'public, max-age="31536000, immutable', 'public, max-age=31536000", immutable', 'public, max-age="31536000\\", immutable', "public, public, max-age=31536000, immutable", "public, max-age=31536000, s-maxage=60, immutable", "public, max-age=31536000, immutable, must-revalidate", "public=1, max-age=31536000, immutable", "public, max-age=31536000, immutable=true", ]) { const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(imageUrl, png, { status: 200, headers: { "cache-control": cacheControl, "content-type": "image/png", }, })) as typeof fetch, createBitmap, }); await expect(probe.probe(request())).resolves.toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); } const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(imageUrl, new Uint8Array(2_048), { status: 200, headers: { "cache-control": "public, max-age=31536000, immutable", "content-type": "image/png", }, })) as typeof fetch, createBitmap, }); await expect(probe.probe(request())).resolves.toMatchObject({ ok: false, error: { code: "LIMIT_EXCEEDED" }, }); expect(createBitmap).not.toHaveBeenCalled(); }); it("rejects non-identity content encoding and mismatched declared lengths", async () => { const png = pngBytes(640, 360); const createBitmap = vi.fn(); const cases = [ { headers: { "content-encoding": "gzip", "content-length": String(png.byteLength), }, code: "POLICY_REJECTED", }, { headers: { "content-length": String(png.byteLength + 1), }, code: "INTEGRITY_FAILED", }, ]; for (const invalid of cases) { const headers = publicImageHeaders("image/png"); for (const [name, value] of Object.entries(invalid.headers)) { headers.set(name, value); } const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(imageUrl, png, { status: 200, headers, })) as typeof fetch, createBitmap, }); await expect(probe.probe(request())).resolves.toMatchObject({ ok: false, error: { code: invalid.code }, }); } expect(createBitmap).not.toHaveBeenCalled(); }); it("rejects malicious dimensions and animated PNG/WebP before native decode", async () => { const createBitmap = vi.fn(async () => ({ width: 640, height: 360, close: vi.fn(), })); const cases = [ { bytes: pngBytes(20_000, 20_000), mediaType: "image/png" as const, code: "LIMIT_EXCEEDED", }, { bytes: pngBytes(320, 180), mediaType: "image/png" as const, code: "INTEGRITY_FAILED", }, { bytes: jpegBytes(320, 180), mediaType: "image/jpeg" as const, code: "INTEGRITY_FAILED", }, { bytes: webpBytes(10_000, 10_000), mediaType: "image/webp" as const, code: "LIMIT_EXCEEDED", }, { bytes: avifBytes(20_000, 20_000), mediaType: "image/avif" as const, code: "LIMIT_EXCEEDED", }, { bytes: pngBytes(640, 360, true), mediaType: "image/png" as const, code: "INTEGRITY_FAILED", }, { bytes: webpBytes(640, 360, true), mediaType: "image/webp" as const, code: "INTEGRITY_FAILED", }, { bytes: avifBytes(640, 360, "avis"), mediaType: "image/avif" as const, code: "INTEGRITY_FAILED", }, ]; for (const malicious of cases) { const url = imageUrl.replace( "format=png", `format=${malicious.mediaType.slice("image/".length)}`, ); const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(url, malicious.bytes, { status: 200, headers: publicImageHeaders( malicious.mediaType, malicious.bytes.byteLength, ), })) as typeof fetch, createBitmap, }); await expect( probe.probe( request({ absoluteUrl: url, expectedMediaType: malicious.mediaType, }), ), ).resolves.toMatchObject({ ok: false, error: { code: malicious.code }, }); } expect(createBitmap).not.toHaveBeenCalled(); }); it("enforces private no-store, omitted credentials and the exact final URL", async () => { const png = pngBytes(640, 360); const fetcher = vi.fn(async () => responseAt(imageUrl, png, { status: 200, headers: { // TR-RR-09. A private response carries `no-store` and nothing else // that describes cacheability. "cache-control": "no-store", "content-type": "image/png", }, }), ); const probe = createBrowserImageProbe({ fetcher: fetcher as typeof fetch, createBitmap: async () => ({ width: 640, height: 360, close: vi.fn(), }), }); await expect( probe.probe( request({ delivery: "PRIVATE_SIGNED", minimumPublicMaxAgeSeconds: 0, }), ), ).resolves.toMatchObject({ ok: true }); expect(fetcher).toHaveBeenCalledWith( imageUrl, expect.objectContaining({ cache: "no-store", credentials: "omit", redirect: "error", }), ); for (const response of [ responseAt(imageUrl, png, { status: 200, headers: { "cache-control": "private, no-store=value", "content-type": "image/png", }, }), responseAt(imageUrl, png, { status: 200, headers: { "cache-control": "public, no-store", "content-type": "image/png", }, }), responseAt( "https://images.example.test/v1/assets/other", png, { status: 200, headers: { "cache-control": "private, no-store", "content-type": "image/png", }, }, ), ]) { const rejectingProbe = createBrowserImageProbe({ fetcher: (async () => response) as typeof fetch, createBitmap: async () => ({ width: 640, height: 360, close: vi.fn(), }), }); await expect( rejectingProbe.probe( request({ delivery: "PRIVATE_SIGNED", minimumPublicMaxAgeSeconds: 0, }), ), ).resolves.toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); } await expect( probe.probe( request({ expectedMediaType: "image/svg+xml", } as unknown as Partial), ), ).resolves.toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); }); /** * TR-RR-09. The recorded BT-IMG-02 contract for a private response is a * fail-closed matrix. Accepting `no-store` next to a directive that describes * cacheability lets a self-contradictory policy read as acceptable. */ it("applies the full private Cache-Control matrix", async () => { const png = pngBytes(640, 360); const probeWith = async (cacheControl: string) => { const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(imageUrl, png, { status: 200, headers: { "cache-control": cacheControl, "content-type": "image/png", }, })) as typeof fetch, createBitmap: async () => ({ width: 640, height: 360, close: vi.fn(), }), }); return await probe.probe( request({ delivery: "PRIVATE_SIGNED", minimumPublicMaxAgeSeconds: 0, }), ); }; // Only `no-store`, plus a syntactically valid unknown extension. expect(await probeWith("no-store")).toMatchObject({ ok: true }); expect(await probeWith('no-store, x-vendor="a,b"')).toMatchObject({ ok: true, }); for (const companion of [ "public", "private", "immutable", "max-age=60", "s-maxage=60", "no-cache", "must-revalidate", "proxy-revalidate", ]) { expect(await probeWith(`no-store, ${companion}`)).toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); } for (const withoutNoStore of ["private", "no-cache", "max-age=0"]) { expect(await probeWith(withoutNoStore)).toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); } }); it("times out a stalled body, aborts the composed signal and cancels its reader", async () => { const manual = manualImageProbeScheduler(); const cancel = vi.fn(async () => undefined); const releaseLock = vi.fn(); const read = vi.fn( () => new Promise>( () => undefined, ), ); const response = { body: { getReader: () => ({ cancel, read, releaseLock }), }, headers: publicImageHeaders("image/png"), ok: true, redirected: false, status: 200, type: "cors", url: imageUrl, } as unknown as Response; const fetcher = vi.fn( async (_input: RequestInfo | URL, _init?: RequestInit) => response, ); const probe = createBrowserImageProbe({ fetcher: fetcher as typeof fetch, createBitmap: vi.fn(), timeoutMs: 1_000, scheduler: manual.scheduler, }); const probeRequest = request(); const pending = probe.probe(probeRequest); await vi.waitFor(() => { expect(read).toHaveBeenCalledOnce(); }); const composedSignal = fetcher.mock.calls[0]?.[1]?.signal as | AbortSignal | null | undefined; expect(composedSignal).not.toBe(probeRequest.signal); manual.fire(); await expect(pending).resolves.toMatchObject({ ok: false, error: { code: "UNAVAILABLE", retryable: true, recovery: "RETRY", }, }); expect(cancel).toHaveBeenCalledOnce(); expect(releaseLock).toHaveBeenCalledOnce(); expect(composedSignal?.aborted).toBe(true); }); it("times out stalled decode and closes a bitmap that resolves late", async () => { const manual = manualImageProbeScheduler(); const close = vi.fn(); let finishDecode: ((bitmap: { width: number; height: number; close(): void; }) => void) | undefined; const createBitmap = vi.fn( () => new Promise<{ width: number; height: number; close(): void; }>((resolve) => { finishDecode = resolve; }), ); const png = pngBytes(640, 360); const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(imageUrl, png, { status: 200, headers: publicImageHeaders("image/png", png.byteLength), })) as typeof fetch, createBitmap, timeoutMs: 1_000, scheduler: manual.scheduler, }); const pending = probe.probe(request()); await vi.waitFor(() => { expect(createBitmap).toHaveBeenCalledOnce(); }); manual.fire(); await expect(pending).resolves.toMatchObject({ ok: false, error: { code: "UNAVAILABLE" }, }); finishDecode?.({ width: 640, height: 360, close }); await vi.waitFor(() => { expect(close).toHaveBeenCalledOnce(); }); }); /** * X-AUDIT-02. `probe()` promises a `BrowserDataResult`. A scheduler that * cannot install the probe deadline must close the probe inside that contract * rather than rejecting it, and must not leave the caller's listener behind. */ describe("scheduler boundary", () => { const trackedSignal = () => { const controller = new AbortController(); const added: string[] = []; const removed: string[] = []; const add = controller.signal.addEventListener.bind(controller.signal); const remove = controller.signal.removeEventListener.bind( controller.signal, ); Object.defineProperty(controller.signal, "addEventListener", { configurable: true, value: (type: string, ...rest: readonly unknown[]) => { added.push(type); return (add as (...args: readonly unknown[]) => unknown)( type, ...rest, ); }, }); Object.defineProperty(controller.signal, "removeEventListener", { configurable: true, value: (type: string, ...rest: readonly unknown[]) => { removed.push(type); return (remove as (...args: readonly unknown[]) => unknown)( type, ...rest, ); }, }); return { controller, added, removed }; }; it("closes the probe when the scheduler cannot install the deadline", async () => { const fetcher = vi.fn(async () => new Response(null, { status: 200 })); const { controller, added, removed } = trackedSignal(); const probe = createBrowserImageProbe({ fetcher: fetcher as unknown as typeof fetch, createBitmap: vi.fn(), timeoutMs: 1_000, scheduler: { setTimeout: () => { throw new TypeError("image scheduler install exploded"); }, clearTimeout: vi.fn(), }, }); await expect( probe.probe({ ...request(), signal: controller.signal }), ).resolves.toMatchObject({ ok: false, error: { code: "UNAVAILABLE", retryable: true, recovery: "RETRY" }, }); expect(fetcher).not.toHaveBeenCalled(); expect(added.filter((type) => type === "abort")).toHaveLength(1); expect(removed.filter((type) => type === "abort")).toHaveLength(1); }); it("starts no timer and no fetch for an already aborted caller", async () => { const fetcher = vi.fn(async () => new Response(null, { status: 200 })); const setTimeout_ = vi.fn(() => 1); const controller = new AbortController(); controller.abort(); const probe = createBrowserImageProbe({ fetcher: fetcher as unknown as typeof fetch, createBitmap: vi.fn(), timeoutMs: 1_000, scheduler: { setTimeout: setTimeout_, clearTimeout: vi.fn() }, }); await expect( probe.probe({ ...request(), signal: controller.signal }), ).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" } }); expect(fetcher).not.toHaveBeenCalled(); expect(setTimeout_).not.toHaveBeenCalled(); }); it("keeps the classified outcome when clearing the deadline throws", async () => { const png = pngBytes(640, 360); const probe = createBrowserImageProbe({ fetcher: (async () => responseAt(imageUrl, png, { status: 200, headers: publicImageHeaders("image/png", png.byteLength), })) as typeof fetch, createBitmap: vi.fn(async () => ({ width: 640, height: 360, close: vi.fn(), })), timeoutMs: 1_000, scheduler: { setTimeout: (callback: () => void, milliseconds: number) => setTimeout(callback, milliseconds), clearTimeout: () => { throw new TypeError("image scheduler clear exploded"); }, }, }); await expect(probe.probe(request())).resolves.toMatchObject({ ok: true }); }); }); });