1import { describe, expect, it } from "@effect/vitest";
2
3import { faviconUrlForOrigin, toolActivityFaviconUrl } from "./favicon.ts";
4
5describe("faviconUrlForOrigin", () => {
6 it.each([
7 "http://192.168.1.10:8080",
8 "http://localhost:3000",
9 "http://home.arpa",
10 "https://printer.local.",
11 "https://api.internal",
12 "https://box.tailnet.ts.net",
13 "http://127.1",
14 "http://0x7f000001",
15 "http://[::]",
16 "http://[::1]",
17 "http://[::ffff:192.168.1.10]",
18 "http://[fd00::1]",
19 "http://[fe80::1]",
20 "http://100.64.0.1",
21 "http://198.51.100.1",
22 "http://[2001:db8::1]",
23 "http://service.test",
24 "http://private.onion",
25 "http://127.1..",
26 "https://grafana.corp",
27 "https://build.lan",
28 "https://wiki.intranet",
29 "https://grafana.internal.acme-corp.com",
30 "https://jira.corp.acme-corp.com:8443",
31 ])("does not disclose %s to the favicon provider", (origin) => {
32 expect(faviconUrlForOrigin(origin)).toBeNull();
33 });
34
35 it("sends only the public hostname and requested size, never the port", () => {
36 expect(faviconUrlForOrigin("https://github.com:8443/pingdotgg/t3code?private=query", 64)).toBe(
37 "https://www.google.com/s2/favicons?domain=github.com&sz=64",
38 );
39 });
40
41 it.each([null, undefined, "", "invalid URL", "file:///tmp/private", "data:text/plain,private"])(
42 "rejects an invalid or unsupported origin %s",
43 (origin) => {
44 expect(faviconUrlForOrigin(origin)).toBeNull();
45 },
46 );
47});
48
49describe("toolActivityFaviconUrl", () => {
50 it("uses the page origin instead of a third-party favicon service", () => {
51 expect(toolActivityFaviconUrl({ pageUrl: "https://example.com/docs/page?q=1" }, "light")).toBe(
52 "https://example.com/favicon.ico",
53 );
54 expect(toolActivityFaviconUrl({ pageUrl: "http://localhost:5173/app" }, "light")).toBe(
55 "http://localhost:5173/favicon.ico",
56 );
57 });
58
59 it("selects site-owned light and dark variants without filtering full-color icons", () => {
60 expect(toolActivityFaviconUrl({ pageUrl: "https://github.com/openai/codex" }, "light")).toBe(
61 "https://github.githubassets.com/favicons/favicon.svg",
62 );
63 expect(toolActivityFaviconUrl({ pageUrl: "https://github.com/openai/codex" }, "dark")).toBe(
64 "https://github.githubassets.com/favicons/favicon-dark.svg",
65 );
66
67 const fullColorIcon = {
68 pageUrl: "https://example.com/docs",
69 faviconUrl: "https://cdn.example.com/full-color.png",
70 };
71 expect(toolActivityFaviconUrl(fullColorIcon, "light")).toBe(fullColorIcon.faviconUrl);
72 expect(toolActivityFaviconUrl(fullColorIcon, "dark")).toBe(fullColorIcon.faviconUrl);
73 });
74
75 it("prefers a provider-supplied dark favicon", () => {
76 expect(
77 toolActivityFaviconUrl(
78 {
79 pageUrl: "https://example.com/docs",
80 faviconUrl: "https://example.com/light.svg",
81 faviconUrlDark: "https://example.com/dark.svg",
82 },
83 "dark",
84 ),
85 ).toBe("https://example.com/dark.svg");
86 });
87
88 it("accepts provider-supplied image URLs but rejects extension URLs", () => {
89 expect(
90 toolActivityFaviconUrl(
91 { pageUrl: "https://example.com/docs", faviconUrl: "https://example.com/icon.png" },
92 "light",
93 ),
94 ).toBe("https://example.com/icon.png");
95 expect(
96 toolActivityFaviconUrl(
97 { pageUrl: "https://example.com/docs", faviconUrl: "chrome-extension://example/_favicon/" },
98 "light",
99 ),
100 ).toBe("https://example.com/favicon.ico");
101 });
102});