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; 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, operationsTree } from '${generatedFile}'; import { getAll } from '${join(outputPath, 'operations', 'get-all.ts')}'; const api = createApiClient(new HttpClient(), { users: { getAll, }, }); const fullApi = createApiClient(new HttpClient(), operationsTree); fullApi.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); 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); }, 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')); let token: string | null = null; // Создаем HttpClient с onRequest для добавления Bearer токена const httpClient = new HttpClient({ onRequest: (params) => { if (!params.secure || !token) { return params; } return { ...params, headers: { ...params.headers, Authorization: `Bearer ${token}`, }, }; }, }); const api = createApiClient(httpClient, { auth: { login, }, profile: { get, }, }); // Логин const loginResult = await api.auth.login({ email: 'test@example.com', password: 'password' }); // Установка токена token = loginResult.token; // Запрос с токеном const profile = await api.profile.get(); expect(profile.email).toBe('test@example.com'); }, 60000); 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); 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); 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); 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); }); });