Files
2026-07-13 13:39:12 +08:00

192 lines
6.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
requestUserCode,
pollForAuthorization,
exchangeCodeForTokens,
runCodexDeviceFlow,
CodexDeviceFlowError,
} from "@/lib/oauth/codexDeviceFlow";
const USERCODE_URL = "https://auth.openai.com/api/accounts/deviceauth/usercode";
const POLL_URL = "https://auth.openai.com/api/accounts/deviceauth/token";
const TOKEN_URL = "https://auth.openai.com/oauth/token";
function withFetch(handler: (url: string, init?: RequestInit) => Response | Promise<Response>) {
const original = global.fetch;
global.fetch = (async (input: RequestInfo | URL, init?: RequestInit) =>
handler(String(input), init)) as typeof fetch;
return () => {
global.fetch = original;
};
}
test("requestUserCode posts client_id and normalizes interval + verification uri", async () => {
const calls: Array<{ url: string; body?: string }> = [];
const restore = withFetch((url, init) => {
calls.push({ url, body: typeof init?.body === "string" ? init.body : undefined });
return new Response(
JSON.stringify({ device_auth_id: "dev-1", user_code: "ABCD-1234", interval: "7" }),
{ status: 200 }
);
});
try {
const uc = await requestUserCode("app_test");
assert.equal(calls[0].url, USERCODE_URL);
assert.deepEqual(JSON.parse(calls[0].body!), { client_id: "app_test" });
assert.equal(uc.deviceAuthId, "dev-1");
assert.equal(uc.userCode, "ABCD-1234");
assert.equal(uc.intervalSec, 7); // string "7" normalized to number
assert.equal(uc.verificationUri, "https://auth.openai.com/codex/device");
} finally {
restore();
}
});
test("requestUserCode accepts the alternate 'usercode' field", async () => {
const restore = withFetch(
() => new Response(JSON.stringify({ device_auth_id: "d", usercode: "X-1", interval: 0 }), {
status: 200,
})
);
try {
const uc = await requestUserCode();
assert.equal(uc.userCode, "X-1");
assert.equal(uc.intervalSec, 5); // 0 → default
} finally {
restore();
}
});
test("requestUserCode maps 404 to device_disabled (admin gating)", async () => {
const restore = withFetch(() => new Response("not enabled", { status: 404 }));
try {
await assert.rejects(requestUserCode(), (err: unknown) => {
assert.ok(err instanceof CodexDeviceFlowError);
assert.equal((err as CodexDeviceFlowError).code, "device_disabled");
assert.equal((err as CodexDeviceFlowError).status, 404);
return true;
});
} finally {
restore();
}
});
test("pollForAuthorization treats 403 as pending then returns code + verifier", async () => {
let n = 0;
const restore = withFetch(() => {
n += 1;
if (n === 1) return new Response("pending", { status: 403 });
return new Response(
JSON.stringify({ authorization_code: "auth-code", code_verifier: "ver-123" }),
{ status: 200 }
);
});
try {
const out = await pollForAuthorization("dev-1", "ABCD", 0.001);
assert.equal(n, 2);
assert.deepEqual(out, { authorizationCode: "auth-code", codeVerifier: "ver-123" });
} finally {
restore();
}
});
test("pollForAuthorization throws timeout when deadline passes", async () => {
const restore = withFetch(() => new Response("pending", { status: 404 }));
try {
await assert.rejects(pollForAuthorization("dev-1", "ABCD", 0.001, { timeoutMs: 5 }), (err) => {
assert.equal((err as CodexDeviceFlowError).code, "timeout");
return true;
});
} finally {
restore();
}
});
test("pollForAuthorization aborts via signal", async () => {
const restore = withFetch(() => new Response("pending", { status: 403 }));
const ac = new AbortController();
ac.abort();
try {
await assert.rejects(
pollForAuthorization("dev-1", "ABCD", 0.001, { signal: ac.signal }),
(err) => {
assert.equal((err as CodexDeviceFlowError).code, "aborted");
return true;
}
);
} finally {
restore();
}
});
test("exchangeCodeForTokens posts the authorization_code grant with the server verifier", async () => {
let body = "";
const restore = withFetch((url, init) => {
body = typeof init?.body === "string" ? init.body : "";
assert.equal(url, TOKEN_URL);
return new Response(
JSON.stringify({
access_token: "at",
refresh_token: "rt",
id_token: "it",
expires_in: 3600,
}),
{ status: 200 }
);
});
try {
const tokens = await exchangeCodeForTokens("auth-code", "ver-123", "app_test");
const params = new URLSearchParams(body);
assert.equal(params.get("grant_type"), "authorization_code");
assert.equal(params.get("client_id"), "app_test");
assert.equal(params.get("code"), "auth-code");
assert.equal(params.get("code_verifier"), "ver-123");
assert.equal(params.get("redirect_uri"), "https://auth.openai.com/deviceauth/callback");
assert.equal(tokens.access_token, "at");
assert.equal(tokens.expires_in, 3600);
} finally {
restore();
}
});
test("runCodexDeviceFlow drives usercode → poll → exchange and reports the user code", async () => {
let polls = 0;
const restore = withFetch((url) => {
if (url === USERCODE_URL) {
return new Response(
JSON.stringify({ device_auth_id: "dev-1", user_code: "WXYZ-9", interval: 0.001 }),
{ status: 200 }
);
}
if (url === POLL_URL) {
polls += 1;
if (polls < 2) return new Response("pending", { status: 404 });
return new Response(
JSON.stringify({ authorization_code: "auth-code", code_verifier: "ver-123" }),
{ status: 200 }
);
}
if (url === TOKEN_URL) {
return new Response(
JSON.stringify({ access_token: "at", refresh_token: "rt", id_token: "it", expires_in: 60 }),
{ status: 200 }
);
}
return new Response("not-found", { status: 404 });
});
const seen: string[] = [];
try {
const tokens = await runCodexDeviceFlow({ onUserCode: (uc) => seen.push(uc.userCode) });
assert.deepEqual(seen, ["WXYZ-9"]);
assert.equal(tokens.access_token, "at");
assert.equal(tokens.refresh_token, "rt");
assert.equal(tokens.id_token, "it");
} finally {
restore();
}
});