import 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 { addOrgMembers, createOrg, createOrgAccount, ORG_HANDLE_SUFFIX, writeOrgProfile } from "$lib/api/orgCreate"; import { putProfile } from "$lib/api/profile"; import type { WrittenRecord } from "$lib/api/write"; import { addController, getScopePresets } from "$lib/api/tranquil"; import { provisionDelegate } from "$lib/api/delegation"; vi.mock("$lib/api/delegation", () => ({ provisionDelegate: vi.fn() })); vi.mock("$lib/api/actAs", () => ({ getOrCreateDelegatedSession: vi.fn() })); vi.mock(import("$lib/api/profile"), async (importOriginal) => ({ ...(await importOriginal()), uploadProfileAvatar: vi.fn(), putProfile: vi.fn() })); vi.mock("$lib/api/tranquil", () => ({ addController: vi.fn(), getScopePresets: vi.fn() })); const service = { serviceUrl: "https://gate.example" }; const ownerScopes = "owner-scopes"; const editorScopes = "atproto repo:*?action=create repo:*?action=update repo:*?action=delete blob:*/* rpc:*"; const controllerDid = "did:plc:controller" as Did; const controller = { sub: controllerDid } as OAuthUserAgent; const orgAgent = {} as OAuthUserAgent; const orgDid = "did:plc:org" as Did; const memberDid = "did:plc:member" as Did; const orgLabel = "acme"; const fullHandle = `${orgLabel}${ORG_HANDLE_SUFFIX}`; const writtenProfile = { uri: `at://${orgDid}/sh.tangled.actor.profile/self`, cid: "bafyreianonexistentcid" } as unknown as WrittenRecord; beforeEach(() => { vi.resetAllMocks(); vi.mocked(getScopePresets).mockResolvedValue([ { name: "owner", label: "Owner", description: "", scopes: ownerScopes }, { name: "editor", label: "Editor", description: "", scopes: editorScopes } ]); vi.mocked(provisionDelegate).mockResolvedValue({ did: orgDid, handle: fullHandle, controllerDid }); vi.mocked(getOrCreateDelegatedSession).mockResolvedValue(orgAgent); vi.mocked(addController).mockResolvedValue({ success: true }); }); describe("createOrgAccount", () => { it("provisions through the sidecar without calling the controller PDS delegation APIs", async () => { const org = await createOrgAccount(controller, " ACME ", service); expect(provisionDelegate).toHaveBeenCalledWith(controller, fullHandle, service); expect(getScopePresets).not.toHaveBeenCalled(); expect(org).toEqual({ did: orgDid, handle: fullHandle }); expect(putProfile).not.toHaveBeenCalled(); expect(addController).not.toHaveBeenCalled(); }); it("rejects a name that is not a valid subdomain label", async () => { await expect(createOrgAccount(controller, "-bad", service)).rejects.toThrow(/name/); expect(provisionDelegate).not.toHaveBeenCalled(); }); it("rejects a name carrying its own domain", async () => { await expect(createOrgAccount(controller, "acme.owner.example", service)).rejects.toThrow( /name/ ); expect(provisionDelegate).not.toHaveBeenCalled(); }); }); describe("writeOrgProfile", () => { it("writes the organization profile through its delegated agent", async () => { await writeOrgProfile(orgAgent, { description: " A software team ", website: "https://example.com" }); expect(putProfile).toHaveBeenCalledWith(orgAgent, { $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: true, description: "A software team", links: ["https://example.com/"] }); }); }); describe("addOrgMembers", () => { it("grants members Tranquil's repository editor preset", async () => { await addOrgMembers(orgAgent, { ownerDid: controllerDid, members: [{ did: memberDid, handle: "member.example" }] }); expect(addController).toHaveBeenCalledWith(orgAgent, { controllerDid: memberDid, grantedScopes: editorScopes }); }); it("ignores the owner when they are also included in the members list", async () => { const memberErrors = await addOrgMembers(orgAgent, { ownerDid: controllerDid, members: [{ did: controllerDid, handle: "owner.example" }] }); expect(getScopePresets).not.toHaveBeenCalled(); expect(addController).not.toHaveBeenCalled(); expect(memberErrors).toEqual([]); }); it("returns member failures to the page flow", async () => { vi.mocked(addController).mockRejectedValue(new Error("insufficient scope")); const memberErrors = await addOrgMembers(orgAgent, { ownerDid: controllerDid, members: [{ did: memberDid, handle: "member.example" }] }); expect(memberErrors).toEqual([ "Unable to add member.example as a member. Try again later." ]); }); }); describe("createOrg", () => { it("orchestrates account, session, profile, and members in order", async () => { const events: string[] = []; vi.mocked(provisionDelegate).mockImplementation(async () => { events.push("account"); return { did: orgDid, handle: fullHandle, controllerDid }; }); vi.mocked(getOrCreateDelegatedSession).mockImplementation(async () => { events.push("session"); return orgAgent; }); vi.mocked(putProfile).mockImplementation(async () => { events.push("profile"); return writtenProfile; }); vi.mocked(addController).mockImplementation(async () => { events.push("member"); return { success: true }; }); const org = await createOrg( controller, { handle: orgLabel, description: "A team", members: [{ did: memberDid, handle: "member.example" }] }, service ); expect(events).toEqual(["account", "session", "profile", "member"]); expect(org).toEqual({ did: orgDid, handle: fullHandle, warnings: [] }); }); it("continues independent setup after a partial failure and returns warnings", async () => { vi.mocked(putProfile).mockRejectedValue(new Error("profile unavailable")); vi.mocked(addController).mockRejectedValue(new Error("controller rejected")); const org = await createOrg( controller, { handle: orgLabel, members: [{ did: memberDid, handle: "member.example" }] }, service ); expect(addController).toHaveBeenCalled(); expect(org.warnings).toEqual([ "Unable to save the organization's profile. Try again later.", "Unable to add member.example as a member. Try again later." ]); }); it("reports authorization failure without attempting dependent setup", async () => { vi.mocked(getOrCreateDelegatedSession).mockRejectedValue(new Error("grant unavailable")); const org = await createOrg(controller, { handle: orgLabel, members: [] }, service); expect(org.warnings).toEqual([ "Unable to authorize the organization, so its profile and members were not saved. Try again later." ]); expect(putProfile).not.toHaveBeenCalled(); expect(addController).not.toHaveBeenCalled(); }); }); it("retries setup using the existing account without consuming another quota slot", async () => { await createOrg(controller, { handle: orgLabel, members: [] }, service, { did: orgDid, handle: fullHandle }); expect(provisionDelegate).not.toHaveBeenCalled(); expect(getOrCreateDelegatedSession).toHaveBeenCalledWith(controller, orgDid); expect(putProfile).toHaveBeenCalled(); });