✨ Features: - Modern tourism website with responsive design - AdminJS admin panel with image editor integration - PostgreSQL database with comprehensive schema - Docker containerization - Image upload and gallery management 🛠 Tech Stack: - Backend: Node.js + Express.js - Database: PostgreSQL 13+ - Frontend: HTML/CSS/JS with responsive design - Admin: AdminJS with custom components - Deployment: Docker + Docker Compose - Image Processing: Sharp with optimization 📱 Admin Features: - Routes/Tours management (city, mountain, fishing) - Guides profiles with specializations - Articles and blog system - Image editor with upload/gallery/URL options - User management and authentication - Responsive admin interface 🎨 Design: - Korean tourism focused branding - Mobile-first responsive design - Custom CSS with modern aesthetics - Image optimization and gallery - SEO-friendly structure 🔒 Security: - Helmet.js security headers - bcrypt password hashing - Input validation and sanitization - CORS protection - Environment variables
370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
#!/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 }; |