🚀 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
This commit is contained in:
370
test-crud.js
Normal file
370
test-crud.js
Normal file
@@ -0,0 +1,370 @@
|
||||
#!/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 };
|
||||
Reference in New Issue
Block a user