Files
lingxi-ai/tests/server/copilot-device-flow.test.ts
T
yi 7d10320a82
Build / build (push) Has been cancelled
NPM Lockfile Check / npm ci --ignore-scripts (push) Has been cancelled
Playwright / e2e (push) Has been cancelled
feat: 灵犀 Studio Web UI 定制版
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 11:29:11 +08:00

140 lines
5.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
startDeviceFlow,
pollDeviceFlow,
COPILOT_OAUTH_CLIENT_ID,
COPILOT_OAUTH_SCOPE,
} from '../../packages/server/src/services/hermes/copilot-device-flow'
function mockJsonResponse(data: any, ok = true, status = 200): any {
return {
ok,
status,
json: async () => data,
text: async () => JSON.stringify(data),
}
}
describe('startDeviceFlow', () => {
beforeEach(() => vi.restoreAllMocks())
it('POSTs client_id + scope and returns parsed device code', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
device_code: 'DC-1',
user_code: 'USER-1234',
verification_uri: 'https://github.com/login/device',
expires_in: 900,
interval: 5,
}))
const data = await startDeviceFlow(fetchSpy as any)
expect(data.device_code).toBe('DC-1')
expect(data.user_code).toBe('USER-1234')
expect(data.verification_uri).toBe('https://github.com/login/device')
expect(data.expires_in).toBe(900)
expect(data.interval).toBe(5)
const [url, init] = fetchSpy.mock.calls[0]
expect(url).toBe('https://github.com/login/device/code')
expect(init.method).toBe('POST')
const body = String(init.body)
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
expect(body).toContain(`scope=${encodeURIComponent(COPILOT_OAUTH_SCOPE)}`)
})
it('throws on non-2xx status', async () => {
const fetchSpy = vi.fn().mockResolvedValue({
ok: false, status: 503, text: async () => 'unavailable',
})
await expect(startDeviceFlow(fetchSpy as any)).rejects.toThrow(/503/)
})
it('throws when required fields are missing', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ device_code: '' }))
await expect(startDeviceFlow(fetchSpy as any)).rejects.toThrow(/missing required/)
})
it('falls back to defaults when expires_in / interval are absent', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
device_code: 'DC-2',
user_code: 'AAAA',
verification_uri: 'https://github.com/login/device',
}))
const data = await startDeviceFlow(fetchSpy as any)
expect(data.expires_in).toBe(900)
expect(data.interval).toBe(5)
})
})
describe('pollDeviceFlow', () => {
beforeEach(() => vi.restoreAllMocks())
it('returns success when access_token is present', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
access_token: 'gho_abc',
token_type: 'bearer',
scope: 'read:user',
}))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('success')
if (r.kind === 'success') {
expect(r.access_token).toBe('gho_abc')
expect(r.token_type).toBe('bearer')
}
})
it('maps authorization_pending → pending', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ error: 'authorization_pending' }))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('pending')
})
it('maps slow_down → slow_down', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ error: 'slow_down' }))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('slow_down')
})
it('maps access_denied → denied', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ error: 'access_denied' }))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('denied')
})
it('maps expired_token → expired', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ error: 'expired_token' }))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('expired')
})
it('maps unknown server errors → error', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({
error: 'unsupported_grant_type',
error_description: 'bad grant',
}))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('error')
if (r.kind === 'error') {
expect(r.error).toBe('unsupported_grant_type')
expect(r.description).toBe('bad grant')
}
})
it('returns error on network failure', async () => {
const fetchSpy = vi.fn().mockRejectedValue(new Error('boom'))
const r = await pollDeviceFlow('DC-1', fetchSpy as any)
expect(r.kind).toBe('error')
if (r.kind === 'error') expect(r.error).toBe('network')
})
it('POSTs grant_type, client_id, device_code', async () => {
const fetchSpy = vi.fn().mockResolvedValue(mockJsonResponse({ access_token: 'gho_x' }))
await pollDeviceFlow('DEVICE-CODE-XYZ', fetchSpy as any)
const [url, init] = fetchSpy.mock.calls[0]
expect(url).toBe('https://github.com/login/oauth/access_token')
const body = String(init.body)
expect(body).toContain(`client_id=${encodeURIComponent(COPILOT_OAUTH_CLIENT_ID)}`)
expect(body).toContain('device_code=DEVICE-CODE-XYZ')
expect(body).toContain('grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code')
})
})