File size: 4,745 Bytes
39e315a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | 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<number> {
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<Response>
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<void>(resolve => {
resolveFetchStart = resolve
})
globalThis.fetch = mock((input, init) => {
const url = String(input)
if (url.startsWith('http://localhost:')) {
return originalFetch(input, init)
}
return new Promise<Response>((_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<Response>
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')
})
|