/** * @fileoverview Integration Tests for Complete Download Workflow * @author GrabZilla Development Team * @version 2.1.0 * @since 2024-01-01 */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { spawn } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; /** * INTEGRATION TESTS FOR COMPLETE DOWNLOAD WORKFLOW WITH BINARIES * * These tests verify the end-to-end download process including: * - Binary availability and execution * - Video metadata extraction * - Actual download process * - Format conversion workflow * - Progress tracking and status updates * - Error handling and recovery */ describe('Complete Download Workflow Integration', () => { let testDownloadDir; let mockBinaryPaths; beforeEach(() => { // Create temporary download directory for tests testDownloadDir = path.join(os.tmpdir(), 'grabzilla-test-' + Date.now()); if (!fs.existsSync(testDownloadDir)) { fs.mkdirSync(testDownloadDir, { recursive: true }); } // Set up platform-specific binary paths const isWindows = process.platform === 'win32'; mockBinaryPaths = { ytDlp: path.join(process.cwd(), 'binaries', `yt-dlp${isWindows ? '.exe' : ''}`), ffmpeg: path.join(process.cwd(), 'binaries', `ffmpeg${isWindows ? '.exe' : ''}`) }; }); afterEach(() => { // Clean up test download directory if (fs.existsSync(testDownloadDir)) { try { fs.rmSync(testDownloadDir, { recursive: true, force: true }); } catch (error) { console.warn('Failed to clean up test directory:', error.message); } } }); describe('Binary Availability and Version Checking', () => { it('should verify yt-dlp binary exists and is executable', async () => { const binaryExists = fs.existsSync(mockBinaryPaths.ytDlp); if (binaryExists) { // Test binary execution const versionCheck = await new Promise((resolve, reject) => { const process = spawn(mockBinaryPaths.ytDlp, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; process.stdout.on('data', (data) => { output += data.toString(); }); process.on('close', (code) => { if (code === 0) { resolve(output.trim()); } else { reject(new Error(`yt-dlp version check failed with code ${code}`)); } }); process.on('error', reject); }); expect(versionCheck).toMatch(/^\d{4}\.\d{2}\.\d{2}/); } else { console.warn('yt-dlp binary not found, skipping execution test'); expect(binaryExists).toBe(false); // Document the missing binary } }); it('should verify ffmpeg binary exists and is executable', async () => { const binaryExists = fs.existsSync(mockBinaryPaths.ffmpeg); if (binaryExists) { // Test binary execution const versionCheck = await new Promise((resolve, reject) => { const process = spawn(mockBinaryPaths.ffmpeg, ['-version'], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; process.stdout.on('data', (data) => { output += data.toString(); }); process.on('close', (code) => { if (code === 0) { resolve(output.trim()); } else { reject(new Error(`ffmpeg version check failed with code ${code}`)); } }); process.on('error', reject); }); expect(versionCheck).toMatch(/ffmpeg version/i); } else { console.warn('ffmpeg binary not found, skipping execution test'); expect(binaryExists).toBe(false); // Document the missing binary } }); it('should handle missing binaries gracefully', () => { const nonExistentPath = path.join(process.cwd(), 'binaries', 'nonexistent-binary'); expect(() => { if (!fs.existsSync(nonExistentPath)) { throw new Error('Binary not found'); } }).toThrow('Binary not found'); }); }); describe('Video Metadata Extraction', () => { it('should extract metadata from YouTube URL using yt-dlp', async () => { const binaryExists = fs.existsSync(mockBinaryPaths.ytDlp); if (!binaryExists) { console.warn('yt-dlp binary not found, skipping metadata test'); return; } // Use a known stable YouTube video for testing const testUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; const metadata = await new Promise((resolve, reject) => { const process = spawn(mockBinaryPaths.ytDlp, [ '--dump-json', '--no-download', testUrl ], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; let errorOutput = ''; process.stdout.on('data', (data) => { output += data.toString(); }); process.stderr.on('data', (data) => { errorOutput += data.toString(); }); process.on('close', (code) => { if (code === 0 && output.trim()) { try { const metadata = JSON.parse(output.trim()); resolve(metadata); } catch (parseError) { reject(new Error(`Failed to parse metadata: ${parseError.message}`)); } } else { reject(new Error(`Metadata extraction failed: ${errorOutput || 'Unknown error'}`)); } }); process.on('error', reject); }); expect(metadata).toHaveProperty('title'); expect(metadata).toHaveProperty('duration'); expect(metadata).toHaveProperty('thumbnail'); expect(metadata.title).toBeTruthy(); }, 30000); // 30 second timeout for network operations it('should handle invalid URLs gracefully', async () => { const binaryExists = fs.existsSync(mockBinaryPaths.ytDlp); if (!binaryExists) { console.warn('yt-dlp binary not found, skipping invalid URL test'); return; } const invalidUrl = 'https://example.com/not-a-video'; await expect(async () => { await new Promise((resolve, reject) => { const process = spawn(mockBinaryPaths.ytDlp, [ '--dump-json', '--no-download', invalidUrl ], { stdio: ['pipe', 'pipe', 'pipe'] }); let errorOutput = ''; process.stderr.on('data', (data) => { errorOutput += data.toString(); }); process.on('close', (code) => { if (code !== 0) { reject(new Error(`Invalid URL: ${errorOutput}`)); } else { resolve(); } }); process.on('error', reject); }); }).rejects.toThrow(); }); }); describe('Download Process Integration', () => { it('should simulate download workflow with progress tracking', async () => { // Mock download process simulation const downloadOptions = { url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', quality: '720p', format: 'mp4', savePath: testDownloadDir }; const progressUpdates = []; const mockProgressCallback = (data) => { progressUpdates.push(data); }; // Simulate download stages mockProgressCallback({ stage: 'metadata', progress: 10, status: 'fetching metadata' }); mockProgressCallback({ stage: 'download', progress: 35, status: 'downloading' }); mockProgressCallback({ stage: 'download', progress: 70, status: 'downloading' }); mockProgressCallback({ stage: 'complete', progress: 100, status: 'completed' }); expect(progressUpdates).toHaveLength(4); expect(progressUpdates[0].stage).toBe('metadata'); expect(progressUpdates[1].stage).toBe('download'); expect(progressUpdates[3].stage).toBe('complete'); expect(progressUpdates[3].progress).toBe(100); }); it('should handle download cancellation', () => { const mockProcess = { pid: 12345, kill: vi.fn(), killed: false }; // Simulate cancellation mockProcess.kill('SIGTERM'); mockProcess.killed = true; expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM'); expect(mockProcess.killed).toBe(true); }); it('should handle download errors and retry logic', async () => { const maxRetries = 3; let attemptCount = 0; const mockDownload = async () => { attemptCount++; if (attemptCount < maxRetries) { throw new Error('Network error'); } return { success: true, filename: 'test.mp4' }; }; // Simulate retry logic let result; let lastError; for (let i = 0; i < maxRetries; i++) { try { result = await mockDownload(); break; } catch (error) { lastError = error; if (i === maxRetries - 1) { throw error; } } } expect(attemptCount).toBe(maxRetries); expect(result.success).toBe(true); }); }); describe('Format Conversion Integration', () => { it('should simulate format conversion workflow', async () => { const conversionOptions = { inputPath: path.join(testDownloadDir, 'input.mp4'), outputPath: path.join(testDownloadDir, 'output_h264.mp4'), format: 'H264', quality: '1080p' }; // Create mock input file fs.writeFileSync(conversionOptions.inputPath, 'mock video data'); const progressUpdates = []; const mockProgressCallback = (data) => { progressUpdates.push(data); }; // Simulate conversion stages mockProgressCallback({ stage: 'conversion', progress: 25, status: 'converting' }); mockProgressCallback({ stage: 'conversion', progress: 50, status: 'converting' }); mockProgressCallback({ stage: 'conversion', progress: 75, status: 'converting' }); mockProgressCallback({ stage: 'conversion', progress: 100, status: 'completed' }); // Simulate output file creation fs.writeFileSync(conversionOptions.outputPath, 'mock converted video data'); expect(progressUpdates).toHaveLength(4); expect(progressUpdates.every(update => update.stage === 'conversion')).toBe(true); expect(progressUpdates[3].progress).toBe(100); expect(fs.existsSync(conversionOptions.outputPath)).toBe(true); }); it('should handle conversion errors', () => { const conversionError = new Error('FFmpeg conversion failed: Invalid codec'); expect(() => { throw conversionError; }).toThrow('FFmpeg conversion failed: Invalid codec'); }); }); describe('End-to-End Workflow Simulation', () => { it('should complete full download and conversion workflow', async () => { const workflowSteps = []; const mockWorkflow = { async validateUrl(url) { workflowSteps.push('url_validation'); return { valid: true, platform: 'youtube' }; }, async extractMetadata(url) { workflowSteps.push('metadata_extraction'); return { title: 'Test Video', duration: '00:03:30', thumbnail: 'https://example.com/thumb.jpg' }; }, async downloadVideo(options) { workflowSteps.push('video_download'); return { success: true, filename: 'test_video.mp4', filePath: path.join(testDownloadDir, 'test_video.mp4') }; }, async convertVideo(options) { workflowSteps.push('video_conversion'); return { success: true, outputPath: path.join(testDownloadDir, 'test_video_h264.mp4') }; } }; // Execute full workflow const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; const validation = await mockWorkflow.validateUrl(url); expect(validation.valid).toBe(true); const metadata = await mockWorkflow.extractMetadata(url); expect(metadata.title).toBeTruthy(); const downloadResult = await mockWorkflow.downloadVideo({ url, quality: '720p', savePath: testDownloadDir }); expect(downloadResult.success).toBe(true); const conversionResult = await mockWorkflow.convertVideo({ inputPath: downloadResult.filePath, format: 'H264' }); expect(conversionResult.success).toBe(true); expect(workflowSteps).toEqual([ 'url_validation', 'metadata_extraction', 'video_download', 'video_conversion' ]); }); it('should handle workflow interruption and cleanup', () => { const activeProcesses = [ { pid: 12345, type: 'download' }, { pid: 12346, type: 'conversion' } ]; const cleanup = () => { activeProcesses.forEach(proc => { // Simulate process termination proc.killed = true; }); return activeProcesses.length; }; const cleanedCount = cleanup(); expect(cleanedCount).toBe(2); expect(activeProcesses.every(proc => proc.killed)).toBe(true); }); }); describe('Performance and Resource Management', () => { it('should handle concurrent downloads efficiently', async () => { const maxConcurrentDownloads = 3; const downloadQueue = [ 'https://www.youtube.com/watch?v=video1', 'https://www.youtube.com/watch?v=video2', 'https://www.youtube.com/watch?v=video3', 'https://www.youtube.com/watch?v=video4', 'https://www.youtube.com/watch?v=video5' ]; const activeDownloads = []; const completedDownloads = []; const processDownload = async (url) => { return new Promise((resolve) => { setTimeout(() => { resolve({ url, success: true }); }, 100); }); }; // Simulate concurrent download management while (downloadQueue.length > 0 || activeDownloads.length > 0) { // Start new downloads up to the limit while (activeDownloads.length < maxConcurrentDownloads && downloadQueue.length > 0) { const url = downloadQueue.shift(); const downloadPromise = processDownload(url); activeDownloads.push(downloadPromise); } // Wait for at least one download to complete if (activeDownloads.length > 0) { const completed = await Promise.race(activeDownloads); completedDownloads.push(completed); // Remove completed download from active list const completedIndex = activeDownloads.findIndex(p => p === Promise.resolve(completed)); if (completedIndex > -1) { activeDownloads.splice(completedIndex, 1); } } } expect(completedDownloads).toHaveLength(5); expect(completedDownloads.every(result => result.success)).toBe(true); }); it('should monitor memory usage during large operations', () => { const initialMemory = process.memoryUsage(); // Simulate memory-intensive operation const largeArray = new Array(1000000).fill('test data'); const currentMemory = process.memoryUsage(); const memoryIncrease = currentMemory.heapUsed - initialMemory.heapUsed; expect(memoryIncrease).toBeGreaterThan(0); // Cleanup largeArray.length = 0; }); }); });