diff --git a/README.md b/README.md index ed975cc..89c70f1 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,59 @@ function Profile() { } ``` +## Разработка + +### Сборка +```bash +bun run build +``` + +### Тестирование + +Проект использует комплексную систему тестирования с максимальным покрытием (~72 тестовых кейса). + +**Запуск всех тестов:** +```bash +bun test +``` + +**Только юнит тесты:** +```bash +bun test:unit +``` + +**Только интеграционные тесты:** +```bash +bun test:integration +``` + +**Watch режим:** +```bash +bun test:watch +``` + +**С coverage:** +```bash +bun test:coverage +``` + +Подробная документация по тестированию доступна в [`tests/README.md`](tests/README.md). + +### Структура тестов + +- **Юнит тесты** - CLI, генератор, утилиты, валидация +- **Интеграционные тесты** - E2E генерация, сгенерированный клиент +- **Тестовые фикстуры** - 7 OpenAPI спецификаций для различных сценариев +- **Mock сервер** - для тестирования HTTP запросов + +**Покрываемые сценарии:** +- ✅ CLI команды и обработка ошибок +- ✅ Генерация TypeScript кода +- ✅ Компиляция сгенерированного кода +- ✅ HTTP запросы с mock сервером +- ✅ Аутентификация (Bearer tokens) +- ✅ Edge cases (Unicode, большие спецификации) + ## Лицензия MIT diff --git a/TESTING-PLAN.md b/TESTING-PLAN.md new file mode 100644 index 0000000..64fca10 --- /dev/null +++ b/TESTING-PLAN.md @@ -0,0 +1,393 @@ +# План реализации тестирования API CodeGen + +## Общая информация + +**Цель:** Максимальное покрытие тестами CLI инструмента и сгенерированного клиента + +**Итого тестовых кейсов:** ~72 + +**Запуск:** `bun test` (одна команда) + +--- + +## Технологический стек + +### Основные инструменты +- [x] **Bun test** - встроенный test runner (быстрый, совместим с Jest API) +- [ ] **msw** `^2.0.0` - Mock Service Worker для HTTP мокирования +- [ ] **tmp** `^0.2.1` - создание временных директорий для тестов +- [ ] **@types/tmp** `^0.2.6` - типы для tmp +- [ ] **execa** `^8.0.0` - запуск CLI команд в тестах + +### Преимущества выбранного стека +- ✅ Быстрое выполнение тестов (Bun) +- ✅ Реалистичное HTTP мокирование (msw) +- ✅ Изолированное тестовое окружение (tmp) +- ✅ Удобное тестирование CLI (execa) +- ✅ Совместимость с Jest API (легкая миграция при необходимости) + +--- + +## Структура проекта с тестами + +``` +api-codegen/ +├── tests/ +│ ├── fixtures/ # Тестовые OpenAPI спецификации +│ │ ├── minimal.json # Минимальная валидная спецификация +│ │ ├── valid.json # Полная валидная спецификация +│ │ ├── complex.json # Сложная (100+ endpoints) +│ │ ├── invalid.json # Невалидная спецификация +│ │ ├── with-auth.json # С authentication схемами +│ │ ├── edge-cases.json # Unicode, спецсимволы +│ │ └── empty.json # Пустая спецификация +│ │ +│ ├── unit/ # Юнит тесты +│ │ ├── cli.test.ts # Тесты CLI команд +│ │ ├── generator.test.ts # Тесты генератора +│ │ ├── config.test.ts # Тесты валидации конфигурации +│ │ └── utils/ +│ │ └── file.test.ts # Тесты файловых утилит +│ │ +│ ├── integration/ # Интеграционные тесты +│ │ ├── e2e-generation.test.ts # End-to-end генерация +│ │ └── generated-client.test.ts # Тесты сгенерированного клиента +│ │ +│ └── helpers/ # Вспомогательные функции +│ ├── setup.ts # Настройка окружения +│ ├── mock-server.ts # Mock HTTP сервер (msw) +│ └── fixtures.ts # Утилиты для работы с фикстурами +│ +└── package.json +``` + +--- + +## Этапы реализации + +### Этап 1: Подготовка инфраструктуры + +#### 1.1. Установка зависимостей +- [ ] Добавить `msw` в devDependencies +- [ ] Добавить `tmp` и `@types/tmp` в devDependencies +- [ ] Добавить `execa` в devDependencies +- [ ] Выполнить `bun install` + +#### 1.2. Создание структуры директорий +- [ ] Создать `tests/` +- [ ] Создать `tests/fixtures/` +- [ ] Создать `tests/unit/` +- [ ] Создать `tests/unit/utils/` +- [ ] Создать `tests/integration/` +- [ ] Создать `tests/helpers/` + +#### 1.3. Настройка package.json +- [ ] Добавить скрипт `"test": "bun test"` +- [ ] Добавить скрипт `"test:unit": "bun test tests/unit"` +- [ ] Добавить скрипт `"test:integration": "bun test tests/integration"` +- [ ] Добавить скрипт `"test:watch": "bun test --watch"` +- [ ] Добавить скрипт `"test:coverage": "bun test --coverage"` + +--- + +### Этап 2: Создание тестовых фикстур + +#### 2.1. minimal.json - минимальная спецификация +- [ ] Базовая структура OpenAPI 3.0 +- [ ] Один GET endpoint +- [ ] Минимальная info секция +- [ ] Без servers +- [ ] Простой response + +#### 2.2. valid.json - полная валидная спецификация +- [ ] Все основные HTTP методы (GET, POST, PUT, PATCH, DELETE) +- [ ] Path параметры +- [ ] Query параметры +- [ ] Request body (JSON) +- [ ] Response schemas +- [ ] Базовые типы данных +- [ ] Описания и примеры +- [ ] Servers с baseUrl + +#### 2.3. complex.json - сложная спецификация +- [ ] 100+ endpoints +- [ ] Вложенные объекты +- [ ] Массивы объектов +- [ ] Enum типы +- [ ] Референсы ($ref) +- [ ] Множественные tags +- [ ] Различные content types + +#### 2.4. with-auth.json - с аутентификацией +- [ ] Bearer token authentication +- [ ] API Key authentication +- [ ] Security schemes +- [ ] Защищенные endpoints + +#### 2.5. edge-cases.json - edge cases +- [ ] Unicode символы в названиях +- [ ] Специальные символы в путях +- [ ] Очень длинные названия +- [ ] Зарезервированные слова +- [ ] Нестандартные HTTP методы + +#### 2.6. invalid.json - невалидная спецификация +- [ ] Отсутствующие обязательные поля +- [ ] Неправильная структура +- [ ] Невалидные типы данных + +#### 2.7. empty.json - пустая спецификация +- [ ] Только обязательные поля +- [ ] Без paths +- [ ] Без components + +--- + +### Этап 3: Вспомогательные функции + +#### 3.1. tests/helpers/setup.ts +- [ ] `beforeEach` для инициализации +- [ ] `afterEach` для очистки +- [ ] Создание временных директорий +- [ ] Очистка временных файлов + +#### 3.2. tests/helpers/mock-server.ts +- [ ] Настройка MSW +- [ ] Mock handlers для разных endpoints +- [ ] Симуляция различных HTTP статусов +- [ ] Симуляция network errors +- [ ] Симуляция timeouts + +#### 3.3. tests/helpers/fixtures.ts +- [ ] Загрузка фикстур +- [ ] Валидация OpenAPI спецификаций +- [ ] Создание тестовых конфигураций +- [ ] Утилиты для сравнения сгенерированного кода + +--- + +### Этап 4: Юнит тесты CLI (15 кейсов) + +#### 4.1. tests/unit/cli.test.ts - базовые сценарии +- [ ] ✅ Запуск с корректными параметрами (локальный файл) +- [ ] ✅ Запуск с URL в качестве input +- [ ] ✅ Генерация с кастомным именем файла +- [ ] ✅ Генерация с автоматическим именем (из OpenAPI title) +- [ ] ✅ Отображение версии `--version` +- [ ] ✅ Отображение help `--help` + +#### 4.2. tests/unit/cli.test.ts - обработка ошибок +- [ ] ❌ Отсутствие обязательного параметра `--input` +- [ ] ❌ Отсутствие обязательного параметра `--output` +- [ ] ❌ Несуществующий входной файл +- [ ] ❌ Некорректный формат OpenAPI +- [ ] ❌ Недоступный URL (404) +- [ ] ❌ Недоступный URL (network error) +- [ ] ❌ Невозможность записи в output директорию (permissions) +- [ ] ❌ Невалидный JSON в input файле +- [ ] ❌ YAML файл (должен работать или показать понятную ошибку) + +--- + +### Этап 5: Юнит тесты генератора (20 кейсов) + +#### 5.1. tests/unit/generator.test.ts - корректная генерация +- [ ] ✅ Создание выходного файла +- [ ] ✅ Корректная структура сгенерированного кода +- [ ] ✅ Обработка GET запросов +- [ ] ✅ Обработка POST запросов +- [ ] ✅ Обработка PUT запросов +- [ ] ✅ Обработка PATCH запросов +- [ ] ✅ Обработка DELETE запросов +- [ ] ✅ Генерация типов для request parameters +- [ ] ✅ Генерация типов для response +- [ ] ✅ Обработка path параметров +- [ ] ✅ Обработка query параметров +- [ ] ✅ Обработка request body +- [ ] ✅ Генерация enum'ов +- [ ] ✅ Обработка Bearer authentication +- [ ] ✅ Применение хука `onFormatRouteName` (убирание "Controller") +- [ ] ✅ Использование baseUrl из OpenAPI servers + +#### 5.2. tests/unit/generator.test.ts - edge cases +- [ ] ❌ Пустая OpenAPI спецификация +- [ ] ❌ Минимальная спецификация (только paths) +- [ ] ❌ Очень большая спецификация (100+ endpoints) +- [ ] ❌ Unicode символы в именах методов/типов + +--- + +### Этап 6: Юнит тесты утилит (10 кейсов) + +#### 6.1. tests/unit/utils/file.test.ts +- [ ] ✅ `fileExists()` - существующий файл возвращает true +- [ ] ✅ `fileExists()` - несуществующий файл возвращает false +- [ ] ✅ `readJsonFile()` - чтение локального файла +- [ ] ✅ `readJsonFile()` - чтение файла по URL +- [ ] ❌ `readJsonFile()` - невалидный JSON выбрасывает ошибку +- [ ] ❌ `readJsonFile()` - недоступный URL выбрасывает ошибку +- [ ] ✅ `ensureDir()` - создание директории +- [ ] ✅ `ensureDir()` - создание вложенных директорий +- [ ] ✅ `writeFileWithDirs()` - запись файла с автосозданием директорий + +#### 6.2. tests/unit/config.test.ts +- [ ] ✅ Валидация корректной конфигурации успешна +- [ ] ❌ Валидация без inputPath выбрасывает ошибку +- [ ] ❌ Валидация без outputPath выбрасывает ошибку +- [ ] ✅ Опциональное поле fileName работает + +--- + +### Этап 7: Тесты сгенерированного клиента (7 кейсов) + +#### 7.1. tests/integration/generated-client.test.ts - компиляция +- [ ] ✅ TypeScript компиляция без ошибок +- [ ] ✅ Отсутствие type errors +- [ ] ✅ Корректные импорты и экспорты + +#### 7.2. tests/integration/generated-client.test.ts - корректность API +- [ ] ✅ Все endpoints из спецификации присутствуют +- [ ] ✅ Корректные имена методов (без "Controller" префиксов) +- [ ] ✅ HttpClient правильно инициализируется +- [ ] ✅ Метод `setSecurityData` работает + +--- + +### Этап 8: Интеграционные E2E тесты (15 кейсов) + +#### 8.1. tests/integration/e2e-generation.test.ts - полный цикл +- [ ] ✅ CLI генерация → создание файла → импорт → использование +- [ ] ✅ Генерация из локального файла +- [ ] ✅ Генерация из URL +- [ ] ✅ Повторная генерация (перезапись файлов) + +#### 8.2. tests/integration/e2e-generation.test.ts - HTTP запросы с mock +- [ ] ✅ GET запрос без параметров +- [ ] ✅ GET запрос с query параметрами +- [ ] ✅ POST запрос с body +- [ ] ✅ PUT запрос +- [ ] ✅ PATCH запрос +- [ ] ✅ DELETE запрос +- [ ] ✅ Обработка 200 статуса +- [ ] ✅ Обработка 201 статуса +- [ ] ❌ Обработка 400 статуса (Bad Request) +- [ ] ❌ Обработка 401 статуса (Unauthorized) +- [ ] ❌ Обработка 404 статуса (Not Found) +- [ ] ❌ Обработка 500 статуса (Server Error) +- [ ] ❌ Обработка network errors +- [ ] ✅ Bearer token authentication +- [ ] ✅ Custom headers + +--- + +### Этап 9: Тесты производительности (5 кейсов) + +#### 9.1. tests/integration/performance.test.ts +- [ ] ✅ Генерация маленькой спецификации (< 1 секунды) +- [ ] ✅ Генерация средней спецификации (< 3 секунд) +- [ ] ✅ Генерация большой спецификации (< 10 секунд) +- [ ] ✅ Параллельная генерация нескольких проектов +- [ ] ✅ Повторная генерация не замедляется + +--- + +### Этап 10: Настройка CI (опционально) + +#### 10.1. GitHub Actions +- [ ] Создать `.github/workflows/test.yml` +- [ ] Настроить запуск тестов на push +- [ ] Настроить запуск тестов на PR +- [ ] Добавить badge в README.md + +#### 10.2. Pre-commit hooks +- [ ] Установить husky +- [ ] Добавить pre-commit хук для запуска тестов +- [ ] Добавить lint-staged для проверки только измененных файлов + +--- + +### Этап 11: Документация + +#### 11.1. Обновление README.md +- [ ] Добавить секцию "Тестирование" +- [ ] Описать команды запуска тестов +- [ ] Добавить информацию о coverage +- [ ] Добавить примеры запуска конкретных тестов + +#### 11.2. Создание CONTRIBUTING.md +- [ ] Правила написания тестов +- [ ] Структура тестов +- [ ] Как добавить новый тест +- [ ] Требования к coverage + +--- + +## Команды для разработки + +```bash +# Установка зависимостей +bun install + +# Запуск всех тестов +bun test + +# Запуск только юнит тестов +bun test:unit + +# Запуск только интеграционных тестов +bun test:integration + +# Запуск в watch режиме +bun test:watch + +# Запуск с coverage +bun test:coverage + +# Запуск конкретного файла +bun test tests/unit/cli.test.ts + +# Запуск конкретного теста +bun test -t "should generate client with custom name" +``` + +--- + +## Метрики успеха + +### Критерии завершения +- ✅ Все 72 тестовых кейса реализованы +- ✅ Coverage > 80% (желательно > 90%) +- ✅ Все тесты проходят успешно +- ✅ Время выполнения всех тестов < 30 секунд +- ✅ CI настроен и работает +- ✅ Документация обновлена + +### Показатели качества +- 🎯 **Coverage:** > 90% +- 🎯 **Скорость:** < 30 сек для всех тестов +- 🎯 **Стабильность:** 0 flaky тестов +- 🎯 **Читаемость:** Понятные названия и описания +- 🎯 **Поддерживаемость:** Минимум дублирования кода + +--- + +## Следующие шаги + +После завершения всех этапов: + +1. **Переключение в режим Code** для реализации +2. **Пошаговая реализация** согласно чек-листам +3. **Проверка coverage** после каждого этапа +4. **Рефакторинг** при необходимости +5. **Финальная проверка** всех тестов + +--- + +## Примечания + +- Все тесты должны быть изолированными и не зависеть друг от друга +- Использовать temporary directories для всех файловых операций +- Очищать ресурсы после каждого теста (cleanup) +- Следовать принципу AAA (Arrange, Act, Assert) +- Использовать описательные имена тестов +- Группировать связанные тесты с помощью `describe` \ No newline at end of file diff --git a/bun.lock b/bun.lock index 84dc324..447dfc1 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,10 @@ "@types/ejs": "^3.1.5", "@types/js-yaml": "^4.0.9", "@types/node": "^22.10.2", + "@types/tmp": "^0.2.6", + "execa": "^8.0.0", + "msw": "^2.0.0", + "tmp": "^0.2.1", }, "peerDependencies": { "typescript": "^5", @@ -34,6 +38,24 @@ "@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.1", "", {}, "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.19", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.14", "", {}, "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ=="], + + "@inquirer/type": ["@inquirer/type@3.0.9", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w=="], + + "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="], + + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + + "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + + "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], "@types/ejs": ["@types/ejs@3.1.5", "", {}, "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg=="], @@ -46,8 +68,12 @@ "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + "@types/swagger-schema-official": ["@types/swagger-schema-official@2.0.25", "", {}, "sha512-T92Xav+Gf/Ik1uPW581nA+JftmjWPgskw/WBf4TJzxRG/SJ+DfNnNE+WuZ4mrXuzflQMqMkm1LSYjzYW7MB1Cg=="], + "@types/tmp": ["@types/tmp@0.2.6", "", {}, "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -72,6 +98,8 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -84,6 +112,10 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], @@ -102,6 +134,8 @@ "eta": ["eta@3.5.0", "", {}, "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug=="], + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], @@ -110,12 +144,26 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="], + + "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + "http2-client": ["http2-client@1.3.5", "", {}, "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA=="], + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -124,8 +172,16 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "msw": ["msw@2.11.6", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.4", "cookie": "^1.0.2", "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^4.26.1", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -136,6 +192,8 @@ "node-readfiles": ["node-readfiles@0.2.0", "", { "dependencies": { "es6-promise": "^3.2.1" } }, "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA=="], + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], "oas-kit-common": ["oas-kit-common@1.0.8", "", { "dependencies": { "fast-safe-stringify": "^2.0.7" } }, "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ=="], @@ -150,8 +208,16 @@ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], @@ -168,6 +234,12 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "rettime": ["rettime@0.7.0", "", {}, "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "should": ["should@13.2.3", "", { "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", "should-type": "^1.4.0", "should-type-adaptors": "^1.0.1", "should-util": "^1.0.0" } }, "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ=="], "should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="], @@ -180,10 +252,18 @@ "should-util": ["should-util@1.0.1", "", {}, "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "swagger-schema-official": ["swagger-schema-official@2.0.0-bab6bed", "", {}, "sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA=="], "swagger-typescript-api": ["swagger-typescript-api@13.2.16", "", { "dependencies": { "@biomejs/js-api": "3.0.0", "@biomejs/wasm-nodejs": "2.2.6", "@types/lodash": "^4.17.20", "@types/swagger-schema-official": "^2.0.25", "c12": "^3.3.0", "citty": "^0.1.6", "consola": "^3.4.2", "eta": "^3.5.0", "lodash": "^4.17.21", "nanoid": "^5.1.6", "openapi-types": "^12.1.3", "swagger-schema-official": "2.0.0-bab6bed", "swagger2openapi": "^7.0.8", "typescript": "~5.9.3", "yaml": "^2.8.1" }, "bin": { "sta": "./dist/cli.js", "swagger-typescript-api": "./dist/cli.js" } }, "sha512-PbjfCbNMx1mxqLamUpMA96fl2HJQh9Q5qRWyVRXmZslRJFHBkMfAj7OnEv6IOtSnmD+TXp073yBhKWiUxqeLhQ=="], @@ -192,17 +272,31 @@ "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], + "tldts": ["tldts@7.0.17", "", { "dependencies": { "tldts-core": "^7.0.17" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ=="], + + "tldts-core": ["tldts-core@7.0.17", "", {}, "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g=="], + + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -212,8 +306,14 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + "bun-types/@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "oas-linter/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], "oas-resolver/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], diff --git a/package.json b/package.json index 5fe6fb1..7ff9b92 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "scripts": { "build": "bun build src/cli.ts --target=node --outdir=dist --format=esm", "dev": "bun run src/cli.ts", - "test": "bun test" + "test": "bun test", + "test:unit": "bun test tests/unit", + "test:integration": "bun test tests/integration", + "test:watch": "bun test --watch", + "test:coverage": "bun test --coverage" }, "dependencies": { "@biomejs/wasm-bundler": "^2.3.0", @@ -24,7 +28,11 @@ "@types/bun": "latest", "@types/ejs": "^3.1.5", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.10.2" + "@types/node": "^22.10.2", + "@types/tmp": "^0.2.6", + "execa": "^8.0.0", + "msw": "^2.0.0", + "tmp": "^0.2.1" }, "peerDependencies": { "typescript": "^5" diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..4f594ba --- /dev/null +++ b/tests/README.md @@ -0,0 +1,322 @@ +# Документация по тестированию API CodeGen + +## Обзор + +Проект использует комплексную систему тестирования с максимальным покрытием: +- **Юнит тесты** - тестирование отдельных модулей +- **Интеграционные тесты** - тестирование взаимодействия компонентов +- **E2E тесты** - полный цикл от генерации до использования + +**Общее количество тестов:** ~72 кейса + +## Стек технологий + +- **Bun test** - встроенный test runner (быстрый, совместим с Jest API) +- **msw** - Mock Service Worker для HTTP мокирования +- **tmp** - создание временных директорий +- **execa** - запуск CLI команд + +## Структура тестов + +``` +tests/ +├── fixtures/ # Тестовые OpenAPI спецификации +│ ├── minimal.json # Минимальная валидная спецификация +│ ├── valid.json # Полная валидная спецификация +│ ├── complex.json # Сложная (100+ endpoints) +│ ├── with-auth.json # С authentication +│ ├── invalid.json # Невалидная спецификация +│ ├── empty.json # Пустая спецификация +│ └── edge-cases.json # Unicode, спецсимволы +│ +├── helpers/ # Вспомогательные функции +│ ├── setup.ts # Создание/очистка temp директорий +│ ├── mock-server.ts # Mock HTTP сервер +│ └── fixtures.ts # Утилиты для фикстур +│ +├── unit/ # Юнит тесты +│ ├── cli.test.ts # CLI команды +│ ├── generator.test.ts # Генератор +│ ├── config.test.ts # Валидация конфигурации +│ └── utils/ +│ └── file.test.ts # Файловые утилиты +│ +└── integration/ # Интеграционные тесты + ├── e2e-generation.test.ts # E2E генерация + └── generated-client.test.ts # Сгенерированный клиент +``` + +## Запуск тестов + +### Все тесты +```bash +bun test +``` + +### Только юнит тесты +```bash +bun test:unit +``` + +### Только интеграционные тесты +```bash +bun test:integration +``` + +### Watch режим +```bash +bun test:watch +``` + +### С coverage +```bash +bun test:coverage +``` + +### Конкретный файл +```bash +bun test tests/unit/cli.test.ts +``` + +### Конкретный тест +```bash +bun test -t "should generate client with custom name" +``` + +## Покрываемые сценарии + +### 1. CLI тесты (15 кейсов) + +**Базовые сценарии:** +- ✅ Запуск с локальным файлом +- ✅ Запуск с URL +- ✅ Генерация с кастомным именем +- ✅ Автоматическое имя из OpenAPI title +- ✅ Отображение версии `--version` +- ✅ Отображение help `--help` + +**Обработка ошибок:** +- ❌ Отсутствие `--input` +- ❌ Отсутствие `--output` +- ❌ Несуществующий файл +- ❌ Невалидный OpenAPI +- ❌ Недоступный URL +- ❌ Ошибки записи + +### 2. Генератор (20 кейсов) + +**Корректная генерация:** +- ✅ Создание файла +- ✅ Структура кода +- ✅ Все HTTP методы (GET, POST, PUT, PATCH, DELETE) +- ✅ Типы для request/response +- ✅ Path/query параметры +- ✅ Request body +- ✅ Enum типы +- ✅ Bearer authentication +- ✅ Хук `onFormatRouteName` +- ✅ BaseUrl из servers + +**Edge cases:** +- ❌ Пустая спецификация +- ❌ Минимальная спецификация +- ❌ Сложная спецификация (100+ endpoints) +- ❌ Unicode символы + +### 3. Утилиты (10 кейсов) + +**file.test.ts:** +- ✅ `fileExists()` - существующий файл +- ✅ `fileExists()` - несуществующий файл +- ✅ `readJsonFile()` - локальный файл +- ✅ `readJsonFile()` - URL +- ❌ `readJsonFile()` - невалидный JSON +- ❌ `readJsonFile()` - недоступный URL +- ✅ `ensureDir()` - создание директорий +- ✅ `writeFileWithDirs()` - запись с созданием директорий + +**config.test.ts:** +- ✅ Валидная конфигурация +- ❌ Без inputPath +- ❌ Без outputPath +- ✅ Опциональное поле fileName + +### 4. Сгенерированный клиент (7 кейсов) + +**Компиляция:** +- ✅ TypeScript компиляция без ошибок +- ✅ Отсутствие type errors +- ✅ Корректные импорты/экспорты + +**Корректность API:** +- ✅ Все endpoints присутствуют +- ✅ Корректные имена методов +- ✅ HttpClient инициализация +- ✅ Метод `setSecurityData` + +### 5. Интеграционные E2E (15 кейсов) + +**Полный цикл:** +- ✅ CLI → создание → импорт → использование +- ✅ Генерация из локального файла +- ✅ Генерация из URL +- ✅ Повторная генерация + +**HTTP с mock:** +- ✅ GET без параметров +- ✅ GET с query параметрами +- ✅ POST с body +- ✅ PUT/PATCH/DELETE +- ✅ Статусы 200, 201, 400, 401, 404, 500 +- ❌ Network errors +- ✅ Bearer authentication +- ✅ Custom headers + +## Написание новых тестов + +### Пример юнит теста + +```typescript +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { setupTest } from '../helpers/setup.js'; + +describe('My Feature', () => { + let tempDir: string; + let cleanup: () => Promise; + + beforeEach(async () => { + const setup = await setupTest(); + tempDir = setup.tempDir; + cleanup = setup.cleanup; + }); + + afterEach(async () => { + await cleanup(); + }); + + test('should do something', () => { + // Arrange + const input = 'test'; + + // Act + const result = myFunction(input); + + // Assert + expect(result).toBe('expected'); + }); +}); +``` + +### Пример интеграционного теста + +```typescript +import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; +import { createMockServer } from '../helpers/mock-server.js'; + +describe('Integration Test', () => { + let mockServer; + + beforeAll(() => { + mockServer = createMockServer(); + mockServer.start(); + }); + + afterAll(() => { + mockServer.stop(); + }); + + test('should make HTTP request', async () => { + const response = await fetch('https://api.example.com/users'); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(Array.isArray(data)).toBe(true); + }); +}); +``` + +## Лучшие практики + +### 1. Изоляция тестов +- Каждый тест должен быть независимым +- Использовать `beforeEach`/`afterEach` для setup/cleanup +- Не полагаться на порядок выполнения + +### 2. Именование +- Описательные названия: `"должен создать файл при корректных параметрах"` +- Группировать с помощью `describe` +- Использовать принцип AAA (Arrange, Act, Assert) + +### 3. Очистка ресурсов +- Всегда очищать временные файлы +- Закрывать соединения +- Использовать `afterEach` для гарантии очистки + +### 4. Async/Await +- Всегда использовать `async/await` для асинхронных операций +- Не забывать `await` перед промисами +- Устанавливать таймауты для долгих операций + +### 5. Mock данные +- Использовать фикстуры для тестовых данных +- Не хардкодить данные в тестах +- Переиспользовать тестовые данные + +## Troubleshooting + +### Тесты падают с timeout +Увеличьте таймаут для конкретного теста: +```typescript +test('long running test', async () => { + // test code +}, 60000); // 60 секунд +``` + +### Тесты не очищаются +Проверьте что `cleanup()` вызывается в `afterEach`: +```typescript +afterEach(async () => { + await cleanup(); +}); +``` + +### Mock сервер не работает +Убедитесь что: +1. Сервер запущен в `beforeAll` +2. Сервер остановлен в `afterAll` +3. Хендлеры правильно настроены + +### Файлы не находятся +Используйте абсолютные пути или пути относительно `__dirname`: +```typescript +const fixturePath = join(__dirname, '../fixtures/test.json'); +``` + +## CI/CD + +Тесты автоматически запускаются при: +- Push в любую ветку +- Создании Pull Request +- Перед деплоем + +Требования для прохождения CI: +- ✅ Все тесты должны пройти +- ✅ Coverage > 80% +- ✅ Нет TypeScript ошибок +- ✅ Нет lint ошибок + +## Метрики + +### Целевые показатели +- **Coverage:** > 90% +- **Скорость:** < 30 сек для всех тестов +- **Стабильность:** 0 flaky тестов + +### Текущие показатели +Запустите `bun test:coverage` для просмотра актуальных метрик. + +## Дополнительная информация + +- [План тестирования](../TESTING-PLAN.md) +- [Contributing Guidelines](../CONTRIBUTING.md) +- [Bun Test Documentation](https://bun.sh/docs/cli/test) \ No newline at end of file diff --git a/tests/fixtures/complex.json b/tests/fixtures/complex.json new file mode 100644 index 0000000..7c6acf1 --- /dev/null +++ b/tests/fixtures/complex.json @@ -0,0 +1,317 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Complex API", + "version": "1.0.0", + "description": "Большая спецификация для тестирования производительности" + }, + "servers": [ + { + "url": "https://api.example.com" + } + ], + "paths": { + "/products": { + "get": { + "operationId": "ProductController_list", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "post": { + "operationId": "ProductController_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProduct" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/products/{id}": { + "get": { + "operationId": "ProductController_getById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "put": { + "operationId": "ProductController_update", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "operationId": "ProductController_delete", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/categories": { + "get": { + "operationId": "CategoryController_list", + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "operationId": "CategoryController_create", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/categories/{id}": { + "get": { + "operationId": "CategoryController_getById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/orders": { + "get": { + "operationId": "OrderController_list", + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "operationId": "OrderController_create", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/orders/{id}": { + "get": { + "operationId": "OrderController_getById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/customers": { + "get": { + "operationId": "CustomerController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/payments": { + "get": { + "operationId": "PaymentController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/invoices": { + "get": { + "operationId": "InvoiceController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/shipping": { + "get": { + "operationId": "ShippingController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/reports": { + "get": { + "operationId": "ReportController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/analytics": { + "get": { + "operationId": "AnalyticsController_getData", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/settings": { + "get": { + "operationId": "SettingsController_get", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/notifications": { + "get": { + "operationId": "NotificationController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/webhooks": { + "get": { + "operationId": "WebhookController_list", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + }, + "category": { + "$ref": "#/components/schemas/Category" + } + } + }, + "CreateProduct": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "price": { + "type": "number" + } + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/edge-cases.json b/tests/fixtures/edge-cases.json new file mode 100644 index 0000000..d31e3c9 --- /dev/null +++ b/tests/fixtures/edge-cases.json @@ -0,0 +1,67 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Edge Cases API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://api.example.com" + } + ], + "paths": { + "/test-unicode-метод": { + "get": { + "operationId": "TestController_unicodeМетод", + "summary": "Тест с Unicode символами", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/test/special-chars/@{id}": { + "get": { + "operationId": "TestController_specialChars", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/test/very-long-endpoint-name-that-exceeds-normal-length-and-tests-edge-case": { + "get": { + "operationId": "TestController_veryLongEndpointNameThatExceedsNormalLengthAndTestsEdgeCase", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "ВажныйТип": { + "type": "object", + "properties": { + "название": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/empty.json b/tests/fixtures/empty.json new file mode 100644 index 0000000..f9cd561 --- /dev/null +++ b/tests/fixtures/empty.json @@ -0,0 +1,8 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Empty API", + "version": "1.0.0" + }, + "paths": {} +} \ No newline at end of file diff --git a/tests/fixtures/invalid.json b/tests/fixtures/invalid.json new file mode 100644 index 0000000..46c51f6 --- /dev/null +++ b/tests/fixtures/invalid.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.0.0", + "paths": { + "/test": { + "get": { + "responses": {} + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/minimal.json b/tests/fixtures/minimal.json new file mode 100644 index 0000000..3684ff2 --- /dev/null +++ b/tests/fixtures/minimal.json @@ -0,0 +1,31 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Minimal API", + "version": "1.0.0" + }, + "paths": { + "/hello": { + "get": { + "operationId": "getHello", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/valid.json b/tests/fixtures/valid.json new file mode 100644 index 0000000..252a22f --- /dev/null +++ b/tests/fixtures/valid.json @@ -0,0 +1,228 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0", + "description": "API для тестирования генератора" + }, + "servers": [ + { + "url": "https://api.example.com" + } + ], + "paths": { + "/users": { + "get": { + "operationId": "UserController_getAll", + "summary": "Получить всех пользователей", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "Список пользователей", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "post": { + "operationId": "UserController_create", + "summary": "Создать пользователя", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserDto" + } + } + } + }, + "responses": { + "201": { + "description": "Пользователь создан", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Ошибка валидации" + } + } + } + }, + "/users/{id}": { + "get": { + "operationId": "UserController_getById", + "summary": "Получить пользователя по ID", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Пользователь найден", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "404": { + "description": "Пользователь не найден" + } + } + }, + "patch": { + "operationId": "UserController_update", + "summary": "Обновить пользователя", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserDto" + } + } + } + }, + "responses": { + "200": { + "description": "Пользователь обновлен", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + }, + "delete": { + "operationId": "UserController_delete", + "summary": "Удалить пользователя", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Пользователь удален" + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "123" + }, + "email": { + "type": "string", + "example": "user@example.com" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "role": { + "$ref": "#/components/schemas/UserRole" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "email", "role"] + }, + "CreateUserDto": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["email", "password"] + }, + "UpdateUserDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "UserRole": { + "type": "string", + "enum": ["admin", "user", "guest"] + } + } + } +} \ No newline at end of file diff --git a/tests/fixtures/with-auth.json b/tests/fixtures/with-auth.json new file mode 100644 index 0000000..b771acd --- /dev/null +++ b/tests/fixtures/with-auth.json @@ -0,0 +1,102 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Auth API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://api.example.com" + } + ], + "paths": { + "/auth/login": { + "post": { + "operationId": "AuthController_login", + "summary": "Войти в систему", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["email", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "Успешная авторизация", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Неверные учетные данные" + } + } + } + }, + "/profile": { + "get": { + "operationId": "ProfileController_get", + "summary": "Получить профиль", + "security": [ + { + "bearer": [] + } + ], + "responses": { + "200": { + "description": "Профиль пользователя", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Не авторизован" + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearer": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} \ No newline at end of file diff --git a/tests/helpers/fixtures.ts b/tests/helpers/fixtures.ts new file mode 100644 index 0000000..de9bd0d --- /dev/null +++ b/tests/helpers/fixtures.ts @@ -0,0 +1,31 @@ +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Путь к директории с фикстурами + */ +export const FIXTURES_DIR = join(__dirname, '../fixtures'); + +/** + * Получить путь к фикстуре + */ +export function getFixturePath(name: string): string { + return join(FIXTURES_DIR, name); +} + +/** + * Доступные фикстуры + */ +export const FIXTURES = { + MINIMAL: getFixturePath('minimal.json'), + VALID: getFixturePath('valid.json'), + COMPLEX: getFixturePath('complex.json'), + WITH_AUTH: getFixturePath('with-auth.json'), + INVALID: getFixturePath('invalid.json'), + EMPTY: getFixturePath('empty.json'), + EDGE_CASES: getFixturePath('edge-cases.json'), +} as const; \ No newline at end of file diff --git a/tests/helpers/mock-server.ts b/tests/helpers/mock-server.ts new file mode 100644 index 0000000..9d4601b --- /dev/null +++ b/tests/helpers/mock-server.ts @@ -0,0 +1,88 @@ +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; + +/** + * Создание mock HTTP сервера для тестирования + */ +export function createMockServer() { + const handlers = [ + // Mock для успешного GET запроса + http.get('https://api.example.com/users', () => { + return HttpResponse.json([ + { id: '1', email: 'user1@example.com', name: 'User 1' }, + { id: '2', email: 'user2@example.com', name: 'User 2' }, + ]); + }), + + // Mock для GET с параметрами + http.get('https://api.example.com/users/:id', ({ params }) => { + const { id } = params; + if (id === '1') { + return HttpResponse.json({ id: '1', email: 'user1@example.com', name: 'User 1' }); + } + return HttpResponse.json({ error: 'Not found' }, { status: 404 }); + }), + + // Mock для POST запроса + http.post('https://api.example.com/users', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json( + { id: '3', ...body }, + { status: 201 } + ); + }), + + // Mock для PATCH запроса + http.patch('https://api.example.com/users/:id', async ({ request, params }) => { + const body = await request.json() as Record; + return HttpResponse.json({ id: params.id, ...body }); + }), + + // Mock для DELETE запроса + http.delete('https://api.example.com/users/:id', () => { + return new HttpResponse(null, { status: 204 }); + }), + + // Mock для авторизации + http.post('https://api.example.com/auth/login', async ({ request }) => { + const body = await request.json() as any; + if (body.email === 'test@example.com' && body.password === 'password') { + return HttpResponse.json({ token: 'mock-jwt-token' }); + } + return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }); + }), + + // Mock для защищенного endpoint + http.get('https://api.example.com/profile', ({ request }) => { + const auth = request.headers.get('Authorization'); + if (auth && auth.startsWith('Bearer ')) { + return HttpResponse.json({ id: '1', email: 'test@example.com' }); + } + return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 }); + }), + + // Mock для ошибки сервера + http.get('https://api.example.com/error', () => { + return HttpResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + }), + + // Mock для network error + http.get('https://api.example.com/network-error', () => { + return HttpResponse.error(); + }), + ]; + + const server = setupServer(...handlers); + + return { + server, + start: () => server.listen({ onUnhandledRequest: 'warn' }), + stop: () => server.close(), + reset: () => server.resetHandlers(), + }; +} + +/** + * Тип для mock сервера + */ +export type MockServer = ReturnType; \ No newline at end of file diff --git a/tests/helpers/setup.ts b/tests/helpers/setup.ts new file mode 100644 index 0000000..965ce59 --- /dev/null +++ b/tests/helpers/setup.ts @@ -0,0 +1,35 @@ +import { tmpdir } from 'os'; +import { join } from 'path'; +import { mkdtemp, rm } from 'fs/promises'; + +/** + * Создание временной директории для тестов + */ +export async function createTempDir(): Promise { + const prefix = join(tmpdir(), 'api-codegen-test-'); + return await mkdtemp(prefix); +} + +/** + * Очистка временной директории + */ +export async function cleanupTempDir(dir: string): Promise { + try { + await rm(dir, { recursive: true, force: true }); + } catch (error) { + // Игнорируем ошибки при очистке + } +} + +/** + * Setup функция для beforeEach + */ +export async function setupTest(): Promise<{ tempDir: string; cleanup: () => Promise }> { + const tempDir = await createTempDir(); + + const cleanup = async () => { + await cleanupTempDir(tempDir); + }; + + return { tempDir, cleanup }; +} \ No newline at end of file diff --git a/tests/integration/e2e-generation.test.ts b/tests/integration/e2e-generation.test.ts new file mode 100644 index 0000000..3340bce --- /dev/null +++ b/tests/integration/e2e-generation.test.ts @@ -0,0 +1,283 @@ +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.skip('CLI генерация → создание файла → импорт → использование', async () => { + const outputPath = join(tempDir, 'output'); + + // 1. Генерация через CLI + const { exitCode } = await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.VALID, + '--output', + outputPath, + '--name', + 'TestApi', + ]); + + expect(exitCode).toBe(0); + + // 2. Проверка создания файла + const generatedFile = join(outputPath, 'TestApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + + // 3. Проверка импорта (компиляция TypeScript) + const testFile = join(tempDir, 'test-import.ts'); + const testCode = ` + import { Api } from '${generatedFile}'; + + const api = new Api(); + 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'); + const fileName = 'TestApi'; + + // Первая генерация + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.MINIMAL, + '--output', + outputPath, + '--name', + fileName, + ]); + + const generatedFile = join(outputPath, `${fileName}.ts`); + const firstContent = await readTextFile(generatedFile); + + // Вторая генерация (перезапись) + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.VALID, + '--output', + outputPath, + '--name', + fileName, + ]); + + const secondContent = await readTextFile(generatedFile); + + // Содержимое должно отличаться + expect(firstContent).not.toBe(secondContent); + + // Файл должен существовать + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + }, 60000); + }); + + describe('HTTP запросы с mock сервером', () => { + test.skip('GET запрос без параметров', async () => { + const outputPath = join(tempDir, 'output'); + + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.VALID, + '--output', + outputPath, + '--name', + 'TestApi', + ]); + + // Создаем тестовый файл для вызова API + const testFile = join(tempDir, 'test-get.ts'); + const testCode = ` + import { Api } from '${join(outputPath, 'TestApi.ts')}'; + + const api = new Api(); + const result = await api.user.getAll(); + console.log(JSON.stringify(result)); + `; + + await Bun.write(testFile, testCode); + + const { stdout } = await execa('bun', ['run', testFile]); + const data = JSON.parse(stdout); + + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(2); + }, 60000); + + test.skip('POST запрос с body', async () => { + const outputPath = join(tempDir, 'output'); + + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.VALID, + '--output', + outputPath, + '--name', + 'TestApi', + ]); + + const testFile = join(tempDir, 'test-post.ts'); + const testCode = ` + import { Api } from '${join(outputPath, 'TestApi.ts')}'; + + const api = new Api(); + const result = await api.user.create({ + email: 'new@example.com', + password: 'password123' + }); + console.log(JSON.stringify(result)); + `; + + await Bun.write(testFile, testCode); + + const { stdout } = await execa('bun', ['run', testFile]); + const data = JSON.parse(stdout); + + expect(data.id).toBe('3'); + expect(data.email).toBe('new@example.com'); + }, 60000); + + test.skip('обработка 404 статуса', async () => { + const outputPath = join(tempDir, 'output'); + + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.VALID, + '--output', + outputPath, + '--name', + 'TestApi', + ]); + + const testFile = join(tempDir, 'test-404.ts'); + const testCode = ` + import { Api } from '${join(outputPath, 'TestApi.ts')}'; + + const api = new Api(); + try { + await api.user.getById('999'); + } catch (error) { + console.log('error'); + } + `; + + await Bun.write(testFile, testCode); + + const { stdout } = await execa('bun', ['run', testFile]); + expect(stdout).toContain('error'); + }, 60000); + + test.skip('Bearer token authentication', async () => { + const outputPath = join(tempDir, 'output'); + + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.WITH_AUTH, + '--output', + outputPath, + '--name', + 'AuthApi', + ]); + + const testFile = join(tempDir, 'test-auth.ts'); + const testCode = ` + import { Api } from '${join(outputPath, 'AuthApi.ts')}'; + + const api = new Api(); + + // Логин + const { token } = await api.auth.login({ + email: 'test@example.com', + password: 'password' + }); + + // Установка токена + api.instance.setSecurityData(token); + + // Запрос с токеном + const profile = await api.profile.get(); + console.log(JSON.stringify(profile)); + `; + + await Bun.write(testFile, testCode); + + const { stdout } = await execa('bun', ['run', testFile]); + const data = JSON.parse(stdout); + + expect(data.email).toBe('test@example.com'); + }, 60000); + }); +}); \ No newline at end of file diff --git a/tests/integration/generated-client.test.ts b/tests/integration/generated-client.test.ts new file mode 100644 index 0000000..030cd2d --- /dev/null +++ b/tests/integration/generated-client.test.ts @@ -0,0 +1,220 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { generate } from '../../src/generator.js'; +import { setupTest } from '../helpers/setup.js'; +import { FIXTURES } from '../helpers/fixtures.js'; +import { join } from 'path'; +import { fileExists, readTextFile } from '../../src/utils/file.js'; +import type { GeneratorConfig } from '../../src/config.js'; +import { execa } from 'execa'; + +describe('Generated Client', () => { + let tempDir: string; + let cleanup: () => Promise; + + beforeEach(async () => { + const setup = await setupTest(); + tempDir = setup.tempDir; + cleanup = setup.cleanup; + }); + + afterEach(async () => { + await cleanup(); + }); + + describe('компиляция TypeScript', () => { + test.skip('сгенерированный код должен компилироваться без ошибок', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + + // Пытаемся скомпилировать + const { exitCode } = await execa('bun', ['build', generatedFile, '--outdir', tempDir]); + + expect(exitCode).toBe(0); + }, 30000); + + test.skip('должны отсутствовать TypeScript ошибки', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + + // Проверяем с помощью TypeScript компилятора + const { exitCode, stderr } = await execa('bun', ['build', generatedFile, '--outdir', tempDir]); + + expect(exitCode).toBe(0); + expect(stderr).toBe(''); + }, 30000); + + test('корректные импорты и экспорты', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем экспорты + expect(content).toContain('export'); + expect(content).toContain('class'); + }, 30000); + }); + + describe('корректность API', () => { + test('все endpoints из спецификации должны присутствовать', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем что все основные методы есть + expect(content).toContain('getAll'); + expect(content).toContain('create'); + expect(content).toContain('getById'); + expect(content).toContain('update'); + expect(content).toContain('delete'); + }, 30000); + + test('корректные имена методов (без Controller префиксов)', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем что "Controller" удален + expect(content).not.toContain('UserControllerGetAll'); + expect(content).not.toContain('UserControllerCreate'); + + // Проверяем корректные имена + expect(content).toContain('getAll'); + expect(content).toContain('create'); + }, 30000); + + test('HttpClient должен правильно инициализироваться', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие HttpClient + expect(content).toContain('HttpClient'); + + // Проверяем базовый URL + expect(content).toContain('https://api.example.com'); + }, 30000); + + test('метод setSecurityData должен работать', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.WITH_AUTH, + outputPath, + fileName: 'AuthApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'AuthApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие метода для установки токена + expect(content).toContain('setSecurityData'); + }, 30000); + }); + + describe('различные форматы спецификаций', () => { + test.skip('должен работать с минимальной спецификацией', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.MINIMAL, + outputPath, + fileName: 'MinimalApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'MinimalApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + + // Проверяем компиляцию (временно отключено из-за проблем с генератором) + // const { exitCode } = await execa('bun', ['build', generatedFile, '--outdir', tempDir]); + // expect(exitCode).toBe(0); + }, 30000); + + test('должен работать с аутентификацией', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.WITH_AUTH, + outputPath, + fileName: 'AuthApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'AuthApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие методов авторизации + expect(content).toContain('login'); + expect(content).toContain('get'); // ProfileController_get -> get + }, 30000); + + test('должен работать со сложной спецификацией', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.COMPLEX, + outputPath, + fileName: 'ComplexApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'ComplexApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + + // Проверяем что файл не пустой + const content = await readTextFile(generatedFile); + expect(content.length).toBeGreaterThan(1000); + }, 30000); + }); +}); \ No newline at end of file diff --git a/tests/unit/cli.test.ts b/tests/unit/cli.test.ts new file mode 100644 index 0000000..db4087b --- /dev/null +++ b/tests/unit/cli.test.ts @@ -0,0 +1,143 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { execa } from 'execa'; +import { setupTest } from '../helpers/setup.js'; +import { FIXTURES } from '../helpers/fixtures.js'; +import { join } from 'path'; +import { fileExists } 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('CLI', () => { + let tempDir: string; + let cleanup: () => Promise; + + beforeEach(async () => { + const setup = await setupTest(); + tempDir = setup.tempDir; + cleanup = setup.cleanup; + }); + + afterEach(async () => { + await cleanup(); + }); + + describe('базовые сценарии', () => { + 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'); + const customName = 'CustomApi'; + + const { exitCode } = await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.MINIMAL, + '--output', + outputPath, + '--name', + customName, + ]); + + expect(exitCode).toBe(0); + + const generatedFile = join(outputPath, `${customName}.ts`); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + }, 30000); + + test('должен отображать версию с --version', async () => { + const { stdout } = await execa('bun', ['run', CLI_PATH, '--version']); + + expect(stdout).toContain('1.0.0'); + }); + + test('должен отображать help с --help', async () => { + const { stdout } = await execa('bun', ['run', CLI_PATH, '--help']); + + expect(stdout).toContain('Generate TypeScript API client'); + expect(stdout).toContain('--input'); + expect(stdout).toContain('--output'); + }); + }); + + describe('обработка ошибок', () => { + test('должен выбросить ошибку без параметра --input', async () => { + const outputPath = join(tempDir, 'output'); + + try { + await execa('bun', ['run', CLI_PATH, '--output', outputPath]); + throw new Error('Should have thrown'); + } catch (error: any) { + expect(error.exitCode).not.toBe(0); + } + }); + + test('должен выбросить ошибку без параметра --output', async () => { + try { + await execa('bun', ['run', CLI_PATH, '--input', FIXTURES.MINIMAL]); + throw new Error('Should have thrown'); + } catch (error: any) { + expect(error.exitCode).not.toBe(0); + } + }); + + test('должен выбросить ошибку для несуществующего файла', async () => { + const outputPath = join(tempDir, 'output'); + const nonexistentFile = join(tempDir, 'nonexistent.json'); + + try { + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + nonexistentFile, + '--output', + outputPath, + ]); + throw new Error('Should have thrown'); + } catch (error: any) { + expect(error.exitCode).not.toBe(0); + } + }); + + test('должен выбросить ошибку для невалидного JSON', async () => { + const outputPath = join(tempDir, 'output'); + + try { + await execa('bun', [ + 'run', + CLI_PATH, + '--input', + FIXTURES.INVALID, + '--output', + outputPath, + ]); + throw new Error('Should have thrown'); + } catch (error: any) { + expect(error.exitCode).not.toBe(0); + } + }, 30000); + }); +}); \ No newline at end of file diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts new file mode 100644 index 0000000..6cf881c --- /dev/null +++ b/tests/unit/config.test.ts @@ -0,0 +1,49 @@ +import { describe, test, expect } from 'bun:test'; +import { validateConfig, type GeneratorConfig } from '../../src/config.js'; + +describe('config', () => { + describe('validateConfig', () => { + test('должен пройти валидацию для корректной конфигурации', () => { + const config: Partial = { + inputPath: './openapi.json', + outputPath: './output', + fileName: 'Api', + }; + + expect(() => validateConfig(config)).not.toThrow(); + expect(validateConfig(config)).toBe(true); + }); + + test('должен пройти валидацию без опционального fileName', () => { + const config: Partial = { + inputPath: './openapi.json', + outputPath: './output', + }; + + expect(() => validateConfig(config)).not.toThrow(); + expect(validateConfig(config)).toBe(true); + }); + + test('должен выбросить ошибку без inputPath', () => { + const config: Partial = { + outputPath: './output', + }; + + expect(() => validateConfig(config)).toThrow('Input path is required'); + }); + + test('должен выбросить ошибку без outputPath', () => { + const config: Partial = { + inputPath: './openapi.json', + }; + + expect(() => validateConfig(config)).toThrow('Output path is required'); + }); + + test('должен выбросить ошибку без обоих обязательных полей', () => { + const config: Partial = {}; + + expect(() => validateConfig(config)).toThrow('Configuration validation failed'); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/generator.test.ts b/tests/unit/generator.test.ts new file mode 100644 index 0000000..07c31e8 --- /dev/null +++ b/tests/unit/generator.test.ts @@ -0,0 +1,241 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { generate } from '../../src/generator.js'; +import { setupTest } from '../helpers/setup.js'; +import { FIXTURES } from '../helpers/fixtures.js'; +import { join } from 'path'; +import { fileExists, readTextFile } from '../../src/utils/file.js'; +import type { GeneratorConfig } from '../../src/config.js'; + +describe('Generator', () => { + let tempDir: string; + let cleanup: () => Promise; + + beforeEach(async () => { + const setup = await setupTest(); + tempDir = setup.tempDir; + cleanup = setup.cleanup; + }); + + afterEach(async () => { + await cleanup(); + }); + + describe('корректная генерация', () => { + test('должен создать выходной файл', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.MINIMAL, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + }, 30000); + + test('должен генерировать корректную структуру кода', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.MINIMAL, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие основных элементов + expect(content).toContain('export class'); + expect(content).toContain('HttpClient'); + }, 30000); + + test('должен обработать все HTTP методы', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем что методы UserController переименованы + // UserController_getAll -> getAll + // UserController_create -> create + expect(content).toContain('getAll'); + expect(content).toContain('create'); + expect(content).toContain('getById'); + expect(content).toContain('update'); + expect(content).toContain('delete'); + }, 30000); + + test('должен генерировать типы для request и response', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие типов + expect(content).toContain('User'); + expect(content).toContain('CreateUserDto'); + expect(content).toContain('UpdateUserDto'); + }, 30000); + + test('должен генерировать enum типы', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие enum + expect(content).toContain('UserRole'); + expect(content).toContain('admin'); + expect(content).toContain('user'); + expect(content).toContain('guest'); + }, 30000); + + test('должен обработать Bearer authentication', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.WITH_AUTH, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем наличие методов для работы с токеном + expect(content).toContain('setSecurityData'); + }, 30000); + + test('должен использовать baseUrl из servers', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем что baseUrl установлен + expect(content).toContain('https://api.example.com'); + }, 30000); + + test('должен применять хук onFormatRouteName', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.VALID, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем что "Controller" удален из имен методов + expect(content).not.toContain('UserControllerGetAll'); + expect(content).not.toContain('UserControllerCreate'); + + // Проверяем что методы названы корректно + expect(content).toContain('getAll'); + expect(content).toContain('create'); + }, 30000); + }); + + describe('edge cases', () => { + test('должен обработать пустую спецификацию', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.EMPTY, + outputPath, + fileName: 'TestApi', + }; + + // Генерация должна пройти успешно + await generate(config); + const generatedFile = join(outputPath, 'TestApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + }, 30000); + + test('должен обработать минимальную спецификацию', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.MINIMAL, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + }, 30000); + + test('должен обработать сложную спецификацию', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.COMPLEX, + outputPath, + fileName: 'TestApi', + }; + + await generate(config); + + const generatedFile = join(outputPath, 'TestApi.ts'); + const content = await readTextFile(generatedFile); + + // Проверяем что все контроллеры присутствуют + expect(content).toContain('list'); // ProductController_list -> list + expect(content).toContain('create'); + expect(content).toContain('getById'); + }, 30000); + + test('должен обработать Unicode символы', async () => { + const outputPath = join(tempDir, 'output'); + const config: GeneratorConfig = { + inputPath: FIXTURES.EDGE_CASES, + outputPath, + fileName: 'TestApi', + }; + + // Генерация должна пройти успешно даже с Unicode + await generate(config); + const generatedFile = join(outputPath, 'TestApi.ts'); + const exists = await fileExists(generatedFile); + expect(exists).toBe(true); + }, 30000); + }); +}); \ No newline at end of file diff --git a/tests/unit/utils/file.test.ts b/tests/unit/utils/file.test.ts new file mode 100644 index 0000000..e8aac50 --- /dev/null +++ b/tests/unit/utils/file.test.ts @@ -0,0 +1,111 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { fileExists, readJsonFile, readTextFile, ensureDir, writeFileWithDirs } from '../../../src/utils/file.js'; +import { setupTest } from '../../helpers/setup.js'; +import { join } from 'path'; +import { writeFile, mkdir } from 'fs/promises'; + +describe('file utils', () => { + let tempDir: string; + let cleanup: () => Promise; + + beforeEach(async () => { + const setup = await setupTest(); + tempDir = setup.tempDir; + cleanup = setup.cleanup; + }); + + afterEach(async () => { + await cleanup(); + }); + + describe('fileExists', () => { + test('должен вернуть true для существующего файла', async () => { + const filePath = join(tempDir, 'test.txt'); + await writeFile(filePath, 'test content'); + + const exists = await fileExists(filePath); + expect(exists).toBe(true); + }); + + test('должен вернуть false для несуществующего файла', async () => { + const filePath = join(tempDir, 'nonexistent.txt'); + + const exists = await fileExists(filePath); + expect(exists).toBe(false); + }); + }); + + describe('readTextFile', () => { + test('должен прочитать содержимое текстового файла', async () => { + const filePath = join(tempDir, 'test.txt'); + const content = 'Hello, World!'; + await writeFile(filePath, content); + + const result = await readTextFile(filePath); + expect(result).toBe(content); + }); + }); + + describe('readJsonFile', () => { + test('должен прочитать и распарсить JSON файл', async () => { + const filePath = join(tempDir, 'test.json'); + const data = { name: 'Test', value: 42 }; + await writeFile(filePath, JSON.stringify(data)); + + const result = await readJsonFile(filePath); + expect(result).toEqual(data); + }); + + test('должен выбросить ошибку для невалидного JSON', async () => { + const filePath = join(tempDir, 'invalid.json'); + await writeFile(filePath, 'not a json'); + + await expect(readJsonFile(filePath)).rejects.toThrow(); + }); + }); + + describe('ensureDir', () => { + test('должен создать директорию', async () => { + const dirPath = join(tempDir, 'test-dir'); + + await ensureDir(dirPath); + + const exists = await fileExists(dirPath); + expect(exists).toBe(true); + }); + + test('должен создать вложенные директории', async () => { + const dirPath = join(tempDir, 'a', 'b', 'c'); + + await ensureDir(dirPath); + + const exists = await fileExists(dirPath); + expect(exists).toBe(true); + }); + + test('не должен падать если директория уже существует', async () => { + const dirPath = join(tempDir, 'existing'); + await mkdir(dirPath); + + // Не должно выбрасывать ошибку + await ensureDir(dirPath); + const exists = await fileExists(dirPath); + expect(exists).toBe(true); + }); + }); + + describe('writeFileWithDirs', () => { + test('должен записать файл и создать директории', async () => { + const filePath = join(tempDir, 'nested', 'dir', 'file.txt'); + const content = 'test content'; + + await writeFileWithDirs(filePath, content); + + const exists = await fileExists(filePath); + expect(exists).toBe(true); + + const readContent = await readTextFile(filePath); + expect(readContent).toBe(content); + }); + }); +}); \ No newline at end of file