#!/usr/bin/env node /** * Скрипт для тестирования всех CRUD операций * Проверяет создание, чтение, обновление и удаление для Routes, Guides и Articles */ const BASE_URL = 'http://localhost:3000/api/crud'; // Цветной вывод в консоль const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m' }; const log = (color, message) => { console.log(`${colors[color]}${message}${colors.reset}`); }; // Утилита для HTTP запросов async function request(method, url, data = null) { const options = { method, headers: { 'Content-Type': 'application/json', } }; if (data) { options.body = JSON.stringify(data); } const response = await fetch(url, options); const result = await response.json(); return { status: response.status, data: result }; } // Тестовые данные const testData = { route: { title: 'Тестовый маршрут CRUD', description: 'Описание тестового маршрута для проверки CRUD операций', content: 'Подробное описание маршрута с инструкциями', type: 'city', difficulty_level: 'easy', price: 25000, duration: 4, max_group_size: 15, image_url: '/uploads/routes/seoul-city-tour.jpg', is_featured: false, is_active: true }, guide: { name: 'Тестовый Гид CRUD', email: 'test-guide-crud@example.com', phone: '+82-10-1234-5678', languages: 'Корейский, Английский, Русский', specialization: 'city', bio: 'Опытный гид для тестирования CRUD операций', experience: 3, image_url: '/uploads/guides/guide-profile.jpg', hourly_rate: 30000, is_active: true }, article: { title: 'Тестовая статья CRUD', excerpt: 'Краткое описание тестовой статьи', content: 'Полный текст тестовой статьи для проверки CRUD операций', category: 'travel-tips', image_url: '/images/articles/test-article.jpg', author_id: 1, is_published: true } }; // Основная функция тестирования async function runCRUDTests() { log('cyan', '🚀 Запуск тестирования CRUD операций...\n'); const results = { routes: await testEntityCRUD('routes', testData.route), guides: await testEntityCRUD('guides', testData.guide), articles: await testEntityCRUD('articles', testData.article) }; // Тестируем общие эндпоинты log('blue', '📊 Тестирование общей статистики...'); try { const statsResponse = await request('GET', `${BASE_URL}/stats`); if (statsResponse.status === 200 && statsResponse.data.success) { log('green', `✅ Статистика: ${JSON.stringify(statsResponse.data.data)}`); } else { log('red', `❌ Ошибка получения статистики: ${JSON.stringify(statsResponse.data)}`); } } catch (error) { log('red', `❌ Ошибка получения статистики: ${error.message}`); } // Итоговый отчет log('cyan', '\n📋 Итоговый отчет тестирования:'); Object.entries(results).forEach(([entity, result]) => { const status = result.success ? '✅' : '❌'; log(result.success ? 'green' : 'red', `${status} ${entity.toUpperCase()}: ${result.message}`); }); const totalTests = Object.values(results).length; const passedTests = Object.values(results).filter(r => r.success).length; log('cyan', `\n🎯 Результат: ${passedTests}/${totalTests} тестов прошли успешно`); if (passedTests === totalTests) { log('green', '🎉 Все CRUD операции работают корректно!'); } else { log('red', '⚠️ Некоторые операции требуют внимания'); } } // Тестирование CRUD для конкретной сущности async function testEntityCRUD(entity, testData) { log('magenta', `\n🔍 Тестирование ${entity.toUpperCase()}...`); let createdId = null; const steps = []; try { // 1. CREATE - Создание записи log('yellow', '1. CREATE - Создание записи...'); const createResponse = await request('POST', `${BASE_URL}/${entity}`, testData); if (createResponse.status === 201 && createResponse.data.success) { createdId = createResponse.data.data.id; log('green', `✅ Создание успешно. ID: ${createdId}`); steps.push('CREATE: ✅'); } else { log('red', `❌ Ошибка создания: ${JSON.stringify(createResponse.data)}`); steps.push('CREATE: ❌'); return { success: false, message: 'Ошибка создания записи', steps }; } // 2. READ - Чтение записи по ID log('yellow', '2. READ - Чтение записи по ID...'); const readResponse = await request('GET', `${BASE_URL}/${entity}/${createdId}`); if (readResponse.status === 200 && readResponse.data.success) { log('green', `✅ Чтение успешно. Заголовок: "${readResponse.data.data.title || readResponse.data.data.name}"`); steps.push('READ: ✅'); } else { log('red', `❌ Ошибка чтения: ${JSON.stringify(readResponse.data)}`); steps.push('READ: ❌'); } // 3. READ ALL - Чтение всех записей log('yellow', '3. READ ALL - Чтение всех записей...'); const readAllResponse = await request('GET', `${BASE_URL}/${entity}?page=1&limit=5`); if (readAllResponse.status === 200 && readAllResponse.data.success) { const count = readAllResponse.data.data.length; log('green', `✅ Получено записей: ${count}`); steps.push('READ ALL: ✅'); } else { log('red', `❌ Ошибка чтения списка: ${JSON.stringify(readAllResponse.data)}`); steps.push('READ ALL: ❌'); } // 4. UPDATE - Обновление записи log('yellow', '4. UPDATE - Обновление записи...'); const updateData = entity === 'guides' ? { name: testData.name + ' (ОБНОВЛЕНО)' } : { title: testData.title + ' (ОБНОВЛЕНО)' }; const updateResponse = await request('PUT', `${BASE_URL}/${entity}/${createdId}`, updateData); if (updateResponse.status === 200 && updateResponse.data.success) { log('green', '✅ Обновление успешно'); steps.push('UPDATE: ✅'); } else { log('red', `❌ Ошибка обновления: ${JSON.stringify(updateResponse.data)}`); steps.push('UPDATE: ❌'); } // 5. DELETE - Удаление записи log('yellow', '5. DELETE - Удаление записи...'); const deleteResponse = await request('DELETE', `${BASE_URL}/${entity}/${createdId}`); if (deleteResponse.status === 200 && deleteResponse.data.success) { log('green', `✅ Удаление успешно: ${deleteResponse.data.message}`); steps.push('DELETE: ✅'); } else { log('red', `❌ Ошибка удаления: ${JSON.stringify(deleteResponse.data)}`); steps.push('DELETE: ❌'); } // 6. Проверка удаления log('yellow', '6. VERIFY DELETE - Проверка удаления...'); const verifyResponse = await request('GET', `${BASE_URL}/${entity}/${createdId}`); if (verifyResponse.status === 404) { log('green', '✅ Запись действительно удалена'); steps.push('VERIFY: ✅'); } else { log('red', '❌ Запись не была удалена'); steps.push('VERIFY: ❌'); } const successCount = steps.filter(s => s.includes('✅')).length; const isSuccess = successCount === steps.length; return { success: isSuccess, message: `${successCount}/6 операций успешно`, steps }; } catch (error) { log('red', `❌ Критическая ошибка: ${error.message}`); return { success: false, message: `Критическая ошибка: ${error.message}`, steps: [...steps, 'ERROR: ❌'] }; } } // Дополнительные тесты для специфических функций async function testAdvancedFeatures() { log('cyan', '\n🔬 Тестирование расширенных функций...'); // Тест поиска log('yellow', 'Тест поиска по routes...'); try { const searchResponse = await request('GET', `${BASE_URL}/routes?search=seoul&limit=3`); if (searchResponse.status === 200) { log('green', `✅ Поиск работает. Найдено: ${searchResponse.data.data.length} записей`); } else { log('red', '❌ Ошибка поиска'); } } catch (error) { log('red', `❌ Ошибка поиска: ${error.message}`); } // Тест фильтрации log('yellow', 'Тест фильтрации guides по специализации...'); try { const filterResponse = await request('GET', `${BASE_URL}/guides?specialization=city&limit=3`); if (filterResponse.status === 200) { log('green', `✅ Фильтрация работает. Найдено: ${filterResponse.data.data.length} гидов`); } else { log('red', '❌ Ошибка фильтрации'); } } catch (error) { log('red', `❌ Ошибка фильтрации: ${error.message}`); } // Тест пагинации log('yellow', 'Тест пагинации для articles...'); try { const paginationResponse = await request('GET', `${BASE_URL}/articles?page=1&limit=2`); if (paginationResponse.status === 200) { const pagination = paginationResponse.data.pagination; log('green', `✅ Пагинация работает. Страница ${pagination.page}, всего ${pagination.total} записей`); } else { log('red', '❌ Ошибка пагинации'); } } catch (error) { log('red', `❌ Ошибка пагинации: ${error.message}`); } } // Тест валидации async function testValidation() { log('cyan', '\n🛡️ Тестирование валидации...'); // Тест создания без обязательных полей log('yellow', 'Тест создания маршрута без обязательных полей...'); try { const invalidResponse = await request('POST', `${BASE_URL}/routes`, { description: 'Только описание, без заголовка' }); if (invalidResponse.status === 400) { log('green', '✅ Валидация работает - отклонены невалидные данные'); } else { log('red', '❌ Валидация не работает - приняты невалидные данные'); } } catch (error) { log('red', `❌ Ошибка тестирования валидации: ${error.message}`); } // Тест обновления несуществующей записи log('yellow', 'Тест обновления несуществующей записи...'); try { const notFoundResponse = await request('PUT', `${BASE_URL}/guides/99999`, { name: 'Не существует' }); if (notFoundResponse.status === 404) { log('green', '✅ Корректно обрабатывается отсутствующая запись'); } else { log('red', '❌ Неправильная обработка отсутствующей записи'); } } catch (error) { log('red', `❌ Ошибка тестирования несуществующей записи: ${error.message}`); } } // Запуск всех тестов async function runAllTests() { try { await runCRUDTests(); await testAdvancedFeatures(); await testValidation(); log('cyan', '\n🏁 Тестирование завершено!'); } catch (error) { log('red', `💥 Критическая ошибка тестирования: ${error.message}`); process.exit(1); } } // Проверка доступности сервера async function checkServer() { try { const response = await fetch('http://localhost:3000/health'); if (response.ok) { log('green', '✅ Сервер доступен'); return true; } else { log('red', '❌ Сервер недоступен'); return false; } } catch (error) { log('red', `❌ Сервер недоступен: ${error.message}`); return false; } } // Главная функция async function main() { log('cyan', '🧪 Система тестирования CRUD API'); log('cyan', '====================================\n'); // Проверяем доступность сервера const serverAvailable = await checkServer(); if (!serverAvailable) { log('red', 'Запустите сервер командой: docker-compose up -d'); process.exit(1); } // Запускаем тесты await runAllTests(); } // Запуск если файл выполняется напрямую if (import.meta.url === `file://${process.argv[1]}`) { main().catch(error => { log('red', `💥 Неожиданная ошибка: ${error.message}`); process.exit(1); }); } export { runCRUDTests, testAdvancedFeatures, testValidation };