import { describe, expect, it } from 'bun:test'; import { changedOAuthAuthFileNames, dedupeAuthFiles, isOAuthCredentialFile, normalizeAuthFilePriorityInput, oauthModelProvidersFromAuthFiles, parseAuthFilePriority, setOAuthCredentialFileDisabled, snapshotAuthFiles, } from '../src/services/authFiles'; describe('认证文件列表规范化', () => { it('合并同名的磁盘和运行时记录并优先保留磁盘状态', () => { const files = dedupeAuthFiles([ { name: 'codex-user.json', provider: 'codex', runtime_only: true, account_type: 'api_key', auth_index: 'runtime-index', email: 'user@example.com', }, { name: 'codex-user.json', provider: 'codex', source: 'file', path: '/tmp/codex-user.json', disabled: false, modtime: 100, }, ]); expect(files).toHaveLength(1); expect(files[0].source).toBe('file'); expect(files[0].path).toBe('/tmp/codex-user.json'); expect(files[0].email).toBe('user@example.com'); expect(files[0].auth_index).toBe('runtime-index'); expect(files[0].runtime_only).toBeUndefined(); expect(files[0].account_type).toBeUndefined(); expect(isOAuthCredentialFile(files[0])).toBe(true); }); }); const oauthFile = { name: 'codex-user.json', provider: 'codex', source: 'file', account_type: 'oauth' }; const nonOAuthFiles = [ { ...oauthFile, runtime_only: true }, { ...oauthFile, runtimeOnly: true }, { ...oauthFile, account_type: 'api_key' }, { ...oauthFile, account_type: 'API-KEY' }, { ...oauthFile, auth_kind: 'apikey' }, { ...oauthFile, authKind: 'api_key' }, { ...oauthFile, source: 'memory' }, { ...oauthFile, source: 'config:codex[key]' }, { ...oauthFile, name: 'codex:apikey:runtime-id' }, { ...oauthFile, name: '' }, ]; describe('OAuth credential file boundaries', () => { it('accepts disk-backed OAuth files, disabled files, and legacy disk listings', () => { expect(isOAuthCredentialFile(oauthFile)).toBe(true); expect(isOAuthCredentialFile({ ...oauthFile, disabled: true })).toBe(true); expect(isOAuthCredentialFile({ name: 'legacy.JSON', type: 'codex' })).toBe(true); expect(isOAuthCredentialFile({})).toBe(false); }); it('rejects API-key and runtime records even when they use an OAuth provider or JSON name', () => { for (const file of nonOAuthFiles) expect(isOAuthCredentialFile(file)).toBe(false); }); it('offers only providers with OAuth credential files, preserving aliases and plugin providers', () => { expect(oauthModelProvidersFromAuthFiles([ ...nonOAuthFiles.map((file) => ({ ...file, provider: 'api-only' })), oauthFile, { ...oauthFile, name: 'second.json' }, { name: 'claude.json', type: 'anthropic' }, { name: 'devin.json', type: 'cognition' }, { name: 'antigravity.json', type: 'anti-gravity' }, { name: 'openai.json', type: 'openai' }, { name: 'plugin.json', provider: 'custom-oauth', source: 'file' }, { name: 'unknown.json' }, ])).toEqual(['antigravity', 'claude', 'codex', 'custom-oauth', 'devin']); expect(oauthModelProvidersFromAuthFiles(nonOAuthFiles)).toEqual([]); }); it('enables and disables only the selected OAuth file without touching API configuration', async () => { const writes: unknown[] = []; const api = { patch: async (path: string, body: Record) => { writes.push({ path, body }); } }; await setOAuthCredentialFileDisabled(oauthFile, true, api); await setOAuthCredentialFileDisabled({ ...oauthFile, disabled: true }, false, api); expect(writes).toEqual([ { path: '/auth-files/status', body: { name: 'codex-user.json', disabled: true } }, { path: '/auth-files/status', body: { name: 'codex-user.json', disabled: false } }, ]); }); it('rejects runtime and API-key status changes before any management request', async () => { const writes: unknown[] = []; const api = { patch: async (path: string, body: Record) => { writes.push({ path, body }); } }; for (const file of nonOAuthFiles) { for (const disabled of [true, false]) { await expect(setOAuthCredentialFileDisabled(file, disabled, api)).rejects.toThrow('OAuth'); } } expect(writes).toEqual([]); }); }); describe('authentication file priority', () => { it('accepts safe integers from API values', () => { expect(parseAuthFilePriority(10)).toBe(10); expect(parseAuthFilePriority(' -3 ')).toBe(-3); expect(parseAuthFilePriority(1.5)).toBeUndefined(); expect(parseAuthFilePriority('high')).toBeUndefined(); }); it('uses zero to restore the default and rejects invalid input', () => { expect(normalizeAuthFilePriorityInput('')).toBe(0); expect(normalizeAuthFilePriorityInput('0')).toBe(0); expect(normalizeAuthFilePriorityInput('12')).toBe(12); expect(normalizeAuthFilePriorityInput('1.5')).toBeNull(); }); it('finds only credentials created or updated by the completed OAuth provider', () => { const before = snapshotAuthFiles([ { name: 'codex-old.json', provider: 'codex', modtime: 1, priority: 8 }, { name: 'codex-custom.json', provider: 'codex', modtime: 1, priority: 5 }, { name: 'claude-old.json', provider: 'claude', modtime: 1 }, ]); expect(changedOAuthAuthFileNames(before, [ { name: 'codex-old.json', provider: 'codex', modtime: 2 }, { name: 'codex-custom.json', provider: 'codex', modtime: 2, priority: 5 }, { name: 'codex-new.json', type: 'codex', modtime: 2 }, { name: 'claude-old.json', provider: 'claude', modtime: 2 }, ], 'codex')).toEqual(['codex-old.json', 'codex-new.json']); }); });