import { afterEach, describe, expect, it, vi } from "vitest"; import { loginAnthropic, refreshAnthropicToken } from "../src/utils/oauth/anthropic.js"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", }, }); } function getUrl(input: unknown): string { if (typeof input === "string") { return input; } if (input instanceof URL) { return input.toString(); } if (input instanceof Request) { return input.url; } throw new Error(`Unsupported fetch input: ${String(input)}`); } function getJsonBody(init?: RequestInit): Record { if (typeof init?.body !== "string") { throw new Error(`Expected string request body, got ${typeof init?.body}`); } return JSON.parse(init.body) as Record; } describe.sequential("Anthropic OAuth", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("keeps the localhost redirect_uri for manual callback login", async () => { let authUrl = ""; const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { expect(getUrl(input)).toBe("https://platform.claude.com/v1/oauth/token"); expect(init?.method).toBe("POST"); const body = getJsonBody(init); expect(body.grant_type).toBe("authorization_code"); expect(body.code).toBe("manual-code"); expect(body.redirect_uri).toBe("http://localhost:53692/callback"); return jsonResponse({ access_token: "access-token", refresh_token: "refresh-token", expires_in: 3600, }); }); vi.stubGlobal("fetch", fetchMock); const credentials = await loginAnthropic({ onAuth: (info) => { authUrl = info.url; }, onPrompt: async () => "", onManualCodeInput: async () => { const url = new URL(authUrl); const state = url.searchParams.get("state"); const redirectUri = url.searchParams.get("redirect_uri"); if (!state || !redirectUri) { throw new Error("Missing OAuth state or redirect_uri in auth URL"); } return `${redirectUri}?code=manual-code&state=${state}`; }, }); expect(credentials.access).toBe("access-token"); expect(credentials.refresh).toBe("refresh-token"); expect(fetchMock).toHaveBeenCalledOnce(); }); it("omits scope from refresh token requests", async () => { const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { expect(getUrl(input)).toBe("https://platform.claude.com/v1/oauth/token"); expect(init?.method).toBe("POST"); const body = getJsonBody(init); expect(body.grant_type).toBe("refresh_token"); expect(body.client_id).toBeTruthy(); expect(body.refresh_token).toBe("refresh-token"); expect(body).not.toHaveProperty("scope"); return jsonResponse({ access_token: "new-access-token", refresh_token: "new-refresh-token", expires_in: 3600, }); }); vi.stubGlobal("fetch", fetchMock); const credentials = await refreshAnthropicToken("refresh-token"); expect(credentials.access).toBe("new-access-token"); expect(credentials.refresh).toBe("new-refresh-token"); expect(fetchMock).toHaveBeenCalledOnce(); }); });