Files
EasyCLIProxyAPI/tests/managementApi.test.ts
T
lzt404 ce3d54acd0 style(usage): restyle request rows and include HTTP error details
Render each request as a rounded card without changing columns or fields. Prefix upstream error messages with their HTTP status when it is not already present.
2026-10-05 16:10:37 +08:00

204 lines
9.2 KiB
TypeScript

import { flattenV8ProviderGroups, groupLegacyProviderRecords } from './fixtures/legacyProviderRecords';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { clearMocks, mockIPC } from '@tauri-apps/api/mocks';
import {
apiCallErrorMessage,
providerGroupsApi,
managementApi,
} from '../src/services/managementApi';
import {
apiAccessRemarkLocatorFromRecord,
buildProviderRecord,
createProviderDraft,
providerCategoryMatchesRecord,
providerRecordWithDisabledState,
} from '../src/pages/ApiAccessPage';
describe('v8 Management API requests', () => {
let originalWindow: PropertyDescriptor | undefined;
beforeEach(() => {
originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
Object.defineProperty(globalThis, 'window', { value: {}, writable: true, configurable: true });
});
afterEach(() => {
clearMocks();
if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow);
else Reflect.deleteProperty(globalThis, 'window');
});
it('returns the native config tree without flattening groups or replacing access keys', async () => {
const config = {
'config-version': 8,
access: { 'api-keys': ['client-test'] },
'api-keys': { claude: [{ name: 'team', models: [{ name: 'upstream', alias: 'public' }],
keys: [{ 'api-key': 'one', models: null }, { 'api-key': 'two', models: [] }] }] },
oauth: { 'model-alias': { claude: [{ name: 'original', alias: 'desktop' }] } },
requests: { payload: { override: [] } },
};
mockIPC((_command, args) => {
expect(args?.request).toMatchObject({ method: 'GET', path: '/config' });
return config;
});
expect(await managementApi.get('/config')).toEqual(config);
});
it('preserves native request bodies, query defaults and timeouts', async () => {
const calls: unknown[] = [];
mockIPC((_command, args) => { calls.push(args?.request); return { status: 'ok' }; });
await managementApi.get('/credentials', { all: false, offset: 0, omitted: undefined });
await managementApi.put('/config/access/api-keys', []);
await managementApi.put('/config/routing/session-affinity', false);
await managementApi.post('/requests/api-call', { method: 'GET', url: 'https://example.invalid', auth_index: '0' }, { timeoutMs: 12345 });
await managementApi.delete('/credentials', { body: { names: ['test.json'] } });
expect(calls).toMatchObject([
{ method: 'GET', path: '/credentials', query: { all: 'false', offset: '0' } },
{ method: 'PUT', path: '/config/access/api-keys', body: [] },
{ method: 'PUT', path: '/config/routing/session-affinity', body: false },
{ method: 'POST', path: '/requests/api-call', body: { method: 'GET', url: 'https://example.invalid', auth_index: '0' }, timeoutMs: 12345 },
{ method: 'DELETE', path: '/credentials', body: { names: ['test.json'] } },
]);
});
it('defaults absent exclusions and provider groups without swallowing other errors', async () => {
for (const section of ['codex', 'openai-compatibility']) {
mockIPC(() => { throw 'Management API error (404): not_found'; });
expect(await providerGroupsApi.get(section)).toEqual([]);
for (const message of [
'Management API error (401): unauthorized',
'Management API error (404): 404 page not found',
'Management API request failed: connection refused',
]) {
mockIPC(() => { throw new Error(message); });
await expect(providerGroupsApi.get(section)).rejects.toThrow(message);
}
}
});
it('saves and reloads DeepSeek through the v8 Codex group without credential names', async () => {
for (const baseUrl of ['https://api.deepseek.com', 'https://gateway.example.test/v1']) {
let persisted: unknown = [];
mockIPC((command, args) => {
expect(command).toBe('management_request');
const request = args?.request as { method: string; path: string; body: unknown };
expect(request.path).toBe('/config/api-keys/codex');
if (request.method === 'GET') return persisted;
expect(request.method).toBe('PUT');
const groups = request.body as Record<string, unknown>[];
for (const group of groups) {
for (const key of group.keys as Record<string, unknown>[]) {
if ('name' in key) throw new Error('invalid_config: field name not found in type config.CodexKey');
}
}
persisted = request.body;
return { status: 'ok' };
});
const original = buildProviderRecord('codex-api-key', {
...createProviderDraft('deepseek'), apiKey: 'deepseek-test-key', baseUrl,
models: [{ name: 'deepseek-chat', alias: 'my-deepseek' }],
priority: '5', proxyUrl: 'direct', headersText: 'X-Team: test',
});
const regular = buildProviderRecord('codex-api-key', {
...createProviderDraft('codex-api-key'), apiKey: 'codex-test-key',
baseUrl: 'https://codex.example.test/v1', models: [{ name: 'gpt-test' }],
});
for (const record of [original, providerRecordWithDisabledState('codex-api-key', original, true)]) {
await providerGroupsApi.put('codex', groupLegacyProviderRecords('codex', [regular, record]));
const loaded = { 'codex-api-key': flattenV8ProviderGroups('codex', await providerGroupsApi.get('codex')) };
expect(loaded['codex-api-key']).toEqual([regular, record]);
expect(providerCategoryMatchesRecord('deepseek', loaded['codex-api-key'][1])).toBe(true);
expect(apiAccessRemarkLocatorFromRecord('codex-api-key', loaded['codex-api-key'][1]))
.toEqual(apiAccessRemarkLocatorFromRecord('codex-api-key', original));
await providerGroupsApi.put('codex', groupLegacyProviderRecords('codex', [record, regular]));
expect(flattenV8ProviderGroups('codex', await providerGroupsApi.get('codex'))).toEqual([record, regular]);
}
await providerGroupsApi.put('codex', groupLegacyProviderRecords('codex', [regular]));
expect(flattenV8ProviderGroups('codex', await providerGroupsApi.get('codex'))).toEqual([regular]);
}
});
});
describe('apiCallErrorMessage', () => {
it('保留 HTTP 状态,并从对象和 JSON 字符串错误体中提取可读消息', () => {
expect(apiCallErrorMessage({
status_code: 401,
body: { error: { message: 'token expired', code: 'invalid_api_key' } },
})).toBe('HTTP 401: token expired (invalid_api_key)');
expect(apiCallErrorMessage({
status_code: 401,
body: { error: { message: 'Encountered invalidated oauth token for user, failing request', code: 401 } },
})).toBe('HTTP 401: Encountered invalidated oauth token for user, failing request');
expect(apiCallErrorMessage({
statusCode: 403,
bodyText: JSON.stringify({ detail: 'permission denied' }),
})).toBe('HTTP 403: permission denied');
});
it('消息已经包含状态码时不重复,没有错误体时回退到 HTTP 状态', () => {
expect(apiCallErrorMessage({
status_code: 401,
body: { error: { message: 'HTTP 401: token expired' } },
})).toBe('HTTP 401: token expired');
expect(apiCallErrorMessage({ status_code: 429 })).toBe('Upstream returned HTTP 429');
});
});
describe('v8 Management API compatibility view', () => {
it('flattens grouped credentials and preserves per-key overrides', () => {
const groups = [{
name: 'production',
'base-url': 'https://example.test/v1',
priority: 10,
models: [{ name: 'model-a' }],
keys: [
{ 'api-key': 'first', priority: null, 'proxy-url': 'direct' },
{ 'api-key': 'second', priority: 0 },
],
}];
expect(flattenV8ProviderGroups('codex', groups)).toEqual([
{
name: 'production',
'base-url': 'https://example.test/v1', priority: 10,
models: [{ name: 'model-a' }], 'api-key': 'first', 'proxy-url': 'direct',
},
{
name: 'production',
'base-url': 'https://example.test/v1', priority: 0,
models: [{ name: 'model-a' }], 'api-key': 'second',
},
]);
});
it('groups legacy records without moving key-specific fields into shared policy', () => {
expect(groupLegacyProviderRecords('claude', [{
name: 'delegated-key',
'api-key': 'secret',
'base-url': 'https://claude.test',
models: [{ name: 'claude-test' }],
cloak: { mode: 'always' },
}])).toEqual([{
name: 'delegated-key',
'base-url': 'https://claude.test',
models: [{ name: 'claude-test' }],
keys: [{ 'api-key': 'secret', cloak: { mode: 'always' } }],
}]);
});
it('repairs legacy credential names while preserving policies and unnamed record identities', () => {
const records = flattenV8ProviderGroups('codex', [{
name: 'codex-1', 'base-url': 'https://gateway.example.test/v1', priority: 5,
keys: [
{ name: 'DeepSeek', 'api-key': 'first', weight: 2 },
{ 'api-key': 'second', priority: 0 },
],
}]);
const groups = groupLegacyProviderRecords('codex', records);
expect(groups[0]).toMatchObject({ name: 'DeepSeek', keys: [{ 'api-key': 'first', weight: 2 }] });
expect(groups[1]).toMatchObject({ name: 'codex-2', keys: [{ 'api-key': 'second' }] });
expect(flattenV8ProviderGroups('codex', groups)).toEqual(records);
expect(records[1]).not.toHaveProperty('name');
});
});