| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- /**
- * @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;
- });
- });
- });
|