🚀 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:
2025-11-30 00:53:15 +09:00
parent ed871fc4d1
commit b4e513e996
36 changed files with 6894 additions and 239 deletions

370
test-crud.js Normal file
View 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 };