~james/core

core/avatar/src/index.test.js -rw-r--r-- 5.7 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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);
    });
  });
});