Files
orcs-code/src/services/github/deviceFlow.test.ts
Agent_J ff7d49990d feat: GitHub provider lifecycle and onboarding hardening (#351)
* feat: improve GitHub provider onboarding and lifecycle

* fix: address copilot review in provider manager

* fix: address follow-up copilot review comments

* test: resolve rebase conflict in provider profiles suite

* fix: clear stale github hydrated marker

* fix: harden github onboarding auth precedence

* fix: remove merge markers from provider tests

* fix: resolve latest copilot onboarding comments

---------

Co-authored-by: KRATOS <84986124+gnanam1990@users.noreply.github.com>
2026-04-06 19:18:58 +08:00

171 lines
4.5 KiB
TypeScript

import { afterEach, describe, expect, mock, test } from 'bun:test'
import {
DEFAULT_GITHUB_DEVICE_SCOPE,
GitHubDeviceFlowError,
pollAccessToken,
requestDeviceCode,
} from './deviceFlow.js'
describe('requestDeviceCode', () => {
const originalFetch = globalThis.fetch
afterEach(() => {
globalThis.fetch = originalFetch
})
test('parses successful device code response', async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
device_code: 'abc',
user_code: 'ABCD-1234',
verification_uri: 'https://github.com/login/device',
expires_in: 600,
interval: 5,
}),
{ status: 200 },
),
),
)
const r = await requestDeviceCode({
clientId: 'test-client',
fetchImpl: globalThis.fetch,
})
expect(r.device_code).toBe('abc')
expect(r.user_code).toBe('ABCD-1234')
expect(r.verification_uri).toBe('https://github.com/login/device')
expect(r.expires_in).toBe(600)
expect(r.interval).toBe(5)
})
test('throws on HTTP error', async () => {
globalThis.fetch = mock(() =>
Promise.resolve(new Response('bad', { status: 500 })),
)
await expect(
requestDeviceCode({ clientId: 'x', fetchImpl: globalThis.fetch }),
).rejects.toThrow(GitHubDeviceFlowError)
})
test('uses OAuth-safe default scope', async () => {
let capturedScope = ''
globalThis.fetch = mock((_url: RequestInfo | URL, init?: RequestInit) => {
const body = init?.body
if (body instanceof URLSearchParams) {
capturedScope = body.get('scope') ?? ''
} else {
capturedScope = new URLSearchParams(String(body ?? '')).get('scope') ?? ''
}
return Promise.resolve(
new Response(
JSON.stringify({
device_code: 'abc',
user_code: 'ABCD-1234',
verification_uri: 'https://github.com/login/device',
}),
{ status: 200 },
),
)
})
await requestDeviceCode({ clientId: 'test-client', fetchImpl: globalThis.fetch })
expect(capturedScope).toBe(DEFAULT_GITHUB_DEVICE_SCOPE)
expect(capturedScope).toBe('read:user')
})
test('retries with OAuth-safe scope on invalid_scope', async () => {
const scopesSeen: string[] = []
let callCount = 0
globalThis.fetch = mock((_url: RequestInfo | URL, init?: RequestInit) => {
const body = init?.body
const scope =
body instanceof URLSearchParams
? body.get('scope') ?? ''
: new URLSearchParams(String(body ?? '')).get('scope') ?? ''
scopesSeen.push(scope)
callCount++
if (callCount === 1) {
return Promise.resolve(
new Response(
JSON.stringify({
error: 'invalid_scope',
error_description: 'invalid models scope',
}),
{ status: 400 },
),
)
}
return Promise.resolve(
new Response(
JSON.stringify({
device_code: 'abc',
user_code: 'ABCD-1234',
verification_uri: 'https://github.com/login/device',
}),
{ status: 200 },
),
)
})
const result = await requestDeviceCode({
clientId: 'test-client',
scope: 'read:user,models:read',
fetchImpl: globalThis.fetch,
})
expect(result.device_code).toBe('abc')
expect(callCount).toBe(2)
expect(scopesSeen).toEqual(['read:user,models:read', 'read:user'])
})
})
describe('pollAccessToken', () => {
const originalFetch = globalThis.fetch
afterEach(() => {
globalThis.fetch = originalFetch
})
test('returns token when GitHub responds with access_token immediately', async () => {
let calls = 0
globalThis.fetch = mock(() => {
calls++
return Promise.resolve(
new Response(JSON.stringify({ access_token: 'tok-xyz' }), {
status: 200,
}),
)
})
const token = await pollAccessToken('dev-code', {
clientId: 'cid',
fetchImpl: globalThis.fetch,
})
expect(token).toBe('tok-xyz')
expect(calls).toBe(1)
})
test('throws on access_denied', async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ error: 'access_denied' }), {
status: 200,
}),
),
)
await expect(
pollAccessToken('dc', {
clientId: 'c',
fetchImpl: globalThis.fetch,
}),
).rejects.toThrow(/denied/)
})
})