mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
Use OS-assigned ports for server integration tests and publish the actual Bun server port after startup, preventing local port collisions from failing server checks. Tested: bun test src/server/__tests__/e2e/business-flow.test.ts Tested: bun test src/server/__tests__/e2e/full-flow.test.ts src/server/__tests__/conversations.test.ts src/server/__tests__/tasks.test.ts src/server/__tests__/haha-openai-oauth-api.test.ts Tested: bun run check:server Confidence: high Scope-risk: narrow
208 lines
6.7 KiB
TypeScript
208 lines
6.7 KiB
TypeScript
/**
|
|
* 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 originalServerPort = ProviderService.getServerPort()
|
|
const server = startServer(0, '127.0.0.1')
|
|
try {
|
|
const res = await fetch(`http://127.0.0.1:${server.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)
|
|
}
|
|
})
|
|
})
|