Files
openclaw/src/commands/openai-codex-oauth.test.ts
2026-03-03 00:15:14 +00:00

168 lines
5.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
const mocks = vi.hoisted(() => ({
loginOpenAICodex: vi.fn(),
createVpsAwareOAuthHandlers: vi.fn(),
runOpenAIOAuthTlsPreflight: vi.fn(),
formatOpenAIOAuthTlsPreflightFix: vi.fn(),
}));
vi.mock("@mariozechner/pi-ai", () => ({
loginOpenAICodex: mocks.loginOpenAICodex,
}));
vi.mock("./oauth-flow.js", () => ({
createVpsAwareOAuthHandlers: mocks.createVpsAwareOAuthHandlers,
}));
vi.mock("./oauth-tls-preflight.js", () => ({
runOpenAIOAuthTlsPreflight: mocks.runOpenAIOAuthTlsPreflight,
formatOpenAIOAuthTlsPreflightFix: mocks.formatOpenAIOAuthTlsPreflightFix,
}));
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js";
function createPrompter() {
const spin = { update: vi.fn(), stop: vi.fn() };
const prompter: Pick<WizardPrompter, "note" | "progress"> = {
note: vi.fn(async () => {}),
progress: vi.fn(() => spin),
};
return { prompter: prompter as unknown as WizardPrompter, spin };
}
function createRuntime(): RuntimeEnv {
return {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
}
async function runCodexOAuth(params: { isRemote: boolean }) {
const { prompter, spin } = createPrompter();
const runtime = createRuntime();
const result = await loginOpenAICodexOAuth({
prompter,
runtime,
isRemote: params.isRemote,
openUrl: async () => {},
});
return { result, prompter, spin, runtime };
}
describe("loginOpenAICodexOAuth", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.runOpenAIOAuthTlsPreflight.mockResolvedValue({ ok: true });
mocks.formatOpenAIOAuthTlsPreflightFix.mockReturnValue("tls fix");
});
it("returns credentials on successful oauth login", async () => {
const creds = {
provider: "openai-codex" as const,
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
email: "user@example.com",
};
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
onAuth: vi.fn(),
onPrompt: vi.fn(),
});
mocks.loginOpenAICodex.mockResolvedValue(creds);
const { result, spin, runtime } = await runCodexOAuth({ isRemote: false });
expect(result).toEqual(creds);
expect(mocks.loginOpenAICodex).toHaveBeenCalledOnce();
expect(spin.stop).toHaveBeenCalledWith("OpenAI OAuth complete");
expect(runtime.error).not.toHaveBeenCalled();
});
it("reports oauth errors and rethrows", async () => {
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
onAuth: vi.fn(),
onPrompt: vi.fn(),
});
mocks.loginOpenAICodex.mockRejectedValue(new Error("oauth failed"));
const { prompter, spin } = createPrompter();
const runtime = createRuntime();
await expect(
loginOpenAICodexOAuth({
prompter,
runtime,
isRemote: true,
openUrl: async () => {},
}),
).rejects.toThrow("oauth failed");
expect(spin.stop).toHaveBeenCalledWith("OpenAI OAuth failed");
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("oauth failed"));
expect(prompter.note).toHaveBeenCalledWith(
"Trouble with OAuth? See https://docs.openclaw.ai/start/faq",
"OAuth help",
);
});
it("continues OAuth flow on non-certificate preflight failures", async () => {
const creds = {
provider: "openai-codex" as const,
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
email: "user@example.com",
};
mocks.runOpenAIOAuthTlsPreflight.mockResolvedValue({
ok: false,
kind: "network",
message: "Client network socket disconnected before secure TLS connection was established",
});
mocks.createVpsAwareOAuthHandlers.mockReturnValue({
onAuth: vi.fn(),
onPrompt: vi.fn(),
});
mocks.loginOpenAICodex.mockResolvedValue(creds);
const { result, prompter, runtime } = await runCodexOAuth({ isRemote: false });
expect(result).toEqual(creds);
expect(mocks.loginOpenAICodex).toHaveBeenCalledOnce();
expect(runtime.error).not.toHaveBeenCalledWith("tls fix");
expect(prompter.note).not.toHaveBeenCalledWith("tls fix", "OAuth prerequisites");
});
it("fails early with actionable message when TLS preflight fails", async () => {
mocks.runOpenAIOAuthTlsPreflight.mockResolvedValue({
ok: false,
kind: "tls-cert",
code: "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
message: "unable to get local issuer certificate",
});
mocks.formatOpenAIOAuthTlsPreflightFix.mockReturnValue("Run brew postinstall openssl@3");
const { prompter } = createPrompter();
const runtime = createRuntime();
await expect(
loginOpenAICodexOAuth({
prompter,
runtime,
isRemote: false,
openUrl: async () => {},
}),
).rejects.toThrow("unable to get local issuer certificate");
expect(mocks.loginOpenAICodex).not.toHaveBeenCalled();
expect(runtime.error).toHaveBeenCalledWith("Run brew postinstall openssl@3");
expect(prompter.note).toHaveBeenCalledWith(
"Run brew postinstall openssl@3",
"OAuth prerequisites",
);
});
});