Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259import { describe, expect, it, vi } from "vitest";import oauthMetadata from "$lib/oauth-client-metadata";import { missingPermissions } from "$lib/auth/scopes";
vi.mock("$lib/auth/agent", () => ({ createClient: () => ({}), mintServiceAuth: async () => "token", serviceDidForHost: (host: string) => `did:web:${host}`, hostForServiceDid: () => null}));
const { sitesPermissions, getDeployHistory, loadSitePanel, branchChoices } = await import("$lib/api/sites");const { createAppviewClient } = await import("$lib/api/appview");type AppviewContext = import("$lib/api/appview").AppviewContext;
const sites = new URL("https://sites.test");
const agent = { sub: "did:plc:alice" } as unknown as AppviewContext["agent"];
interface Reply { status?: number; body?: unknown;}
const ctxFor = (reply: (url: URL) => Reply, seen: URL[]): AppviewContext => createAppviewClient({ apiUrl: "https://sites.test", agent, fetch: async (input) => { const url = input as URL; seen.push(url); const { status = 200, body = {} } = reply(url); return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } });
const nsidOf = (url: URL) => url.pathname.replace("/xrpc/", "");const called = (seen: URL[]) => seen.map(nsidOf);
const routes = (table: Record<string, Reply>) => (url: URL): Reply => table[nsidOf(url)] ?? { body: {} };
const DOMAIN_CLAIM = "org.tangled.temp.site.getDomainClaim";const SITE_CONFIG = "org.tangled.temp.repo.getSiteConfig";const DEPLOY_HISTORY = "org.tangled.temp.repo.getDeployHistory";
const NO_SITE = { status: 404, body: { error: "SiteNotFound", message: "no site configuration for this repository" }};
const config = { branch: "site", dir: "/docs", isIndex: false };
const entry = { status: "failure", trigger: "push", branch: "main", dir: "/", commitSha: "0c4d0e9b", error: "knot unreachable", createdAt: "2026-07-01T10:00:00Z"};
describe("sites permissions", () => { it("covers every method the repo sites panel calls, the deploy history included", () => { const permissions = sitesPermissions(sites); expect(permissions).toHaveLength(5); expect(missingPermissions("atproto", permissions)).toEqual(permissions); expect(missingPermissions(oauthMetadata.scope, permissions)).toEqual([]); expect(permissions).toContainEqual({ resource: "rpc", lxm: "org.tangled.temp.repo.deploySite", aud: "did:web:sites.test" }); expect(permissions).toContainEqual({ resource: "rpc", lxm: "org.tangled.temp.repo.getDeployHistory", aud: "did:web:sites.test" }); });});
describe("getDeployHistory", () => { it("asks sites for the repo's deploys", async () => { const seen: URL[] = []; const ctx = ctxFor(() => ({ body: { deploys: [entry] } }), seen); const history = await getDeployHistory(ctx, "did:plc:repo"); expect(history.deploys).toEqual([entry]); expect(seen[0].pathname).toBe("/xrpc/org.tangled.temp.repo.getDeployHistory"); expect(seen[0].searchParams.get("repoDid")).toBe("did:plc:repo"); });
it("reads a payload without the deploys key as an empty history", async () => { expect( await getDeployHistory( ctxFor(() => ({}), []), "did:plc:repo" ) ).toEqual({ deploys: [] }); });
it("still refuses a history read for a repo with no site config", async () => { const ctx = ctxFor(() => NO_SITE, []); await expect(getDeployHistory(ctx, "did:plc:repo")).rejects.toMatchObject({ status: 404, error: "SiteNotFound" }); });});
describe("loadSitePanel", () => { it("reads the domain, config and deploys of a configured site", async () => { const seen: URL[] = []; const ctx = ctxFor( routes({ [DOMAIN_CLAIM]: { body: { domain: "alice.sites.test" } }, [SITE_CONFIG]: { body: { config } }, [DEPLOY_HISTORY]: { body: { deploys: [entry] } } }), seen );
await expect(loadSitePanel(ctx, "did:plc:repo")).resolves.toEqual({ domain: "alice.sites.test", config, deploys: [entry] }); expect(called(seen).sort()).toEqual([DOMAIN_CLAIM, SITE_CONFIG, DEPLOY_HISTORY].sort()); });
it("loads an unconfigured repo without asking for a history it cannot have", async () => { const seen: URL[] = []; const ctx = ctxFor( routes({ [DOMAIN_CLAIM]: { body: { domain: "alice.sites.test" } }, [SITE_CONFIG]: { body: {} }, [DEPLOY_HISTORY]: NO_SITE }), seen );
await expect(loadSitePanel(ctx, "did:plc:repo")).resolves.toEqual({ domain: "alice.sites.test", config: null, deploys: [] }); expect(called(seen)).not.toContain(DEPLOY_HISTORY); });
it("loads for an account with no domain claim", async () => { const ctx = ctxFor(routes({ [SITE_CONFIG]: { body: {} } }), []); await expect(loadSitePanel(ctx, "did:plc:repo")).resolves.toEqual({ domain: null, config: null, deploys: [] }); });
it("loads a configured site whose domain claim lapsed, without asking for its history", async () => { const seen: URL[] = []; const ctx = ctxFor( routes({ [SITE_CONFIG]: { body: { config } }, [DEPLOY_HISTORY]: NO_SITE }), seen );
await expect(loadSitePanel(ctx, "did:plc:repo")).resolves.toEqual({ domain: null, config, deploys: [] }); expect(called(seen)).not.toContain(DEPLOY_HISTORY); });
it("leaves a failed config read on the panel", async () => { const ctx = ctxFor( routes({ [SITE_CONFIG]: { status: 500, body: { error: "InternalServerError", message: "boom" } } }), [] ); await expect(loadSitePanel(ctx, "did:plc:repo")).rejects.toMatchObject({ status: 500 }); });
it("keeps the owner refusal of a configured site's history", async () => { const ctx = ctxFor( routes({ [DOMAIN_CLAIM]: { body: { domain: "alice.sites.test" } }, [SITE_CONFIG]: { body: { config } }, [DEPLOY_HISTORY]: { status: 403, body: { error: "NotOwner", message: "Only the repository owner can read this site's deploy history." } } }), [] ); await expect(loadSitePanel(ctx, "did:plc:repo")).rejects.toMatchObject({ status: 403, error: "NotOwner" }); });
it("leaves a failed domain read on the panel", async () => { const ctx = ctxFor( routes({ [DOMAIN_CLAIM]: { status: 502, body: { error: "UpstreamFailed", message: "no pds" } } }), [] ); await expect(loadSitePanel(ctx, "did:plc:repo")).rejects.toMatchObject({ status: 502 }); });});
describe("branchChoices", () => { it("offers every branch the repo lists", () => { expect(branchChoices("main", ["main", "preview", "docs"])).toEqual([ { value: "main" }, { value: "preview" }, { value: "docs" } ]); });
it("keeps a branch the repo no longer lists at the top, without repeating it", () => { expect(branchChoices("docs", ["main", "docs"])).toEqual([ { value: "main" }, { value: "docs" } ]); expect(branchChoices("gh-pages", ["main", "docs"])).toEqual([ { value: "gh-pages" }, { value: "main" }, { value: "docs" } ]); });
it("offers nothing when neither the config nor the knot named a branch", () => { expect(branchChoices("", [])).toEqual([]); expect(branchChoices("", ["main"])).toEqual([{ value: "main" }]); });});