File size: 6,900 Bytes
1f21206 | 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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | /**
* Integration tests for /api/haha-openai-oauth/* endpoints.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'
import { createServer } from 'net'
import { handleHahaOpenAIOAuthApi } from '../api/haha-openai-oauth.js'
import { hahaOpenAIOAuthService } from '../services/hahaOpenAIOAuthService.js'
import { startServer } from '../index.js'
import { ProviderService } from '../services/providerService.js'
let tmpDir: string
let originalConfigDir: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'haha-openai-oauth-api-test-'),
)
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
process.env.CLAUDE_CONFIG_DIR = tmpDir
}
async function teardown() {
hahaOpenAIOAuthService.dispose()
hahaOpenAIOAuthService.resetCallbackPortForTests()
if (originalConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
function buildReq(
method: string,
pathname: string,
body?: unknown,
): { req: Request; url: URL; segments: string[] } {
const url = new URL(`http://localhost:3456${pathname}`)
const req = new Request(url.toString(), {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
})
const segments = url.pathname.split('/').filter(Boolean)
return { req, url, segments }
}
async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const server = createServer()
server.on('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.port
server.close(() => resolve(port))
})
})
}
describe('POST /api/haha-openai-oauth/start', () => {
beforeEach(setup)
afterEach(teardown)
test('returns authorize URL with PKCE challenge', async () => {
const callbackPort = await getFreePort()
hahaOpenAIOAuthService.setCallbackPortForTests(callbackPort)
const { req, url, segments } = buildReq(
'POST',
'/api/haha-openai-oauth/start',
{ serverPort: 54321 },
)
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
const data = (await res.json()) as { authorizeUrl: string; state: string }
expect(data.authorizeUrl).toContain('code_challenge_method=S256')
expect(data.authorizeUrl).toContain(
'codex_cli_simplified_flow=true',
)
expect(data.authorizeUrl).toContain(
encodeURIComponent(`http://localhost:${callbackPort}/auth/callback`),
)
expect(data.authorizeUrl).not.toContain(
encodeURIComponent('http://localhost:54321/auth/callback'),
)
expect(data.authorizeUrl).not.toContain('originator=')
expect(data.state).toMatch(/^[a-f0-9]{64}$/)
})
test('400 if serverPort missing', async () => {
const { req, url, segments } = buildReq(
'POST',
'/api/haha-openai-oauth/start',
{},
)
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(400)
const body = (await res.json()) as { error: string; message?: string }
expect(body.error).toBe('BAD_REQUEST')
})
})
describe('GET /api/haha-openai-oauth', () => {
beforeEach(setup)
afterEach(teardown)
test('returns loggedIn=false when no token file', async () => {
const { req, url, segments } = buildReq('GET', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
const data = (await res.json()) as { loggedIn: boolean }
expect(data.loggedIn).toBe(false)
})
test('returns loggedIn=true + metadata when token saved', async () => {
await hahaOpenAIOAuthService.saveTokens({
accessToken: 'openai-access-token-xxx',
refreshToken: 'openai-refresh-token-xxx',
expiresAt: Date.now() + 3600_000,
email: 'test@example.com',
accountId: 'acct_123',
})
const { req, url, segments } = buildReq('GET', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
const data = (await res.json()) as {
loggedIn: boolean
expiresAt: number | null
email: string | null
accountId: string | null
}
expect(data.loggedIn).toBe(true)
expect(data.email).toBe('test@example.com')
expect(data.accountId).toBe('acct_123')
// Never leak token values
expect(JSON.stringify(data)).not.toContain('openai-access-token')
expect(JSON.stringify(data)).not.toContain('openai-refresh-token')
})
test('returns loggedIn=false when stored token is expired and refresh fails', async () => {
await hahaOpenAIOAuthService.saveTokens({
accessToken: 'expired-token',
refreshToken: 'revoked-refresh-token',
expiresAt: Date.now() - 1_000,
email: 'test@example.com',
accountId: 'acct_123',
})
hahaOpenAIOAuthService.setRefreshFn(async () => {
throw new Error('refresh revoked')
})
const { req, url, segments } = buildReq('GET', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ loggedIn: false })
})
})
describe('DELETE /api/haha-openai-oauth', () => {
beforeEach(setup)
afterEach(teardown)
test('clears token file', async () => {
await hahaOpenAIOAuthService.saveTokens({
accessToken: 'a',
refreshToken: null,
expiresAt: null,
email: null,
accountId: null,
})
const { req, url, segments } = buildReq('DELETE', '/api/haha-openai-oauth')
const res = await handleHahaOpenAIOAuthApi(req, url, segments)
expect(res.status).toBe(200)
expect(await hahaOpenAIOAuthService.loadTokens()).toBeNull()
})
})
describe('GET /auth/callback', () => {
beforeEach(setup)
afterEach(teardown)
test('routes the OpenAI Codex redirect path to the desktop callback page', async () => {
const port = await getFreePort()
const originalServerPort = ProviderService.getServerPort()
const server = startServer(port, '127.0.0.1')
try {
const res = await fetch(`http://127.0.0.1:${port}/auth/callback`)
expect(res.status).toBe(200)
const html = await res.text()
expect(html).toContain('OpenAI Login Failed')
expect(html).toContain('Missing code or state parameter')
} finally {
server.stop(true)
ProviderService.setServerPort(originalServerPort)
}
})
})
|