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);
});
});
});