Files
api-codegen/tests/integration/e2e-generation.test.ts
S.Gromov bf340b3dbe feat: добавить split-режим генерации REST-клиента
- добавлен режим генерации single, split и both
- добавлены отдельные operation-файлы и createApiClient
- удалена генерация SWR-хуков и зависимости React/SWR
- обновлены CLI, шаблоны, примеры, документация и тесты
- версия пакета повышена до 3.0.0
2026-06-30 07:59:52 +03:00

377 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('полный цикл генерации', () => {
test('CLI генерация → создание файла → импорт → использование', async () => {
const outputPath = join(tempDir, 'output');
// 1. Генерация через CLI
const { exitCode } = await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--mode',
'split',
]);
expect(exitCode).toBe(0);
// 2. Проверка создания файла
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
// 3. Проверка импорта (компиляция TypeScript)
const testFile = join(tempDir, 'test-import.ts');
const testCode = `
import { createApiClient, HttpClient } from '${generatedFile}';
import { getAll } from '${join(outputPath, 'operations', 'get-all.ts')}';
const api = createApiClient(new HttpClient(), {
users: {
getAll,
},
});
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);
const generatedFile = join(outputPath, 'MinimalAPI.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
}, 30000);
test('повторная генерация (перезапись файлов)', async () => {
const outputPath = join(tempDir, 'output');
// Первая генерация с endpoints
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--mode',
'split',
]);
const generatedFile = join(outputPath, 'operations', 'index.ts');
const staleOperationFile = join(outputPath, 'operations', 'get-all.ts');
const firstContent = await readTextFile(generatedFile);
expect(await fileExists(staleOperationFile)).toBe(true);
// Вторая генерация (перезапись меньшей схемой)
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.MINIMAL,
'--output',
outputPath,
'--mode',
'split',
]);
const secondContent = await readTextFile(generatedFile);
// Содержимое должно отличаться
expect(firstContent).not.toBe(secondContent);
// Файл должен существовать
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
expect(await fileExists(staleOperationFile)).toBe(false);
}, 60000);
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,
'--mode',
'split',
]);
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'index.ts');
const exists = await fileExists(generatedFile);
expect(exists).toBe(true);
// Проверяем что файл не пустой
const content = await readTextFile(join(outputPath, 'http-client.ts'));
expect(content.length).toBeGreaterThan(1000);
}, 60000);
test('флаг --swr больше не поддерживается', async () => {
const outputPath = join(tempDir, 'output');
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);
}
}, 30000);
test('REST генерация не содержит SWR хуки', async () => {
const outputPath = join(tempDir, 'output');
const { exitCode } = await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--mode',
'split',
]);
expect(exitCode).toBe(0);
const generatedFile = join(outputPath, 'http-client.ts');
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);
});
describe('HTTP запросы с mock сервером', () => {
test('GET запрос без параметров', async () => {
const outputPath = join(tempDir, 'output');
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--mode',
'split',
]);
// Динамически импортируем сгенерированный API
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({});
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(2);
}, 60000);
test('POST запрос с body', async () => {
const outputPath = join(tempDir, 'output');
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--mode',
'split',
]);
// Динамически импортируем сгенерированный API
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,
},
});
const result = await api.users.create({
email: 'new@example.com',
password: 'password123'
});
expect(result.id).toBe('3');
expect(result.email).toBe('new@example.com');
}, 60000);
test('обработка 404 статуса', async () => {
const outputPath = join(tempDir, 'output');
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.VALID,
'--output',
outputPath,
'--mode',
'split',
]);
const testFile = join(tempDir, 'test-404.ts');
const testCode = `
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,
},
});
try {
await api.users.getById({ id: '999' });
} catch (error) {
console.log('error');
}
`;
await Bun.write(testFile, testCode);
const { stdout } = await execa('bun', ['run', testFile]);
expect(stdout).toContain('error');
}, 60000);
test('Bearer token authentication', async () => {
const outputPath = join(tempDir, 'output');
await execa('bun', [
'run',
CLI_PATH,
'--input',
FIXTURES.WITH_AUTH,
'--output',
outputPath,
'--mode',
'split',
]);
// Динамически импортируем сгенерированный API
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'));
// Создаем HttpClient с securityWorker для добавления Bearer токена
const httpClient = new HttpClient({
securityWorker: (securityData: string | null) => {
if (securityData) {
return {
headers: {
Authorization: `Bearer ${securityData}`
}
};
}
}
});
const api = createApiClient(httpClient, {
auth: {
login,
},
profile: {
get,
},
});
// Логин
const loginResult = await api.auth.login({
email: 'test@example.com',
password: 'password'
});
// Установка токена
httpClient.setSecurityData(loginResult.token);
// Запрос с токеном
const profile = await api.profile.get();
expect(profile.email).toBe('test@example.com');
}, 60000);
});
});