import type { Did } from "@atcute/lexicons/syntax";
import { TokenRefreshError, type Session } from "@atcute/oauth-browser-client";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { orgDidsControlledBy } from "#lib/auth/accounts.ts";
import {
revokeAllOrgSessions,
revokeOrgSessionsFor,
type OrgSessionDependencies
} from "#lib/auth/orgSessions.ts";
const alice = "did:plc:alice" as Did;
const bob = "did:plc:bob" as Did;
const acme = "did:plc:acme" as Did;
const globex = "did:plc:globex" as Did;
const initech = "did:plc:initech" as Did;
const controllers: Record<string, Did> = { [acme]: alice, [globex]: alice, [initech]: bob };
const signOut = vi.fn(async () => {});
const deps: OrgSessionDependencies = {
loadOrgDids: vi.fn(() => Object.keys(controllers) as Did[]),
loadOrgControllers: vi.fn(() => controllers),
getSession: vi.fn(async (sub: Did) => ({ info: { sub } }) as unknown as Session),
createAgent: vi.fn(() => ({ signOut })),
deleteStoredSession: vi.fn(),
forgetOrgDid: vi.fn()
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("orgDidsControlledBy", () => {
it("lists only the orgs minted from the given controller", () => {
expect(orgDidsControlledBy(controllers, alice).sort()).toEqual([acme, globex].sort());
expect(orgDidsControlledBy(controllers, "did:plc:nobody" as Did)).toEqual([]);
});
});
describe("revokeOrgSessionsFor", () => {
it("revokes the controller's org sessions without a refresh and forgets them", async () => {
await revokeOrgSessionsFor(alice, deps);
expect(deps.getSession).toHaveBeenCalledWith(acme, { allowStale: true });
expect(deps.getSession).toHaveBeenCalledWith(globex, { allowStale: true });
expect(deps.getSession).not.toHaveBeenCalledWith(initech, expect.anything());
expect(signOut).toHaveBeenCalledTimes(2);
expect(deps.forgetOrgDid).toHaveBeenCalledWith(acme);
expect(deps.forgetOrgDid).toHaveBeenCalledWith(globex);
expect(deps.forgetOrgDid).not.toHaveBeenCalledWith(initech);
});
it("drops the local copy when there is nothing left to revoke", async () => {
vi.mocked(deps.getSession).mockRejectedValueOnce(
new TokenRefreshError(acme, "session deleted by another tab")
);
await revokeOrgSessionsFor(alice, deps);
expect(deps.deleteStoredSession).toHaveBeenCalledWith(acme);
expect(deps.deleteStoredSession).not.toHaveBeenCalledWith(globex);
expect(deps.forgetOrgDid).toHaveBeenCalledWith(acme);
});
it("still forgets an org whose revocation request failed", async () => {
signOut.mockRejectedValueOnce(new TypeError("fetch failed"));
await revokeOrgSessionsFor(alice, deps);
expect(deps.deleteStoredSession).toHaveBeenCalledTimes(1);
expect(deps.forgetOrgDid).toHaveBeenCalledTimes(2);
});
});
describe("revokeAllOrgSessions", () => {
it("revokes every recorded org session and forgets each one", async () => {
await revokeAllOrgSessions(deps);
expect(signOut).toHaveBeenCalledTimes(3);
expect(deps.forgetOrgDid).toHaveBeenCalledTimes(3);
});
});