Files
tourrism_site/test-crud.js
Andrey K. Choi b4e513e996 🚀 Korea Tourism Agency - Complete implementation
 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
2025-11-30 00:53:15 +09:00

370 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

#!/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 };