import { createServer } from 'node:http' import { afterEach, expect, mock, test } from 'bun:test' import { CodexOAuthService } from './codexOAuth.js' const originalFetch = globalThis.fetch const originalCallbackPort = process.env.CODEX_OAUTH_CALLBACK_PORT const originalClientId = process.env.CODEX_OAUTH_CLIENT_ID afterEach(() => { mock.restore() globalThis.fetch = originalFetch if (originalCallbackPort === undefined) { delete process.env.CODEX_OAUTH_CALLBACK_PORT } else { process.env.CODEX_OAUTH_CALLBACK_PORT = originalCallbackPort } if (originalClientId === undefined) { delete process.env.CODEX_OAUTH_CLIENT_ID } else { process.env.CODEX_OAUTH_CLIENT_ID = originalClientId } }) async function getFreePort(): Promise { return await new Promise((resolve, reject) => { const server = createServer() server.once('error', reject) server.listen(0, '127.0.0.1', () => { const address = server.address() if (!address || typeof address === 'string') { server.close(() => reject(new Error('Failed to allocate test port.'))) return } const { port } = address server.close(error => { if (error) { reject(error) return } resolve(port) }) }) }) } function buildCallbackRequest(authUrl: string): string { const authorizeUrl = new URL(authUrl) const redirectUri = authorizeUrl.searchParams.get('redirect_uri') const state = authorizeUrl.searchParams.get('state') if (!redirectUri || !state) { throw new Error('Codex OAuth test did not receive a valid authorization URL.') } const callbackUrl = new URL(redirectUri) callbackUrl.searchParams.set('code', 'auth-code') callbackUrl.searchParams.set('state', state) return callbackUrl.toString() } test('serves updated success copy after a successful Codex OAuth flow', async () => { const callbackPort = await getFreePort() process.env.CODEX_OAUTH_CALLBACK_PORT = String(callbackPort) process.env.CODEX_OAUTH_CLIENT_ID = 'test-client-id' globalThis.fetch = mock(async (input, init) => { const url = String(input) if (url.startsWith('http://localhost:')) { return originalFetch(input, init) } return new Response( JSON.stringify({ access_token: 'access-token', refresh_token: 'refresh-token', }), { status: 200, headers: { 'Content-Type': 'application/json' }, }, ) }) as typeof fetch const service = new CodexOAuthService() let callbackResponsePromise!: Promise const flowPromise = service.startOAuthFlow(async authUrl => { callbackResponsePromise = originalFetch(buildCallbackRequest(authUrl)) }) const tokens = await flowPromise const callbackResponse = await callbackResponsePromise const html = await callbackResponse.text() expect(tokens.accessToken).toBe('access-token') expect(tokens.refreshToken).toBe('refresh-token') expect(html).toContain('You can return to OpenClaude now.') expect(html).toContain( 'OpenClaude will finish activating your new Codex OAuth login.', ) expect(html).not.toContain('continue automatically') }) test('cancellation during token exchange returns a cancelled page and rejects the flow', async () => { const callbackPort = await getFreePort() process.env.CODEX_OAUTH_CALLBACK_PORT = String(callbackPort) process.env.CODEX_OAUTH_CLIENT_ID = 'test-client-id' let resolveFetchStart!: () => void const fetchStarted = new Promise(resolve => { resolveFetchStart = resolve }) globalThis.fetch = mock((input, init) => { const url = String(input) if (url.startsWith('http://localhost:')) { return originalFetch(input, init) } return new Promise((_resolve, reject) => { resolveFetchStart() const signal = init?.signal if (!signal) { return } if (signal.aborted) { reject(signal.reason) return } signal.addEventListener( 'abort', () => { reject(signal.reason) }, { once: true }, ) }) }) as typeof fetch const service = new CodexOAuthService() let callbackResponsePromise!: Promise const flowPromise = service.startOAuthFlow(async authUrl => { callbackResponsePromise = originalFetch(buildCallbackRequest(authUrl)) }) await fetchStarted service.cleanup() await expect(flowPromise).rejects.toThrow('Codex OAuth flow was cancelled.') const callbackResponse = await callbackResponsePromise const html = await callbackResponse.text() expect(html).toContain('Codex login cancelled') expect(html).toContain('retry in OpenClaude') })