import { ClientResponseError } from "@atcute/client";
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.ts";
import { provisionDelegate } from "#lib/api/delegation.ts";
import { createOrg, createOrgAccount, ORG_PROFILE_MARKER } from "#lib/api/orgCreate.ts";
import { ensureProfile } from "#lib/api/profile.ts";
vi.mock("#lib/api/delegation.ts", () => ({ provisionDelegate: vi.fn() }));
vi.mock("#lib/api/actAs.ts", () => ({ getOrCreateDelegatedSession: vi.fn() }));
vi.mock("#lib/api/profile.ts", () => ({
PROFILE_COLLECTION: "org.tangled.actor.profile",
ensureProfile: vi.fn()
}));
const service = { serviceUrl: "https://gate.example", handleDomain: "orgs.example" };
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 orgLabel = "acme";
const fullHandle = `${orgLabel}.orgs.example`;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(provisionDelegate).mockResolvedValue({
did: orgDid,
handle: fullHandle,
controllerDid
});
vi.mocked(getOrCreateDelegatedSession).mockResolvedValue(orgAgent);
});
describe("createOrgAccount", () => {
it("provisions through the sidecar", async () => {
const org = await createOrgAccount(controller, " ACME ", service);
expect(provisionDelegate).toHaveBeenCalledWith(controller, fullHandle, service);
expect(org).toEqual({ did: orgDid, handle: fullHandle });
});
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("refuses to guess a handle domain the deployment does not set", async () => {
await expect(
createOrgAccount(controller, orgLabel, { serviceUrl: "https://gate.example" })
).rejects.toThrow("Organization creation isn't set up on this server.");
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("createOrg", () => {
it("creates the account, then writes the profile marker as the organization", async () => {
const org = await createOrg(controller, orgLabel, service);
expect(provisionDelegate).toHaveBeenCalledWith(controller, fullHandle, service);
expect(getOrCreateDelegatedSession).toHaveBeenCalledWith(controller, orgDid);
expect(ensureProfile).toHaveBeenCalledWith(orgAgent, ORG_PROFILE_MARKER);
expect(ORG_PROFILE_MARKER).toEqual({
$type: "org.tangled.actor.profile",
isOrganization: true
});
expect(org).toEqual({ did: orgDid, handle: fullHandle, warnings: [] });
});
it("retries the organization session once before giving up", async () => {
vi.mocked(getOrCreateDelegatedSession)
.mockRejectedValueOnce(new Error("grant unavailable"))
.mockResolvedValueOnce(orgAgent);
const org = await createOrg(controller, orgLabel, service);
expect(getOrCreateDelegatedSession).toHaveBeenCalledTimes(2);
expect(ensureProfile).toHaveBeenCalledWith(orgAgent, ORG_PROFILE_MARKER);
expect(org.warnings).toEqual([]);
});
it("keeps the account and names the failed session step in the warning", async () => {
const cause = new Error("grant unavailable");
vi.mocked(getOrCreateDelegatedSession).mockRejectedValue(cause);
const log = vi.spyOn(console, "error").mockImplementation(() => {});
const org = await createOrg(controller, orgLabel, service);
expect(getOrCreateDelegatedSession).toHaveBeenCalledTimes(2);
expect(org).toEqual({
did: orgDid,
handle: fullHandle,
warnings: ["Unable to sign in to the organization account (grant unavailable)."]
});
expect(ensureProfile).not.toHaveBeenCalled();
expect(log).toHaveBeenCalledWith(
expect.stringContaining("organization session"),
orgDid,
cause
);
});
it("names the profile write when that is what failed, without redoing the session", async () => {
const cause = new Error("scope missing.");
vi.mocked(ensureProfile).mockRejectedValue(cause);
const log = vi.spyOn(console, "error").mockImplementation(() => {});
const org = await createOrg(controller, orgLabel, service);
expect(getOrCreateDelegatedSession).toHaveBeenCalledTimes(1);
expect(org.warnings).toEqual([
"Unable to save the organization's profile (scope missing)."
]);
expect(log).toHaveBeenCalledWith(expect.stringContaining("profile write"), orgDid, cause);
});
it("shows a PDS error's description rather than its code", async () => {
vi.mocked(ensureProfile).mockRejectedValue(
new ClientResponseError({
status: 500,
data: { error: "InternalServerError", message: "Repo is not ready yet" }
})
);
vi.spyOn(console, "error").mockImplementation(() => {});
const org = await createOrg(controller, orgLabel, service);
expect(org.warnings).toEqual([
"Unable to save the organization's profile (Repo is not ready yet)."
]);
});
it("retries with the existing account without consuming another quota slot", async () => {
await createOrg(controller, orgLabel, service, { did: orgDid, handle: fullHandle });
expect(provisionDelegate).not.toHaveBeenCalled();
expect(getOrCreateDelegatedSession).toHaveBeenCalledWith(controller, orgDid);
expect(ensureProfile).toHaveBeenCalledWith(orgAgent, ORG_PROFILE_MARKER);
});
});