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 { addController, createDelegatedAccount, getScopePresets, listControlledAccounts } from "$lib/api/tranquil"; vi.mock("$lib/api/actAs", () => ({ getOrCreateDelegatedSession: vi.fn() })); vi.mock("$lib/api/profile", () => ({ uploadProfileAvatar: vi.fn(), putProfile: vi.fn() })); vi.mock("$lib/api/tranquil", () => ({ addController: vi.fn(), createDelegatedAccount: vi.fn(), getScopePresets: vi.fn(), listControlledAccounts: vi.fn() })); 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 fullHandle = `acme${ORG_HANDLE_SUFFIX}`; beforeEach(() => { vi.resetAllMocks(); vi.mocked(getScopePresets).mockResolvedValue([ { name: "owner", label: "Owner", description: "", scopes: ownerScopes }, { name: "editor", label: "Editor", description: "", scopes: editorScopes } ]); vi.mocked(listControlledAccounts).mockResolvedValue([]); vi.mocked(createDelegatedAccount).mockResolvedValue({ did: orgDid, handle: fullHandle }); vi.mocked(getOrCreateDelegatedSession).mockResolvedValue(orgAgent); vi.mocked(addController).mockResolvedValue({ success: true }); }); describe("createOrgAccount", () => { it("creates only the delegated account with the owner preset", async () => { const org = await createOrgAccount(controller, " ACME "); expect(createDelegatedAccount).toHaveBeenCalledWith(controller, { handle: fullHandle, controllerScopes: ownerScopes }); expect(org).toEqual({ did: orgDid, handle: fullHandle }); expect(putProfile).not.toHaveBeenCalled(); expect(addController).not.toHaveBeenCalled(); }); it("rejects an existing controlled handle", async () => { vi.mocked(listControlledAccounts).mockResolvedValue([ { did: orgDid, handle: fullHandle, grantedScopes: ownerScopes, grantedAt: "now" } ]); await expect(createOrgAccount(controller, "acme")).rejects.toThrow(/already in use/); expect(createDelegatedAccount).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(createDelegatedAccount).mockImplementation(async () => { events.push("account"); return { did: orgDid, handle: fullHandle }; }); vi.mocked(getOrCreateDelegatedSession).mockImplementation(async () => { events.push("session"); return orgAgent; }); vi.mocked(putProfile).mockImplementation(async () => { events.push("profile"); }); vi.mocked(addController).mockImplementation(async () => { events.push("member"); return { success: true }; }); const org = await createOrg(controller, { handle: "acme", description: "A team", members: [{ did: memberDid, handle: "member.example" }] }); 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: "acme", members: [{ did: memberDid, handle: "member.example" }] }); 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: "acme", members: [] }); 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(); }); });