Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185import type { Did } from "@atcute/lexicons/syntax";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { beforeEach, describe, expect, it, vi } from "vitest";import { getOrCreateDelegatedSession } from "$lib/api/actAs";import type { BobbinContext } from "$lib/api/client";import { REPO_COUNT } from "$lib/api/descriptors";import { enrich, TYPE_COUNT } from "$lib/api/enrich";import { leaveOrganization, listUserOrganizations, roleForScopes } from "$lib/api/organizations";import { listControlledAccounts, removeController } from "$lib/api/tranquil";import { forgetOrgDid } from "$lib/auth/accounts";
vi.mock("$lib/api/actAs", () => ({ getOrCreateDelegatedSession: vi.fn() }));vi.mock("$lib/api/tranquil", () => ({ listControlledAccounts: vi.fn(), removeController: vi.fn()}));vi.mock("$lib/auth/accounts", () => ({ forgetOrgDid: vi.fn() }));vi.mock(import("$lib/api/enrich"), async (importOriginal) => ({ ...(await importOriginal()), enrich: vi.fn()}));
const OWNER_SCOPES = "atproto repo:* blob:*/* rpc:* identity:* account:*?action=manage transition:generic";const EDITOR_SCOPES = "atproto repo:*?action=create blob:*/* rpc:*";
const controllerDid = "did:plc:controller" as Did;const controller = { sub: controllerDid } as OAuthUserAgent;const ctx = {} as BobbinContext;const tangledDid = "did:plc:tangled" as Did;const microcosmDid = "did:plc:microcosm" as Did;
const profileUri = (did: Did) => `at://${did}/sh.tangled.actor.profile/self`;
const countFor = (did: Did, count: number) => ({ [did]: { [REPO_COUNT.source]: { [TYPE_COUNT]: count } }});
beforeEach(() => { vi.resetAllMocks(); vi.mocked(listControlledAccounts).mockResolvedValue([ { did: tangledDid, handle: "tangled.org", grantedAt: "2025-01-14T09:00:00.000Z", grantedScopes: OWNER_SCOPES }, { did: microcosmDid, handle: "microcosm.blue", grantedAt: "2025-03-02T09:00:00.000Z", grantedScopes: EDITOR_SCOPES } ]); vi.mocked(enrich).mockResolvedValue({ output: { items: [ { uri: profileUri(tangledDid), value: { $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: true, description: "Social coding." } }, { uri: profileUri(microcosmDid), value: { $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: true } } ] }, data: { ...countFor(tangledDid, 24), ...countFor(microcosmDid, 9) } } as never);});
describe("roleForScopes", () => { it("reads account management as ownership", () => { expect(roleForScopes(OWNER_SCOPES)).toBe("owner"); });
it("treats every other grant as membership", () => { expect(roleForScopes(EDITOR_SCOPES)).toBe("member"); expect(roleForScopes("")).toBe("member"); });});
describe("listUserOrganizations", () => { it("joins the controlled accounts with their profiles and repo counts", async () => { const organizations = await listUserOrganizations(controller, ctx);
expect(vi.mocked(enrich).mock.calls[0][1]).toMatchObject({ xrpc: "sh.tangled.actor.getProfiles", params: { actors: [profileUri(tangledDid), profileUri(microcosmDid)] } }); expect(organizations).toEqual([ { did: tangledDid, handle: "tangled.org", description: "Social coding.", repos: 24, role: "owner", joinedAt: "2025-01-14T09:00:00.000Z" }, { did: microcosmDid, handle: "microcosm.blue", description: undefined, repos: 9, role: "member", joinedAt: "2025-03-02T09:00:00.000Z" } ]); });
it("skips delegated accounts that are not organizations", async () => { vi.mocked(enrich).mockResolvedValue({ output: { items: [ { uri: profileUri(microcosmDid), value: { $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: false } } ] }, data: countFor(tangledDid, 24) } as never);
const organizations = await listUserOrganizations(controller, ctx);
expect(organizations.map((organization) => organization.did)).toEqual([tangledDid]); });
it("still lists the organizations when the appview is unreachable", async () => { vi.mocked(enrich).mockRejectedValue(new Error("bobbin is down"));
const organizations = await listUserOrganizations(controller, ctx);
expect(organizations).toHaveLength(2); expect(organizations[0]).toMatchObject({ handle: "tangled.org", repos: 0, role: "owner" }); });
it("does not reach for profiles when nothing is controlled", async () => { vi.mocked(listControlledAccounts).mockResolvedValue([]);
await expect(listUserOrganizations(controller, ctx)).resolves.toEqual([]); expect(enrich).not.toHaveBeenCalled(); });});
describe("leaveOrganization", () => { it("revokes the viewer's grant and forgets the organization", async () => { const signOut = vi.fn().mockResolvedValue(undefined); vi.mocked(getOrCreateDelegatedSession).mockResolvedValue({ signOut } as unknown as OAuthUserAgent); vi.mocked(removeController).mockResolvedValue({ success: true });
await leaveOrganization(controller, tangledDid);
expect(getOrCreateDelegatedSession).toHaveBeenCalledWith(controller, tangledDid); expect(removeController).toHaveBeenCalledWith(expect.anything(), controllerDid); expect(signOut).toHaveBeenCalled(); expect(forgetOrgDid).toHaveBeenCalledWith(tangledDid); });
it("keeps the organization when the grant cannot be revoked", async () => { vi.mocked(getOrCreateDelegatedSession).mockResolvedValue({ signOut: vi.fn() } as unknown as OAuthUserAgent); vi.mocked(removeController).mockRejectedValue(new Error("Forbidden"));
await expect(leaveOrganization(controller, tangledDid)).rejects.toThrow("Forbidden"); expect(forgetOrgDid).not.toHaveBeenCalled(); });});