import { mkdtempSync, statSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { extractAccountId, parseJwtClaims } from './openai.js'; function base64UrlEncode(obj: unknown): string { return Buffer.from(JSON.stringify(obj)).toString('base64url'); } function makeJwt(payload: Record): string { const header = base64UrlEncode({ alg: 'none', typ: 'JWT' }); const body = base64UrlEncode(payload); // Signature is ignored by parseJwtClaims. return `${header}.${body}.sig`; } describe('OpenAI OAuth helpers', () => { it('parseJwtClaims returns undefined for non-jwt strings', () => { expect(parseJwtClaims('not-a-jwt')).toBeUndefined(); }); it('parseJwtClaims parses base64url payload', () => { const token = makeJwt({ chatgpt_account_id: 'acct_123' }); const claims = parseJwtClaims(token); expect(claims?.chatgpt_account_id).toBe('acct_123'); }); it('extractAccountId prefers chatgpt_account_id', () => { const tokens = { access_token: makeJwt({ chatgpt_account_id: 'acct_a' }), refresh_token: 'rt', id_token: makeJwt({ chatgpt_account_id: 'acct_b' }), }; expect(extractAccountId(tokens)).toBe('acct_b'); }); it('extractAccountId falls back to organizations[0].id', () => { const tokens = { access_token: makeJwt({ organizations: [{ id: 'org_1' }] }), refresh_token: 'rt', }; expect(extractAccountId(tokens)).toBe('org_1'); }); }); describe('auth/openai api key storage', () => { const originalHome = process.env.HOME; const originalEnvKey = process.env.OPENAI_API_KEY; let homeDir: string; beforeEach(() => { homeDir = mkdtempSync(join(tmpdir(), 'flynn-auth-openai-')); process.env.HOME = homeDir; delete process.env.OPENAI_API_KEY; vi.resetModules(); }); afterEach(() => { process.env.HOME = originalHome; if (originalEnvKey) { process.env.OPENAI_API_KEY = originalEnvKey; } else { delete process.env.OPENAI_API_KEY; } }); it('stores, loads, and clears OpenAI API key (preserves OAuth entry)', async () => { const mod = await import('./openai.js'); expect(mod.loadStoredOpenAIApiKey()).toBeNull(); expect(mod.loadStoredOpenAIAuth()).toBeNull(); mod.storeOpenAIApiKey('sk-test'); expect(mod.loadStoredOpenAIApiKey()).toBe('sk-test'); const authFile = join(homeDir, '.config/flynn/auth.json'); const mode = statSync(authFile).mode & 0o777; expect(mode).toBe(0o600); const oauth = { access_token: 'at', refresh_token: 'rt', expires_at: Date.now() + 60_000, created_at: new Date().toISOString(), }; mod.storeOpenAIAuth(oauth); expect(mod.loadStoredOpenAIAuth()?.access_token).toBe('at'); expect(mod.loadStoredOpenAIApiKey()).toBe('sk-test'); mod.clearOpenAIAuth(); expect(mod.loadStoredOpenAIAuth()).toBeNull(); expect(mod.loadStoredOpenAIApiKey()).toBe('sk-test'); mod.clearOpenAIApiKey(); expect(mod.loadStoredOpenAIApiKey()).toBeNull(); }); it('getOpenAIApiKey prefers environment variable', async () => { process.env.OPENAI_API_KEY = 'sk-env'; const mod = await import('./openai.js'); expect(mod.getOpenAIApiKey()).toBe('sk-env'); }); });