2025-10-28 09:58:44 +03:00
|
|
|
|
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test';
|
|
|
|
|
|
import { execa } from 'execa';
|
|
|
|
|
|
import { setupTest } from '../helpers/setup.js';
|
|
|
|
|
|
import { createMockServer, type MockServer } from '../helpers/mock-server.js';
|
|
|
|
|
|
import { FIXTURES } from '../helpers/fixtures.js';
|
|
|
|
|
|
import { join } from 'path';
|
|
|
|
|
|
import { fileExists, readTextFile } from '../../src/utils/file.js';
|
|
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
|
import { dirname } from 'path';
|
|
|
|
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
|
const __dirname = dirname(__filename);
|
|
|
|
|
|
const CLI_PATH = join(__dirname, '../../src/cli.ts');
|
|
|
|
|
|
|
|
|
|
|
|
describe('E2E Generation', () => {
|
|
|
|
|
|
let tempDir: string;
|
|
|
|
|
|
let cleanup: () => Promise<void>;
|
|
|
|
|
|
let mockServer: MockServer;
|
|
|
|
|
|
|
|
|
|
|
|
beforeAll(() => {
|
|
|
|
|
|
mockServer = createMockServer();
|
|
|
|
|
|
mockServer.start();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterAll(() => {
|
|
|
|
|
|
mockServer.stop();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
|
const setup = await setupTest();
|
|
|
|
|
|
tempDir = setup.tempDir;
|
|
|
|
|
|
cleanup = setup.cleanup;
|
|
|
|
|
|
mockServer.reset();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
|
await cleanup();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('полный цикл генерации', () => {
|
2025-10-28 10:51:14 +03:00
|
|
|
|
test('CLI генерация → создание файла → импорт → использование', async () => {
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Генерация через CLI
|
|
|
|
|
|
const { exitCode } = await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.VALID,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Проверка создания файла
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const exists = await fileExists(generatedFile);
|
|
|
|
|
|
expect(exists).toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Проверка импорта (компиляция TypeScript)
|
|
|
|
|
|
const testFile = join(tempDir, 'test-import.ts');
|
|
|
|
|
|
const testCode = `
|
2026-06-30 10:46:15 +03:00
|
|
|
|
import { createApiClient, HttpClient, operationsTree } from '${generatedFile}';
|
2026-06-30 07:59:52 +03:00
|
|
|
|
import { getAll } from '${join(outputPath, 'operations', 'get-all.ts')}';
|
|
|
|
|
|
|
|
|
|
|
|
const api = createApiClient(new HttpClient(), {
|
|
|
|
|
|
users: {
|
|
|
|
|
|
getAll,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-06-30 10:46:15 +03:00
|
|
|
|
const fullApi = createApiClient(new HttpClient(), operationsTree);
|
|
|
|
|
|
fullApi.getAll;
|
2025-10-28 09:58:44 +03:00
|
|
|
|
console.log('Import successful');
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
await Bun.write(testFile, testCode);
|
|
|
|
|
|
|
|
|
|
|
|
// Компилируем тестовый файл
|
|
|
|
|
|
const { exitCode: compileExitCode } = await execa('bun', ['build', testFile, '--outdir', tempDir]);
|
|
|
|
|
|
expect(compileExitCode).toBe(0);
|
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
|
|
|
|
|
test('генерация из локального файла', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
const { exitCode } = await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.MINIMAL,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
|
|
|
2026-06-30 10:46:15 +03:00
|
|
|
|
expect(await fileExists(join(outputPath, 'index.ts'))).toBe(true);
|
|
|
|
|
|
expect(await fileExists(join(outputPath, 'http-client.ts'))).toBe(true);
|
|
|
|
|
|
expect(await fileExists(join(outputPath, 'operations-tree.ts'))).toBe(true);
|
|
|
|
|
|
expect(await fileExists(join(outputPath, 'operations', 'index.ts'))).toBe(true);
|
2025-10-28 09:58:44 +03:00
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
|
|
test('повторная генерация (перезапись файлов)', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
2026-06-30 07:59:52 +03:00
|
|
|
|
// Первая генерация с endpoints
|
2025-10-28 09:58:44 +03:00
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
2026-06-30 07:59:52 +03:00
|
|
|
|
FIXTURES.VALID,
|
2025-10-28 09:58:44 +03:00
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'operations', 'index.ts');
|
|
|
|
|
|
const staleOperationFile = join(outputPath, 'operations', 'get-all.ts');
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const firstContent = await readTextFile(generatedFile);
|
2026-06-30 07:59:52 +03:00
|
|
|
|
expect(await fileExists(staleOperationFile)).toBe(true);
|
2025-10-28 09:58:44 +03:00
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
// Вторая генерация (перезапись меньшей схемой)
|
2025-10-28 09:58:44 +03:00
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
2026-06-30 07:59:52 +03:00
|
|
|
|
FIXTURES.MINIMAL,
|
2025-10-28 09:58:44 +03:00
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const secondContent = await readTextFile(generatedFile);
|
|
|
|
|
|
|
|
|
|
|
|
// Содержимое должно отличаться
|
|
|
|
|
|
expect(firstContent).not.toBe(secondContent);
|
|
|
|
|
|
|
|
|
|
|
|
// Файл должен существовать
|
|
|
|
|
|
const exists = await fileExists(generatedFile);
|
|
|
|
|
|
expect(exists).toBe(true);
|
2026-06-30 07:59:52 +03:00
|
|
|
|
expect(await fileExists(staleOperationFile)).toBe(false);
|
2025-10-28 09:58:44 +03:00
|
|
|
|
}, 60000);
|
2025-10-28 10:51:14 +03:00
|
|
|
|
|
|
|
|
|
|
test('генерация из HTTP URL', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
// Используем публичный OpenAPI spec
|
|
|
|
|
|
const { exitCode } = await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
'https://petstore3.swagger.io/api/v3/openapi.json',
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 10:51:14 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
2025-10-28 10:51:14 +03:00
|
|
|
|
const exists = await fileExists(generatedFile);
|
|
|
|
|
|
expect(exists).toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем что файл не пустой
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const content = await readTextFile(join(outputPath, 'http-client.ts'));
|
2025-10-28 10:51:14 +03:00
|
|
|
|
expect(content.length).toBeGreaterThan(1000);
|
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
test('флаг --swr больше не поддерживается', async () => {
|
2025-10-28 10:51:14 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
try {
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.VALID,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
|
|
|
|
|
'--swr',
|
|
|
|
|
|
]);
|
|
|
|
|
|
throw new Error('Should have thrown');
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
expect(error.exitCode).not.toBe(0);
|
|
|
|
|
|
}
|
2025-10-28 10:51:14 +03:00
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
test('REST генерация не содержит SWR хуки', async () => {
|
2025-10-28 10:51:14 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
const { exitCode } = await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.VALID,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 10:51:14 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
expect(exitCode).toBe(0);
|
|
|
|
|
|
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'http-client.ts');
|
2025-10-28 10:51:14 +03:00
|
|
|
|
const content = await readTextFile(generatedFile);
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем отсутствие импорта useSWR
|
|
|
|
|
|
expect(content).not.toContain('import useSWR from "swr"');
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем отсутствие use* хуков
|
|
|
|
|
|
expect(content).not.toContain('useGetAll');
|
|
|
|
|
|
expect(content).not.toContain('useGetById');
|
|
|
|
|
|
}, 30000);
|
2025-10-28 09:58:44 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('HTTP запросы с mock сервером', () => {
|
2025-10-28 10:51:14 +03:00
|
|
|
|
test('GET запрос без параметров', async () => {
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.VALID,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
// Динамически импортируем сгенерированный API
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { createApiClient, HttpClient } = await import(generatedFile);
|
|
|
|
|
|
const { getAll } = await import(join(outputPath, 'operations', 'get-all.ts'));
|
|
|
|
|
|
|
|
|
|
|
|
const api = createApiClient(new HttpClient(), {
|
|
|
|
|
|
users: {
|
|
|
|
|
|
getAll,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const result = await api.users.getAll({});
|
2025-10-28 09:58:44 +03:00
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
|
|
|
|
expect(result.length).toBe(2);
|
2025-10-28 09:58:44 +03:00
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
test('POST запрос с body', async () => {
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.VALID,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
// Динамически импортируем сгенерированный API
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { createApiClient, HttpClient } = await import(generatedFile);
|
|
|
|
|
|
const { create } = await import(join(outputPath, 'operations', 'create.ts'));
|
|
|
|
|
|
|
|
|
|
|
|
const api = createApiClient(new HttpClient(), {
|
|
|
|
|
|
users: {
|
|
|
|
|
|
create,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-10-28 10:51:14 +03:00
|
|
|
|
const result = await api.users.create({
|
|
|
|
|
|
email: 'new@example.com',
|
|
|
|
|
|
password: 'password123'
|
|
|
|
|
|
});
|
2025-10-28 09:58:44 +03:00
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
expect(result.id).toBe('3');
|
|
|
|
|
|
expect(result.email).toBe('new@example.com');
|
2025-10-28 09:58:44 +03:00
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
test('обработка 404 статуса', async () => {
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.VALID,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const testFile = join(tempDir, 'test-404.ts');
|
|
|
|
|
|
const testCode = `
|
2026-06-30 07:59:52 +03:00
|
|
|
|
import { createApiClient, HttpClient } from '${join(outputPath, 'index.ts')}';
|
|
|
|
|
|
import { getById } from '${join(outputPath, 'operations', 'get-by-id.ts')}';
|
|
|
|
|
|
|
|
|
|
|
|
const api = createApiClient(new HttpClient(), {
|
|
|
|
|
|
users: {
|
|
|
|
|
|
getById,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-10-28 09:58:44 +03:00
|
|
|
|
try {
|
2026-06-30 07:59:52 +03:00
|
|
|
|
await api.users.getById({ id: '999' });
|
2025-10-28 09:58:44 +03:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.log('error');
|
|
|
|
|
|
}
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
await Bun.write(testFile, testCode);
|
|
|
|
|
|
|
|
|
|
|
|
const { stdout } = await execa('bun', ['run', testFile]);
|
|
|
|
|
|
expect(stdout).toContain('error');
|
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
test('Bearer token authentication', async () => {
|
2025-10-28 09:58:44 +03:00
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.WITH_AUTH,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
2026-06-30 07:59:52 +03:00
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
2025-10-28 09:58:44 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
// Динамически импортируем сгенерированный API
|
2026-06-30 07:59:52 +03:00
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { createApiClient, HttpClient } = await import(generatedFile);
|
|
|
|
|
|
const { login } = await import(join(outputPath, 'operations', 'login.ts'));
|
|
|
|
|
|
const { get } = await import(join(outputPath, 'operations', 'get.ts'));
|
|
|
|
|
|
|
2026-06-30 23:52:06 +03:00
|
|
|
|
let token: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Создаем HttpClient с onRequest для добавления Bearer токена
|
2025-10-28 10:51:14 +03:00
|
|
|
|
const httpClient = new HttpClient({
|
2026-06-30 23:52:06 +03:00
|
|
|
|
onRequest: (params) => {
|
|
|
|
|
|
if (!params.secure || !token) {
|
|
|
|
|
|
return params;
|
2025-10-28 10:51:14 +03:00
|
|
|
|
}
|
2026-06-30 23:52:06 +03:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...params,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
...params.headers,
|
|
|
|
|
|
Authorization: `Bearer ${token}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
2025-10-28 10:51:14 +03:00
|
|
|
|
});
|
2026-06-30 07:59:52 +03:00
|
|
|
|
|
|
|
|
|
|
const api = createApiClient(httpClient, {
|
|
|
|
|
|
auth: {
|
|
|
|
|
|
login,
|
|
|
|
|
|
},
|
|
|
|
|
|
profile: {
|
|
|
|
|
|
get,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-10-28 10:51:14 +03:00
|
|
|
|
|
|
|
|
|
|
// Логин
|
|
|
|
|
|
const loginResult = await api.auth.login({
|
|
|
|
|
|
email: 'test@example.com',
|
|
|
|
|
|
password: 'password'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Установка токена
|
2026-06-30 23:52:06 +03:00
|
|
|
|
token = loginResult.token;
|
2025-10-28 10:51:14 +03:00
|
|
|
|
|
|
|
|
|
|
// Запрос с токеном
|
|
|
|
|
|
const profile = await api.profile.get();
|
2026-06-30 23:52:06 +03:00
|
|
|
|
|
2025-10-28 10:51:14 +03:00
|
|
|
|
expect(profile.email).toBe('test@example.com');
|
2025-10-28 09:58:44 +03:00
|
|
|
|
}, 60000);
|
2026-06-30 23:52:06 +03:00
|
|
|
|
|
2026-07-01 00:13:18 +03:00
|
|
|
|
test('onRequest не должен перетирать явно переданный Authorization', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.MINIMAL,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
|
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { HttpClient } = await import(generatedFile);
|
|
|
|
|
|
let authorizationHeader: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
const http = new HttpClient({
|
|
|
|
|
|
customFetch: async (_url, init) => {
|
|
|
|
|
|
authorizationHeader = new Headers(init?.headers).get('Authorization');
|
|
|
|
|
|
return Response.json({ ok: true });
|
|
|
|
|
|
},
|
|
|
|
|
|
onRequest: (params) => {
|
|
|
|
|
|
if (!params.secure) {
|
|
|
|
|
|
return params;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const headers = new Headers(params.headers);
|
|
|
|
|
|
|
|
|
|
|
|
if (!headers.has('Authorization')) {
|
|
|
|
|
|
headers.set('Authorization', 'Bearer default-token');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...params,
|
|
|
|
|
|
headers,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await http.request({
|
|
|
|
|
|
path: '/auth-header',
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
secure: true,
|
|
|
|
|
|
format: 'json',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
Authorization: 'Bearer explicit-token',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
|
|
expect(authorizationHeader).toBe('Bearer explicit-token');
|
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2026-06-30 23:52:06 +03:00
|
|
|
|
test('onError должен поддерживать retry после ApiError', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.MINIMAL,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
|
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { ApiError, HttpClient } = await import(generatedFile);
|
|
|
|
|
|
let calls = 0;
|
|
|
|
|
|
|
|
|
|
|
|
const http = new HttpClient({
|
|
|
|
|
|
customFetch: async () => {
|
|
|
|
|
|
calls += 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (calls === 1) {
|
|
|
|
|
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Response.json({ ok: true });
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (error, context) => {
|
|
|
|
|
|
if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
|
|
|
|
|
|
return context.retry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await http.request({
|
|
|
|
|
|
path: '/retry',
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
format: 'json',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(calls).toBe(2);
|
|
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2026-07-01 00:13:18 +03:00
|
|
|
|
test('onError должен получать ошибки парсинга успешного ответа', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.MINIMAL,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
|
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { HttpClient } = await import(generatedFile);
|
|
|
|
|
|
let handledError: unknown;
|
|
|
|
|
|
|
|
|
|
|
|
const http = new HttpClient({
|
|
|
|
|
|
customFetch: async () => new Response('{ invalid json', {
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
handledError = error;
|
|
|
|
|
|
return { parsed: false };
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await http.request({
|
|
|
|
|
|
path: '/broken-json',
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
format: 'json',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({ parsed: false });
|
|
|
|
|
|
expect(handledError).toBeInstanceOf(Error);
|
|
|
|
|
|
}, 60000);
|
|
|
|
|
|
|
2026-06-30 23:52:06 +03:00
|
|
|
|
test('timeout должен отменять зависший запрос', async () => {
|
|
|
|
|
|
const outputPath = join(tempDir, 'output');
|
|
|
|
|
|
|
|
|
|
|
|
await execa('bun', [
|
|
|
|
|
|
'run',
|
|
|
|
|
|
CLI_PATH,
|
|
|
|
|
|
'--input',
|
|
|
|
|
|
FIXTURES.MINIMAL,
|
|
|
|
|
|
'--output',
|
|
|
|
|
|
outputPath,
|
|
|
|
|
|
'--mode',
|
|
|
|
|
|
'split',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const generatedFile = join(outputPath, 'index.ts');
|
|
|
|
|
|
const { HttpClient } = await import(generatedFile);
|
|
|
|
|
|
|
|
|
|
|
|
const http = new HttpClient({
|
|
|
|
|
|
customFetch: (_url, init) => new Promise((resolve, reject) => {
|
|
|
|
|
|
init?.signal?.addEventListener('abort', () => {
|
|
|
|
|
|
reject(new DOMException('The operation was aborted.', 'AbortError'));
|
|
|
|
|
|
});
|
|
|
|
|
|
}),
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
|
|
|
|
return { aborted: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await http.request({
|
|
|
|
|
|
path: '/timeout',
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
timeout: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({ aborted: true });
|
|
|
|
|
|
}, 60000);
|
2025-10-28 09:58:44 +03:00
|
|
|
|
});
|
2026-06-30 07:59:52 +03:00
|
|
|
|
});
|