import { describe, expect, it, vi } from "vitest";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { mainSchema as addSecretSchema } from "#lib/api/lexicons/types/org/tangled/secret/addSecret.ts";
import { didOf, type Capability, type KnotOrigin } from "#lib/api/syntax.ts";
import type { Fetch } from "#lib/fetch.ts";
const knot = "knot.test" as KnotOrigin;
const minted = vi.hoisted(() => vi.fn());
vi.mock("#lib/auth/agent.ts", () => ({
createClient: () => ({}),
mintServiceAuth: async () => "user-token",
serviceDidForHost: (host: string) => `did:web:${host}`,
hostForServiceDid: () => null
}));
vi.mock("#lib/api/knot.ts", () => ({
knotCapabilities: async (asked: KnotOrigin) =>
asked === "knot.test"
? (["knot-repo-token-mint"] as unknown as readonly Capability[])
: null
}));
vi.mock("#lib/api/cob.ts", () => ({
knotProcedure: minted
}));
const { spindleXrpc } = await import("#lib/api/spindlewrite.ts");
const agent = { sub: "did:plc:nel" } as unknown as OAuthUserAgent;
const target = { knot, repoDid: didOf("did:plc:repoproof") };
const stubbed = (body: unknown) => {
const fetch = vi.fn<Fetch>(async () => new Response(JSON.stringify(body), { status: 200 }));
vi.stubGlobal("fetch", fetch);
return fetch;
};
describe("spindle write targets", () => {
it("mints a repo token under the per-repo session when the knot advertises it", async () => {
minted.mockResolvedValue({ token: "repo-token" });
const fetch = stubbed({});
const client = await spindleXrpc(agent, new URL("https://spindle.test"), target);
await client.call(addSecretSchema, {
input: { repo: target.repoDid, key: "K", value: "v" }
});
expect(minted).toHaveBeenCalledWith(
agent,
knot,
target.repoDid,
"org.tangled.repo.mintToken",
{
aud: "did:web:spindle.test",
lxm: "org.tangled.secret.addSecret",
exp: expect.any(Number)
},
{ signal: expect.any(AbortSignal), invalidate: false }
);
const sent = new URL(String(fetch.mock.calls[0][0]));
expect(sent.origin).toBe("https://spindle.test");
expect(sent.pathname).toBe("/xrpc/org.tangled.secret.addSecret");
expect(new Headers(fetch.mock.calls[0][1]?.headers).get("authorization")).toBe(
"Bearer repo-token"
);
});
it("uses user token when the knot lacks the cap or mint will fail", async () => {
minted.mockClear();
const olderKnot = stubbed({});
const incapable = await spindleXrpc(agent, new URL("https://spindle.test"), {
repoDid: target.repoDid,
knot: "older-knot.test" as KnotOrigin
});
await incapable.call(addSecretSchema, {
input: { repo: target.repoDid, key: "K", value: "v" }
});
expect(minted).not.toHaveBeenCalled();
expect(new Headers(olderKnot.mock.calls[0][1]?.headers).get("authorization")).toBe(
"Bearer user-token"
);
minted.mockImplementation(() => {
throw new Error("Exp too far in the future.");
});
const failing = stubbed({});
const client = await spindleXrpc(agent, new URL("https://spindle.test"), target);
await client.call(addSecretSchema, {
input: { repo: target.repoDid, key: "K", value: "v" }
});
expect(minted).toHaveBeenCalledTimes(1);
expect(new Headers(failing.mock.calls[0][1]?.headers).get("authorization")).toBe(
"Bearer user-token"
);
});
});