Files
EasyCLIProxyAPI/tests/browserMockQuota.test.ts
T
lzt404 46a9d3d449 feat(mock): expand browser mock coverage for providers and edge cases
Add realistic quota, credential, usage, OAuth, and error scenarios.
Support in-memory mutations for imports, resets, cooldowns, and deletions.
Add browser UI and runtime regression tests with mock usage documentation.
2026-10-05 16:10:37 +08:00

83 lines
5.2 KiB
TypeScript

import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test';
import { createBrowserMockRuntime } from '../src/mocks/browserMockRuntime';
import { managementApi } from '../src/services/managementApi';
import { consumeClaudeResetCredit, consumeCodexResetCredit, loadQuota, type AuthFile } from '../src/services/quotaService';
import { resetAuthFileCooldown } from '../src/services/authFiles';
let runtime: ReturnType<typeof createBrowserMockRuntime>;
let get: ReturnType<typeof spyOn>;
let post: ReturnType<typeof spyOn>;
const request = (method: string, path: string, body?: unknown, query?: unknown) =>
runtime.invoke('management_request', { request: { method, path, body, query } });
const files = async () => (await request('GET', '/credentials') as { files: AuthFile[] }).files;
beforeEach(() => {
runtime = createBrowserMockRuntime('running');
get = spyOn(managementApi, 'get').mockImplementation(async (path, query) => await request('GET', path, undefined, query) as never);
post = spyOn(managementApi, 'post').mockImplementation(async (path, body) => await request('POST', path, body) as never);
});
afterEach(() => { get.mockRestore(); post.mockRestore(); });
test('all supported providers parse through the actual quota service', async () => {
const accounts = await files();
expect(accounts.filter(file => file.source === 'file').length).toBeGreaterThan(10);
for (const provider of ['codex', 'claude', 'antigravity', 'kimi', 'xai', 'devin']) {
const file = accounts.find(file => file.provider === provider)!;
const quota = await loadQuota(file);
expect(quota.status).toBe('success');
expect(quota.rows.length).toBeGreaterThanOrEqual(2);
expect(quota.resetCreditsError).toBeUndefined();
expect(quota.rows.every(row => row.remainingPercent !== null), JSON.stringify({ provider, rows: quota.rows })).toBe(true);
}
});
test('edge accounts expose exhausted, unknown, disabled and upstream-error states', async () => {
const accounts = await files();
const quota = (name: string) => loadQuota(accounts.find(file => file.name === name)!);
expect((await quota('codex-exhausted.json')).rows[0].remainingPercent).toBe(0);
expect((await quota('codex-unknown-quota.json')).rows[0].remainingPercent).toBeNull();
expect((await quota('antigravity-empty-quota.json')).status).toBe('error');
expect((await quota('xai-paid-api.json')).rows[0].remainingPercent).toBeNull();
expect((await quota('codex-disabled.json')).status).toBe('error');
expect(await quota('claude-expired.json')).toMatchObject({ status: 'error', error: 'Mock OAuth token expired' });
});
test('Codex and Claude resets consume credits and refresh only that account', async () => {
const accounts = await files();
for (const provider of ['codex', 'claude']) {
const account = accounts.find(file => file.provider === provider)!;
const reset = provider === 'codex' ? consumeCodexResetCredit : consumeClaudeResetCredit;
expect((await loadQuota(account)).resetCredits).toBe(2);
const result = await reset(account);
expect(result.status).toBe('success');
expect(result.resetCredits).toBe(1);
expect(result.rows[0].remainingPercent).toBe(100);
expect((await reset(account)).resetCredits).toBe(0);
}
expect((await loadQuota(accounts.find(file => file.name === 'codex-idle.json')!)).resetCredits).toBe(2);
});
test('cooldown reset acknowledges the correct credential and preserves disabled state', async () => {
const account = (await files()).find(file => file.name === 'codex-exhausted.json')!;
await request('PATCH', '/credentials/status', { name: account.name, disabled: true });
expect(await resetAuthFileCooldown(String(account.auth_index))).toMatchObject({ status: 'ok', auth_index: account.auth_index, models: ['gpt-5.2-codex'] });
expect((await files()).find(file => file.name === account.name)).toMatchObject({ disabled: true, cooldowns: [] });
});
test('imports JSON contents, replaces the same filename, and deletes without mutating other accounts', async () => {
const originalCount = (await files()).length;
const upload = (content: string) => runtime.invoke('upload_auth_file', { name: 'imported.json', data: Array.from(new TextEncoder().encode(content)) });
await upload(JSON.stringify({ type: 'kimi', note: 'Imported example', priority: 9 }));
const first = (await files()).find(file => file.name === 'imported.json')!;
expect(first).toMatchObject({ provider: 'kimi', note: 'Imported example', priority: 9 });
await upload(JSON.stringify({ type: 'claude', note: 'Replaced' }));
expect((await files()).length).toBe(originalCount + 1);
expect((await files()).find(file => file.name === 'imported.json')?.priority).toBeUndefined();
expect(await request('GET', '/credentials/download', undefined, { name: 'imported.json' })).toMatchObject({ provider: 'claude', note: 'Replaced', auth_index: first.auth_index });
await expect(upload('{broken')).rejects.toThrow();
await expect(upload('[]')).rejects.toThrow('JSON object');
await request('DELETE', '/credentials', undefined, { name: 'imported.json' });
expect((await files()).length).toBe(originalCount);
await expect(request('GET', '/credentials/download', undefined, { name: 'imported.json' })).rejects.toThrow('not found');
});