import { createHmac } from "node:crypto"; import { afterEach, describe, expect, it, vi } from "vitest"; import worker from "./index.js"; import { avatarWidths } from "../../web/src/lib/avatarWidths.ts"; import { widthFor } from "./size.js"; const env = { AVATAR_SHARED_SECRET: "s3cret", BOBBIN_URL: "https://bobbin.test", }; const did = "did:plc:alice"; const signed = (query) => `https://avatar.test/${createHmac("sha256", "s3cret").update(did).digest("hex")}/${did}${query}`; const upstream = ( blobType, blob = () => new Response("img", { headers: { "Content-Type": blobType } }), ) => vi.fn(async (url) => { if (url.includes("resolveMiniDoc")) return Response.json({ did, pds: "https://pds.test" }); if (url.includes("getRecord")) return Response.json({ value: { avatar: { ref: { $link: "bafyavatar" } } }, }); return blob(); }); // a tangled profile whose avatar blob no pds has, next to a bluesky profile const blueskyFallback = ({ tangledAvatar = true, bluesky = true } = {}) => vi.fn(async (url) => { if (url.includes("resolveMiniDoc")) return Response.json({ did, pds: "https://pds.test" }); if (url.includes("getRecord")) return Response.json({ value: tangledAvatar ? { avatar: { ref: { $link: "bafygone" } } } : {}, }); if (url.includes("app.bsky.actor.getProfile")) return Response.json( bluesky ? { avatar: "https://cdn.bsky.test/avatar.jpg" } : {}, ); if (url.includes("cdn.bsky.test")) return new Response("bsky", { headers: { "Content-Type": "image/jpeg" } }); return new Response("Blob not found", { status: 400 }); }); const serve = async (query, fetch, put = async () => {}) => { vi.stubGlobal("fetch", fetch); vi.stubGlobal("caches", { default: { match: async () => undefined, put } }); return worker.fetch(new Request(signed(query)), env); }; afterEach(() => vi.unstubAllGlobals()); describe("widthFor", () => { it.each(avatarWidths)("resizes to %i", (width) => { expect(widthFor(String(width))).toBe(width); }); it.each([null, "", "100", "abc"])( "leaves %s at the original size", (size) => { expect(widthFor(size)).toBeNull(); }, ); }); describe("fetch", () => { it("asks cloudflare to resize every width the web app uses", async () => { const fetch = upstream("image/jpeg"); const res = await serve("?size=192", fetch); expect(fetch.mock.calls.at(-1)[1]).toEqual({ cf: { image: { format: "webp", width: 192, height: 192, fit: "cover" } }, }); expect(res.headers.get("Content-Type")).toBe("image/webp"); }); it("keeps the blob's type when it serves the original", async () => { const fetch = upstream("image/jpeg"); const res = await serve("", fetch); expect(fetch.mock.calls.at(-1)[1]).toEqual({}); expect(res.headers.get("Content-Type")).toBe("image/jpeg"); }); it("returns 415 when PDS presents a given blob as SVG", async () => { const res = await serve("", upstream("image/svg+xml")); expect(res.status).toBe(415); }); it("serves raster yes, but with nosniff and sandbox", async () => { const res = await serve("", upstream("image/png")); expect(res.headers.get("Content-Type")).toBe("image/png"); expect(res.headers.get("X-Content-Type-Options")).toBe("nosniff"); expect(res.headers.get("Content-Security-Policy")).toContain("sandbox"); }); describe("when the blob can't be fetched", () => { it.each([ ["is gone", () => new Response("", { status: 400 }), 400, 3600], ["is not found", () => new Response("", { status: 404 }), 404, 3600], ["is rate limited", () => new Response("", { status: 429 }), 429, 120], ["has a broken host", () => new Response("", { status: 502 }), 502, 120], [ "has an unreachable host", () => { throw new TypeError("network lost"); }, 502, 120, ], ])("caches the error when the blob %s", async (_, blob, status, ttl) => { const put = vi.fn(async () => {}); const res = await serve("?size=64", upstream("image/png", blob), put); expect(res.status).toBe(status); expect(res.headers.get("Cache-Control")).toBe(`public, max-age=${ttl}`); expect(put).toHaveBeenCalledOnce(); }); }); describe("bluesky fallback", () => { it("shows the bluesky avatar when the tangled blob is gone", async () => { const fetch = blueskyFallback(); const res = await serve("?size=64", fetch); expect(res.status).toBe(200); expect(await res.text()).toBe("bsky"); expect(fetch.mock.calls.at(-1)).toEqual([ "https://cdn.bsky.test/avatar.jpg", { cf: { image: { format: "webp", width: 64, height: 64, fit: "cover" } } }, ]); }); it("shows the bluesky avatar when the tangled profile has none", async () => { const res = await serve("", blueskyFallback({ tangledAvatar: false })); expect(await res.text()).toBe("bsky"); }); it("draws the gradient when neither has an avatar", async () => { const res = await serve( "", blueskyFallback({ tangledAvatar: false, bluesky: false }), ); expect(res.headers.get("Content-Type")).toBe("image/svg+xml"); }); it("keeps the tangled error when bluesky has no avatar either", async () => { const res = await serve("", blueskyFallback({ bluesky: false })); expect(res.status).toBe(400); }); it("doesn't swap in bluesky while the tangled pds is rate limited", async () => { const fetch = upstream("image/png", () => new Response("", { status: 429 })); const res = await serve("", fetch); expect(res.status).toBe(429); expect(fetch.mock.calls.some(([url]) => url.includes("bsky"))).toBe(false); }); }); });