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 {
inviteOrganizationMembers,
listOrganizationMembers,
removeOrganizationMember
} from "#lib/api/orgMembers.ts";
import { removeCollaborator } from "#lib/api/repoSettings.ts";
import {
addController,
getScopePresets,
listControllers,
removeController
} from "#lib/api/tranquil.ts";
vi.mock("#lib/api/actAs.ts", () => ({ getOrCreateDelegatedSession: vi.fn() }));
vi.mock("#lib/api/repoSettings.ts", () => ({
addCollaborator: vi.fn(),
removeCollaborator: vi.fn()
}));
vi.mock("#lib/api/tranquil.ts", () => ({
addController: vi.fn(),
getScopePresets: vi.fn(),
listControllers: vi.fn(),
removeController: vi.fn()
}));
const OWNER_SCOPES =
"atproto repo:* blob:*/* rpc:* identity:* account:*?action=manage transition:generic";
const EDITOR_SCOPES = "atproto repo:*?action=create repo:*?action=update blob:*/* rpc:*";
const controller = { sub: "did:plc:controller" as Did } as OAuthUserAgent;
const orgAgent = { sub: "did:plc:org" as Did } as OAuthUserAgent;
const orgDid = "did:plc:org" as Did;
const aliceDid = "did:plc:alice" as Did;
const bobDid = "did:plc:bob" as Did;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(getOrCreateDelegatedSession).mockResolvedValue(orgAgent);
vi.mocked(getScopePresets).mockResolvedValue([
{ name: "owner", label: "Owner", description: "", scopes: OWNER_SCOPES },
{ name: "editor", label: "Editor", description: "", scopes: EDITOR_SCOPES }
]);
});
describe("listOrganizationMembers", () => {
it("lists the active controllers of the organization with their roles", async () => {
vi.mocked(listControllers).mockResolvedValue([
{
did: aliceDid,
handle: "alice.test",
grantedScopes: OWNER_SCOPES,
grantedAt: "2025-01-14T09:00:00.000Z",
isActive: true,
isLocal: true
},
{
did: bobDid,
grantedScopes: EDITOR_SCOPES,
grantedAt: "2025-03-02T09:00:00.000Z",
isActive: true,
isLocal: false
},
{
did: "did:plc:gone" as Did,
grantedScopes: EDITOR_SCOPES,
grantedAt: "2025-03-03T09:00:00.000Z",
isActive: false,
isLocal: false
}
]);
const members = await listOrganizationMembers(controller, orgDid);
expect(getOrCreateDelegatedSession).toHaveBeenCalledWith(controller, orgDid);
expect(listControllers).toHaveBeenCalledWith(orgAgent);
expect(members).toEqual([
{
did: aliceDid,
handle: "alice.test",
role: "owner",
grantedAt: "2025-01-14T09:00:00.000Z"
},
{
did: bobDid,
handle: undefined,
role: "member",
grantedAt: "2025-03-02T09:00:00.000Z"
}
]);
});
});
describe("inviteOrganizationMembers", () => {
it("adds every invitee as a controller with the editor preset", async () => {
await inviteOrganizationMembers(controller, orgDid, [aliceDid, bobDid]);
expect(getScopePresets).toHaveBeenCalledWith(orgAgent);
expect(vi.mocked(addController).mock.calls).toEqual([
[orgAgent, { controllerDid: aliceDid, grantedScopes: EDITOR_SCOPES }],
[orgAgent, { controllerDid: bobDid, grantedScopes: EDITOR_SCOPES }]
]);
});
it("refuses when the PDS has no editor preset", async () => {
vi.mocked(getScopePresets).mockResolvedValue([]);
await expect(inviteOrganizationMembers(controller, orgDid, [aliceDid])).rejects.toThrow(
/editor role/
);
expect(addController).not.toHaveBeenCalled();
});
});
describe("removeOrganizationMember", () => {
it("removes the member's grant as the organization", async () => {
await removeOrganizationMember(controller, orgDid, bobDid);
expect(removeController).toHaveBeenCalledWith(orgAgent, bobDid);
expect(removeCollaborator).not.toHaveBeenCalled();
});
it("removes the member as a collaborator and returns the repos that failed", async () => {
const kept = { repoDid: "did:plc:kept" as Did, name: "kept", knot: "knot.test" };
const failing = { repoDid: "did:plc:failing" as Did, name: "failing", knot: "knot.test" };
vi.mocked(removeCollaborator).mockImplementation(async (_agent, _knot, repoDid) => {
if (repoDid === failing.repoDid) throw new Error("knot unavailable");
});
const failed = await removeOrganizationMember(controller, orgDid, bobDid, [kept, failing]);
expect(removeController).toHaveBeenCalledWith(orgAgent, bobDid);
expect(vi.mocked(removeCollaborator).mock.calls).toEqual([
[orgAgent, "knot.test", kept.repoDid, bobDid],
[orgAgent, "knot.test", failing.repoDid, bobDid]
]);
expect(failed).toEqual([failing]);
});
});