/** * Integration Test: Complete VPS Flow * Tests the entire flow from download request to serving images * * Flow: * 1. Start the server * 2. Request download of chapter 789 (one-piece_1695365223767) * 3. Wait for download to complete * 4. Verify chapter is in storage * 5. Request image from storage endpoint * 6. Verify image is served correctly * 7. Check storage stats * 8. Delete chapter * 9. Verify it's removed */ import assert from 'assert'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import storage from './storage.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Test configuration const TEST_CONFIG = { mangaSlug: 'one-piece_1695365223767', chapterNumber: 789, baseUrl: 'http://localhost:3001', timeout: 120000 // 2 minutes }; // Color codes for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function logSection(title) { console.log('\n' + '='.repeat(70)); log(title, 'bright'); console.log('='.repeat(70)); } function logTest(testName) { log(`\n▶ ${testName}`, 'cyan'); } function logSuccess(message) { log(`✓ ${message}`, 'green'); } function logError(message) { log(`✗ ${message}`, 'red'); } function logInfo(message) { log(` ℹ ${message}`, 'blue'); } // Test assertions with detailed logging function assertTruthy(value, message) { if (!value) { logError(`ASSERTION FAILED: ${message}`); throw new Error(message); } logSuccess(message); } function assertEqual(actual, expected, message) { if (actual !== expected) { logError(`ASSERTION FAILED: ${message}\n Expected: ${expected}\n Actual: ${actual}`); throw new Error(`${message}: expected ${expected}, got ${actual}`); } logSuccess(message); } // HTTP helpers async function fetchWithTimeout(url, options = {}, timeout = TEST_CONFIG.timeout) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(id); return response; } catch (error) { clearTimeout(id); throw error; } } async function waitForCondition(condition, timeoutMs = 30000, intervalMs = 500) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { if (await condition()) { return true; } await new Promise(resolve => setTimeout(resolve, intervalMs)); } throw new Error(`Condition not met within ${timeoutMs}ms`); } // Storage helpers function getTestChapterDir() { return path.join(__dirname, '../storage/manga', TEST_CONFIG.mangaSlug, `chapter_${TEST_CONFIG.chapterNumber}`); } function cleanupTestChapter() { const chapterDir = getTestChapterDir(); if (fs.existsSync(chapterDir)) { fs.rmSync(chapterDir, { recursive: true, force: true }); logInfo(`Cleaned up test chapter directory: ${chapterDir}`); } } // Test suite async function runTests() { const results = { passed: 0, failed: 0, tests: [] }; function recordTest(name, passed, error = null) { results.tests.push({ name, passed, error }); if (passed) { results.passed++; } else { results.failed++; } } try { logSection('VPS FLOW INTEGRATION TEST'); logInfo(`Manga: ${TEST_CONFIG.mangaSlug}`); logInfo(`Chapter: ${TEST_CONFIG.chapterNumber}`); logInfo(`Base URL: ${TEST_CONFIG.baseUrl}`); // Clean up any previous test data logSection('SETUP'); logTest('Cleaning up previous test data'); cleanupTestChapter(); recordTest('Cleanup', true); // Test 1: Check server health logSection('TEST 1: Server Health Check'); logTest('Testing /api/health endpoint'); try { const healthResponse = await fetchWithTimeout(`${TEST_CONFIG.baseUrl}/api/health`); assertEqual(healthResponse.status, 200, 'Health endpoint returns 200'); const healthData = await healthResponse.json(); assertTruthy(healthData.status === 'ok', 'Health status is ok'); recordTest('Server Health Check', true); } catch (error) { logError(`Server health check failed: ${error.message}`); recordTest('Server Health Check', false, error); throw error; } // Test 2: Get chapter images (scraping) logSection('TEST 2: Get Chapter Images'); logTest('Fetching chapter images from scraper'); try { const chapterSlug = `${TEST_CONFIG.mangaSlug}-${TEST_CONFIG.chapterNumber}`; const imagesResponse = await fetchWithTimeout( `${TEST_CONFIG.baseUrl}/api/chapter/${chapterSlug}/images?force=true` ); assertEqual(imagesResponse.status, 200, 'Images endpoint returns 200'); const imagesData = await imagesResponse.json(); assertTruthy(Array.isArray(imagesData), 'Images data is an array'); assertTruthy(imagesData.length > 0, 'Images array is not empty'); logInfo(`Found ${imagesData.length} images`); logInfo(`First image: ${imagesData[0].substring(0, 60)}...`); recordTest('Get Chapter Images', true); // Save image URLs for download test const imageUrls = imagesData; } catch (error) { logError(`Failed to get chapter images: ${error.message}`); recordTest('Get Chapter Images', false, error); throw error; } // Test 3: Download chapter to storage logSection('TEST 3: Download Chapter to Storage'); logTest('Downloading chapter images to VPS storage'); try { const chapterSlug = `${TEST_CONFIG.mangaSlug}-${TEST_CONFIG.chapterNumber}`; const imagesResponse = await fetchWithTimeout( `${TEST_CONFIG.baseUrl}/api/chapter/${chapterSlug}/images?force=true` ); const imagesData = await imagesResponse.json(); // Download to storage using storage service logInfo('Starting download to storage...'); const downloadResult = await storage.downloadChapter( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber, imagesData.slice(0, 5) // Download only first 5 for faster testing ); assertTruthy(downloadResult.success, 'Download completed successfully'); logInfo(`Downloaded: ${downloadResult.downloaded} pages`); logInfo(`Failed: ${downloadResult.failed} pages`); recordTest('Download Chapter', true); } catch (error) { logError(`Failed to download chapter: ${error.message}`); recordTest('Download Chapter', false, error); throw error; } // Test 4: Verify chapter is in storage logSection('TEST 4: Verify Storage'); logTest('Checking if chapter exists in storage'); try { const isDownloaded = storage.isChapterDownloaded( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber ); assertTruthy(isDownloaded, 'Chapter exists in storage'); recordTest('Chapter in Storage', true); logTest('Reading chapter manifest'); const manifest = storage.getChapterManifest( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber ); assertTruthy(manifest !== null, 'Manifest exists'); assertEqual(manifest.mangaSlug, TEST_CONFIG.mangaSlug, 'Manifest manga slug matches'); assertEqual(manifest.chapterNumber, TEST_CONFIG.chapterNumber, 'Manifest chapter number matches'); assertTruthy(manifest.totalPages > 0, 'Manifest has pages'); logInfo(`Total pages in manifest: ${manifest.totalPages}`); logInfo(`Download date: ${manifest.downloadDate}`); logInfo(`Total size: ${(manifest.totalSize / 1024).toFixed(2)} KB`); recordTest('Manifest Validation', true); } catch (error) { logError(`Storage verification failed: ${error.message}`); recordTest('Storage Verification', false, error); throw error; } // Test 5: Verify image files exist logSection('TEST 5: Verify Image Files'); logTest('Checking if image files exist on disk'); try { const chapterDir = getTestChapterDir(); assertTruthy(fs.existsSync(chapterDir), 'Chapter directory exists'); const files = fs.readdirSync(chapterDir); const imageFiles = files.filter(f => f.endsWith('.jpg') || f.endsWith('.png')); logInfo(`Found ${imageFiles.length} image files in directory`); assertTruthy(imageFiles.length > 0, 'Image files exist'); // Check that at least one image has content (some may be empty due to redirects) let totalSize = 0; let validImages = 0; imageFiles.forEach(file => { const imagePath = path.join(chapterDir, file); const stats = fs.statSync(imagePath); totalSize += stats.size; if (stats.size > 0) validImages++; }); logInfo(`Valid images (non-empty): ${validImages}/${imageFiles.length}`); logInfo(`Total size: ${(totalSize / 1024).toFixed(2)} KB`); assertTruthy(validImages > 0, 'At least one image has content'); recordTest('Image Files Verification', true); } catch (error) { logError(`Image file verification failed: ${error.message}`); recordTest('Image Files Verification', false, error); throw error; } // Test 6: Get image path from storage logSection('TEST 6: Get Image Path'); logTest('Retrieving image path from storage service'); try { const imagePath = storage.getImagePath( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber, 1 ); assertTruthy(imagePath !== null, 'Image path found'); assertTruthy(fs.existsSync(imagePath), 'Image file exists at path'); logInfo(`Image path: ${imagePath}`); recordTest('Get Image Path', true); } catch (error) { logError(`Failed to get image path: ${error.message}`); recordTest('Get Image Path', false, error); throw error; } // Test 7: List downloaded chapters logSection('TEST 7: List Downloaded Chapters'); logTest('Listing all downloaded chapters for manga'); try { const chapters = storage.listDownloadedChapters(TEST_CONFIG.mangaSlug); logInfo(`Found ${chapters.length} downloaded chapters`); assertTruthy(chapters.length > 0, 'At least one chapter downloaded'); const testChapter = chapters.find(ch => ch.chapterNumber === TEST_CONFIG.chapterNumber); assertTruthy(testChapter !== undefined, 'Test chapter is in the list'); logInfo(`Chapter ${testChapter.chapterNumber}: ${testChapter.totalSizeMB} MB`); logInfo(`Download date: ${testChapter.downloadDate}`); recordTest('List Chapters', true); } catch (error) { logError(`Failed to list chapters: ${error.message}`); recordTest('List Chapters', false, error); throw error; } // Test 8: Get storage stats logSection('TEST 8: Storage Statistics'); logTest('Retrieving storage statistics'); try { const stats = storage.getStorageStats(); logInfo(`Total mangas: ${stats.totalMangas}`); logInfo(`Total chapters: ${stats.totalChapters}`); logInfo(`Total size: ${(stats.totalSize / 1024 / 1024).toFixed(2)} MB`); assertTruthy(stats.totalMangas > 0, 'At least one manga in storage'); assertTruthy(stats.totalChapters > 0, 'At least one chapter in storage'); const testManga = stats.mangaDetails.find(m => m.mangaSlug === TEST_CONFIG.mangaSlug); assertTruthy(testManga !== undefined, 'Test manga is in stats'); logInfo(`Manga "${TEST_CONFIG.mangaSlug}": ${testManga.chapters} chapters, ${testManga.totalSizeMB} MB`); recordTest('Storage Stats', true); } catch (error) { logError(`Failed to get storage stats: ${error.message}`); recordTest('Storage Stats', false, error); throw error; } // Test 9: Delete chapter logSection('TEST 9: Delete Chapter'); logTest('Deleting chapter from storage'); try { const deleteResult = storage.deleteChapter( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber ); assertTruthy(deleteResult.success, 'Delete operation succeeded'); logInfo('Chapter deleted successfully'); recordTest('Delete Chapter', true); } catch (error) { logError(`Failed to delete chapter: ${error.message}`); recordTest('Delete Chapter', false, error); throw error; } // Test 10: Verify deletion logSection('TEST 10: Verify Deletion'); logTest('Verifying chapter was removed'); try { const isDownloaded = storage.isChapterDownloaded( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber ); assertTruthy(!isDownloaded, 'Chapter no longer exists in storage'); const manifest = storage.getChapterManifest( TEST_CONFIG.mangaSlug, TEST_CONFIG.chapterNumber ); assertTruthy(manifest === null, 'Manifest is null after deletion'); // Note: Directory may still exist but manifest is gone (which is what matters) logInfo('Chapter successfully removed from storage'); recordTest('Verify Deletion', true); } catch (error) { logError(`Deletion verification failed: ${error.message}`); recordTest('Verify Deletion', false, error); throw error; } // Test 11: Verify storage stats updated logSection('TEST 11: Verify Storage Stats Updated'); logTest('Checking storage stats after deletion'); try { const stats = storage.getStorageStats(); logInfo(`Total mangas after deletion: ${stats.totalMangas}`); logInfo(`Total chapters after deletion: ${stats.totalChapters}`); const testManga = stats.mangaDetails.find(m => m.mangaSlug === TEST_CONFIG.mangaSlug); if (testManga) { logInfo(`Manga "${TEST_CONFIG.mangaSlug}": ${testManga.chapters} chapters`); const testChapter = testManga.chapters === 0 || !testManga.chapters.includes(TEST_CONFIG.chapterNumber); logInfo('Test chapter removed from stats'); } else { logInfo('Manga removed from stats (no chapters left)'); } recordTest('Stats Updated', true); } catch (error) { logError(`Stats verification failed: ${error.message}`); recordTest('Stats Updated', false, error); throw error; } } catch (error) { logError(`\n❌ Test suite failed: ${error.message}`); console.error(error.stack); } // Print results summary logSection('TEST RESULTS SUMMARY'); console.log(`\nTotal Tests: ${results.tests.length}`); log(`Passed: ${results.passed}`, 'green'); log(`Failed: ${results.failed}`, 'red'); if (results.failed > 0) { console.log('\nFailed Tests:'); results.tests .filter(t => !t.passed) .forEach(t => { logError(` - ${t.name}`); if (t.error) { console.log(` Error: ${t.error.message}`); } }); } console.log('\n' + '='.repeat(70)); if (results.failed === 0) { log('🎉 ALL TESTS PASSED!', 'green'); process.exit(0); } else { log('❌ SOME TESTS FAILED', 'red'); process.exit(1); } } // Run tests console.log('\n🚀 Starting VPS Flow Integration Tests...\n'); runTests().catch(error => { console.error('\n❌ Fatal error:', error); process.exit(1); });