Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162import { afterEach, describe, expect, it, vi } from "vitest";import { didOf } from "#lib/api/syntax.ts";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import oauthMetadata from "#lib/oauth-client-metadata.ts";import { missingPermissions } from "#lib/auth/scopes.ts";
vi.mock("#lib/auth/agent.ts", () => ({ createClient: () => ({}), mintServiceAuth: async () => "token", serviceDidForHost: (host: string) => `did:web:${host}`, hostForServiceDid: () => null}));
vi.mock("#lib/api/knot.ts", () => ({ knotCapabilities: async () => null}));
const { createWebhook, deleteWebhook, isWebhookUrl, listWebhookDeliveries, listWebhooks, retryWebhookDelivery, selectedWebhookEvents, updateWebhook, webhookActivation, webhookEventSelection, webhookIsActive, webhookPermissions} = await import("#lib/api/webhooks.ts");
const agent = { sub: "did:plc:nel" } as unknown as OAuthUserAgent;const spindle = new URL("https://spindle.test");const target = { repoDid: didOf("did:plc:repo") };
const respond = (...bodies: unknown[]) => { let request = 0; const fetch = vi.fn<typeof globalThis.fetch>(async () => { const body = bodies[request++] ?? {}; return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); }); vi.stubGlobal("fetch", fetch); return fetch;};
const requestUrl = (fetch: ReturnType<typeof respond>, index = 0) => new URL(String(fetch.mock.calls[index][0]));const requestBody = (fetch: ReturnType<typeof respond>, index = 0) => JSON.parse(String(fetch.mock.calls[index][1]?.body));
afterEach(() => vi.unstubAllGlobals());
describe("webhook settings", () => { it("lists hooks and deliveries from the repository spindle", async () => { const hook = { id: "7", url: "https://hooks.test/tangled", events: ["push"], active: true, createdAt: "2026-09-20T00:00:00Z" }; const delivery = { id: "attempt", webhook: "7", event: "push", url: hook.url, success: true, createdAt: hook.createdAt }; const fetch = respond( { webhooks: [hook] }, // unpadded base64, as spindle encodes lexicon bytes { deliveries: [{ ...delivery, requestBody: { $bytes: "eyJvayI6dHJ1ZX0" } }] } );
await expect(listWebhooks(agent, spindle, target)).resolves.toEqual([hook]); await expect( listWebhookDeliveries(agent, spindle, target, ["7", "8"], 25) ).resolves.toEqual([{ ...delivery, requestBody: '{"ok":true}', responseBody: undefined }]);
const listed = requestUrl(fetch); expect(listed.pathname).toBe("/xrpc/org.tangled.webhook.listWebhooks"); expect(listed.searchParams.get("repo")).toBe(target.repoDid); const deliveries = requestUrl(fetch, 1); expect(deliveries.pathname).toBe("/xrpc/org.tangled.webhook.listWebhookDeliveries"); expect(deliveries.searchParams.getAll("webhooks")).toEqual(["7", "8"]); expect(deliveries.searchParams.get("limit")).toBe("25"); });
it("creates, updates, retries, and deletes through spindle xrpc", async () => { const fetch = respond({ id: "7" }, {}, {}, {});
await expect( createWebhook(agent, spindle, target, { url: "https://hooks.test/tangled", events: ["push"], secret: "shared", active: true }) ).resolves.toBe("7"); await updateWebhook(agent, spindle, target, "7", { url: "https://hooks.test/renamed", events: ["repository:renamed"], secret: "" }); await retryWebhookDelivery(agent, spindle, target, "7", "attempt"); await deleteWebhook(agent, spindle, target, "7");
expect(fetch.mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual([ "/xrpc/org.tangled.webhook.createWebhook", "/xrpc/org.tangled.webhook.updateWebhook", "/xrpc/org.tangled.webhook.retryWebhookDelivery", "/xrpc/org.tangled.webhook.deleteWebhook" ]); expect(requestBody(fetch)).toEqual({ repo: target.repoDid, url: "https://hooks.test/tangled", events: ["push"], secret: "shared", active: true }); expect(requestBody(fetch, 1)).toEqual({ repo: target.repoDid, webhook: "7", url: "https://hooks.test/renamed", events: ["repository:renamed"], secret: "" }); expect(requestBody(fetch, 2)).toEqual({ repo: target.repoDid, webhook: "7", delivery: "attempt" }); });
it("maps event selections, permissions, and accepts only HTTP webhook URLs", () => { const selection = webhookEventSelection(["push", "pull_request:merged", "unknown"]); expect(selectedWebhookEvents(selection)).toEqual(["push", "pull_request:merged"]); expect(webhookActivation(true)).toBe("active"); expect(webhookActivation(false)).toBe("inactive"); expect(webhookIsActive("active")).toBe(true); expect(webhookIsActive("inactive")).toBe(false); expect(isWebhookUrl(" https://hooks.test/tangled ")).toBe(true); expect(isWebhookUrl("http://hooks.test/tangled")).toBe(true); expect(isWebhookUrl("ftp://hooks.test/tangled")).toBe(false); expect(isWebhookUrl("not a url")).toBe(false); const permissions = webhookPermissions(spindle); expect(permissions).toHaveLength(6); expect(missingPermissions("atproto", permissions)).toEqual(permissions); expect(missingPermissions(oauthMetadata.scope, permissions)).toEqual([]); expect(permissions).toContainEqual({ resource: "rpc", lxm: "org.tangled.webhook.listWebhooks", aud: "did:web:spindle.test" }); });});