mirror of
https://github.com/hanzoai/opencode-openai-codex-auth.git
synced 2026-09-24 17:01:02 +00:00
148 lines
5.8 KiB
TypeScript
148 lines
5.8 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import {
|
|
shouldRefreshToken,
|
|
extractRequestUrl,
|
|
rewriteUrlForCodex,
|
|
createCodexHeaders,
|
|
handleErrorResponse,
|
|
} from '../lib/request/fetch-helpers.js';
|
|
import type { Auth } from '../lib/types.js';
|
|
import { URL_PATHS, OPENAI_HEADERS, OPENAI_HEADER_VALUES } from '../lib/constants.js';
|
|
|
|
describe('Fetch Helpers Module', () => {
|
|
describe('shouldRefreshToken', () => {
|
|
it('should return true for non-oauth auth', () => {
|
|
const auth: Auth = { type: 'api', key: 'test-key' };
|
|
expect(shouldRefreshToken(auth)).toBe(true);
|
|
});
|
|
|
|
it('should return true when access token is missing', () => {
|
|
const auth: Auth = { type: 'oauth', access: '', refresh: 'refresh-token', expires: Date.now() + 1000 };
|
|
expect(shouldRefreshToken(auth)).toBe(true);
|
|
});
|
|
|
|
it('should return true when token is expired', () => {
|
|
const auth: Auth = {
|
|
type: 'oauth',
|
|
access: 'access-token',
|
|
refresh: 'refresh-token',
|
|
expires: Date.now() - 1000 // expired
|
|
};
|
|
expect(shouldRefreshToken(auth)).toBe(true);
|
|
});
|
|
|
|
it('should return false for valid oauth token', () => {
|
|
const auth: Auth = {
|
|
type: 'oauth',
|
|
access: 'access-token',
|
|
refresh: 'refresh-token',
|
|
expires: Date.now() + 10000 // valid for 10 seconds
|
|
};
|
|
expect(shouldRefreshToken(auth)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('extractRequestUrl', () => {
|
|
it('should extract URL from string', () => {
|
|
const url = 'https://example.com/test';
|
|
expect(extractRequestUrl(url)).toBe(url);
|
|
});
|
|
|
|
it('should extract URL from URL object', () => {
|
|
const url = new URL('https://example.com/test');
|
|
expect(extractRequestUrl(url)).toBe('https://example.com/test');
|
|
});
|
|
|
|
it('should extract URL from Request object', () => {
|
|
const request = new Request('https://example.com/test');
|
|
expect(extractRequestUrl(request)).toBe('https://example.com/test');
|
|
});
|
|
});
|
|
|
|
describe('rewriteUrlForCodex', () => {
|
|
it('should rewrite /responses to /codex/responses', () => {
|
|
const url = 'https://chatgpt.com/backend-api/responses';
|
|
expect(rewriteUrlForCodex(url)).toBe('https://chatgpt.com/backend-api/codex/responses');
|
|
});
|
|
|
|
it('should not modify URL without /responses', () => {
|
|
const url = 'https://chatgpt.com/backend-api/other';
|
|
expect(rewriteUrlForCodex(url)).toBe(url);
|
|
});
|
|
|
|
it('should only replace first occurrence', () => {
|
|
const url = 'https://example.com/responses/responses';
|
|
const result = rewriteUrlForCodex(url);
|
|
expect(result).toBe('https://example.com/codex/responses/responses');
|
|
});
|
|
});
|
|
|
|
describe('createCodexHeaders', () => {
|
|
const accountId = 'test-account-123';
|
|
const accessToken = 'test-access-token';
|
|
|
|
it('should create headers with all required fields when cache key provided', () => {
|
|
const headers = createCodexHeaders(undefined, accountId, accessToken, { model: 'gpt-5-codex', promptCacheKey: 'session-1' });
|
|
|
|
expect(headers.get('Authorization')).toBe(`Bearer ${accessToken}`);
|
|
expect(headers.get(OPENAI_HEADERS.ACCOUNT_ID)).toBe(accountId);
|
|
expect(headers.get(OPENAI_HEADERS.BETA)).toBe(OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
expect(headers.get(OPENAI_HEADERS.ORIGINATOR)).toBe(OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
expect(headers.get(OPENAI_HEADERS.SESSION_ID)).toBe('session-1');
|
|
expect(headers.get(OPENAI_HEADERS.CONVERSATION_ID)).toBe('session-1');
|
|
expect(headers.get('accept')).toBe('text/event-stream');
|
|
});
|
|
|
|
it('enriches usage limit errors with friendly message and rate limits', async () => {
|
|
const body = {
|
|
error: {
|
|
code: 'usage_limit_reached',
|
|
message: 'limit reached',
|
|
plan_type: 'pro',
|
|
},
|
|
};
|
|
const headers = new Headers({
|
|
'x-codex-primary-used-percent': '75',
|
|
'x-codex-primary-window-minutes': '300',
|
|
'x-codex-primary-reset-at': String(Math.floor(Date.now() / 1000) + 1800),
|
|
});
|
|
const resp = new Response(JSON.stringify(body), { status: 429, headers });
|
|
const enriched = await handleErrorResponse(resp);
|
|
expect(enriched.status).toBe(429);
|
|
const json = await enriched.json() as any;
|
|
expect(json.error).toBeTruthy();
|
|
expect(json.error.friendly_message).toMatch(/usage limit/i);
|
|
expect(json.error.rate_limits.primary.used_percent).toBe(75);
|
|
expect(json.error.rate_limits.primary.window_minutes).toBe(300);
|
|
expect(typeof json.error.rate_limits.primary.resets_at).toBe('number');
|
|
});
|
|
|
|
it('should remove x-api-key header', () => {
|
|
const init = { headers: { 'x-api-key': 'should-be-removed' } } as any;
|
|
const headers = createCodexHeaders(init, accountId, accessToken, { model: 'gpt-5', promptCacheKey: 'session-2' });
|
|
|
|
expect(headers.has('x-api-key')).toBe(false);
|
|
});
|
|
|
|
it('should preserve other existing headers', () => {
|
|
const init = { headers: { 'Content-Type': 'application/json' } } as any;
|
|
const headers = createCodexHeaders(init, accountId, accessToken, { model: 'gpt-5', promptCacheKey: 'session-3' });
|
|
|
|
expect(headers.get('Content-Type')).toBe('application/json');
|
|
});
|
|
|
|
it('should use provided promptCacheKey for both conversation_id and session_id', () => {
|
|
const key = 'ses_abc123';
|
|
const headers = createCodexHeaders(undefined, accountId, accessToken, { promptCacheKey: key });
|
|
expect(headers.get(OPENAI_HEADERS.CONVERSATION_ID)).toBe(key);
|
|
expect(headers.get(OPENAI_HEADERS.SESSION_ID)).toBe(key);
|
|
});
|
|
|
|
it('does not set conversation/session headers when no promptCacheKey provided', () => {
|
|
const headers = createCodexHeaders(undefined, accountId, accessToken, { model: 'gpt-5' });
|
|
expect(headers.get(OPENAI_HEADERS.CONVERSATION_ID)).toBeNull();
|
|
expect(headers.get(OPENAI_HEADERS.SESSION_ID)).toBeNull();
|
|
});
|
|
});
|
|
});
|