Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-gatekeeper/__tests__/sidebar.test.ts

Snapshot at 34e3175

Containing directory

7,033 bytes · typescriptView raw
1import { env, RpcTarget } from "cloudflare:workers";
2import { describe, expect, it, vi } from "vitest";
3import { sidebarAccountDescription, type WorkerUpdateReceiver } from "../src/worker/sidebar.ts";
4import type { SidebarBatch } from "../src/ui-types.ts";
5import type { FixtureHarness } from "./worker.ts";
6import brand from "../../../brand.json";
7import { markIconUrl } from "../../forkhorse-dashboard/src/mark.ts";
8import { railIconUrl } from "../src/rail-icon.ts";
9
10declare global {
11  namespace Cloudflare {
12    interface Env {
13      FIXTURE: Service<FixtureHarness>;
14    }
15  }
16}
17
18class Receiver extends RpcTarget implements WorkerUpdateReceiver {
19  readonly batches: SidebarBatch[] = [];
20  async receive(batch: SidebarBatch): Promise<void> {
21    this.batches.push(batch);
22  }
23}
24
25async function refuses(operation: () => PromiseLike<unknown>, message = ""): Promise<void> {
26  try {
27    await operation();
28  } catch (error) {
29    expect(error instanceof Error ? error.message : String(error)).toContain(message);
30    return;
31  }
32  throw new Error("Expected RPC refusal.");
33}
34
35describe("Worker-native sidebar capability (local fixtures)", () => {
36  it("checks a configurator draft through native RPC without echoing its credential or granting a connection", async () => {
37    using draft = await env.FIXTURE.configurator();
38    const result = await draft.checkDraft({
39      coreOrigin: "https://core.fixture.idiot.tax",
40      project: "fixture",
41      agentCredential: "fixture-only-not-a-real-credential",
42    });
43    expect(result.connectionAvailable).toBe(false);
44    expect(JSON.stringify(result)).not.toContain("fixture-only-not-a-real-credential");
45    await refuses(
46      () =>
47        draft.checkDraft({
48          coreOrigin: "http://127.0.0.1",
49          project: "fixture",
50          agentCredential: "fixture-only-not-a-real-credential",
51        }),
52      "HTTPS",
53    );
54  });
55  it("runs under workerd and keeps the emoji avatar separate from the regular Horse rail icon", async () => {
56    expect(navigator.userAgent).toBe("Cloudflare-Workers");
57    expect(sidebarAccountDescription()).toMatchObject({
58      displayName: brand.name,
59      providesUi: { title: brand.name },
60    });
61    expect(sidebarAccountDescription().singleton).toBeUndefined();
62    const icon = { url: markIconUrl(brand.mark) };
63    const description = sidebarAccountDescription();
64    expect(description.avatar).toEqual(icon);
65    expect(description.providesUi!.icon).toEqual({ url: railIconUrl(brand.railIcon) });
66    expect(description.providesUi!.icon).not.toEqual(icon);
67    const svg = decodeURIComponent(description.providesUi!.icon!.url.split(",").slice(1).join(","));
68    expect(svg).toContain('viewBox="0 0 256 256" fill="currentColor"');
69    expect(svg.match(/<path /g)).toHaveLength(1);
70    const path = /<path d="([^"]+)"/.exec(svg)![1]!;
71    // Independently recorded from Phosphor 2.1.10's regular definition, not another weight.
72    const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(path));
73    expect(Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join(""))
74      .toBe("0003357977cd1a2ac5cf9cf1606a93830a72be0069dcb9434428c18e3c8a1232");
75  });
76
77  it("authorizes before returning a capability", async () => {
78    expect(await env.FIXTURE.deniedSetup()).toMatchObject({ opened: false, reads: 0 });
79  });
80
81  it("releasing the UI capability stops its producer", async () => {
82    using session = await env.FIXTURE.session();
83    {
84      using ui = await session.open();
85      await ui.snapshot();
86    }
87    await vi.waitFor(async () => expect(await session.stopped()).toBe(1));
88  });
89
90  it("retains push callbacks across RPC and closes subscriptions", async () => {
91    using session = await env.FIXTURE.session();
92    using ui = await session.open();
93    expect((await ui.snapshot()).state.project.id).toBe("fixture");
94    const receiver = new Receiver();
95    using subscription = await ui.subscribe(receiver);
96    await vi.waitFor(() => expect(receiver.batches).toHaveLength(1));
97    await session.publish();
98    await vi.waitFor(() => expect(receiver.batches.at(-1)?.version).toBe(1));
99    expect(receiver.batches.at(-1)?.state.packets).toHaveLength(1);
100    await subscription.close();
101    await subscription.close();
102    await session.publish();
103    await vi.waitFor(async () => expect((await ui.snapshot()).version).toBe(2));
104    expect(receiver.batches).toHaveLength(2);
105  });
106
107  it("bounds pending polls, wakes on a batch, and rejects forged cursors", async () => {
108    using session = await env.FIXTURE.session();
109    using ui = await session.open();
110    const pending = ui.waitForChanges(0);
111    await refuses(() => ui.waitForChanges(0), "one pending poll");
112    await session.publish();
113    expect((await pending).version).toBe(1);
114    await refuses(() => ui.waitForChanges(-1), "Invalid UI version");
115    await refuses(() => ui.waitForChanges(99), "Invalid UI version");
116  });
117
118  it("revocation stops the producer and rejects already-waiting reads", async () => {
119    using session = await env.FIXTURE.session();
120    using ui = await session.open();
121    const pending = ui.waitForChanges(0);
122    const rejected = refuses(() => pending);
123    await session.revoke();
124    await rejected;
125    await refuses(() => ui.snapshot(), "unavailable");
126    expect(await session.stopped()).toBe(1);
127  });
128
129  it("rechecks authorization before a callback even without a revocation notification", async () => {
130    using session = await env.FIXTURE.session();
131    using ui = await session.open();
132    const receiver = new Receiver();
133    using _subscription = await ui.subscribe(receiver);
134    await vi.waitFor(() => expect(receiver.batches).toHaveLength(1));
135    await session.revoke(false);
136    await session.publish();
137    await vi.waitFor(async () => expect(await session.stopped()).toBe(1));
138    expect(receiver.batches).toHaveLength(1);
139    await refuses(() => ui.snapshot(), "unavailable");
140  });
141
142  it("closes instead of leaking a foreign project emitted by its producer", async () => {
143    using session = await env.FIXTURE.session();
144    using ui = await session.open();
145    await session.foreign();
146    await refuses(() => ui.snapshot(), "unavailable");
147    expect(await session.stopped()).toBe(1);
148  });
149});
150
151it("replaces and releases native subscriptions after reconnect without leaking callbacks", async () => {
152  using session = await env.FIXTURE.session();
153  using ui = await session.open();
154  const first = new Receiver();
155  using old = await ui.subscribe(first);
156  await vi.waitFor(() => expect(first.batches.length).toBe(1));
157  await old.close();
158  const second = new Receiver();
159  using replacement = await ui.subscribe(second);
160  await session.publish();
161  await vi.waitFor(() => expect(second.batches.at(-1)?.version).toBe(1));
162  expect(first.batches.length).toBe(1);
163  await replacement.close();
164  await session.publish();
165  await vi.waitFor(async () => expect((await ui.snapshot()).version).toBe(2));
166  expect(second.batches.at(-1)?.version).toBe(1);
167});
168