integration-workflow.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. /**
  2. * @fileoverview Integration Tests for Complete Download Workflow
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  8. import { spawn } from 'child_process';
  9. import fs from 'fs';
  10. import path from 'path';
  11. import os from 'os';
  12. /**
  13. * INTEGRATION TESTS FOR COMPLETE DOWNLOAD WORKFLOW WITH BINARIES
  14. *
  15. * These tests verify the end-to-end download process including:
  16. * - Binary availability and execution
  17. * - Video metadata extraction
  18. * - Actual download process
  19. * - Format conversion workflow
  20. * - Progress tracking and status updates
  21. * - Error handling and recovery
  22. */
  23. describe('Complete Download Workflow Integration', () => {
  24. let testDownloadDir;
  25. let mockBinaryPaths;
  26. beforeEach(() => {
  27. // Create temporary download directory for tests
  28. testDownloadDir = path.join(os.tmpdir(), 'grabzilla-test-' + Date.now());
  29. if (!fs.existsSync(testDownloadDir)) {
  30. fs.mkdirSync(testDownloadDir, { recursive: true });
  31. }
  32. // Set up platform-specific binary paths
  33. const isWindows = process.platform === 'win32';
  34. mockBinaryPaths = {
  35. ytDlp: path.join(process.cwd(), 'binaries', `yt-dlp${isWindows ? '.exe' : ''}`),
  36. ffmpeg: path.join(process.cwd(), 'binaries', `ffmpeg${isWindows ? '.exe' : ''}`)
  37. };
  38. });
  39. afterEach(() => {
  40. // Clean up test download directory
  41. if (fs.existsSync(testDownloadDir)) {
  42. try {
  43. fs.rmSync(testDownloadDir, { recursive: true, force: true });
  44. } catch (error) {
  45. console.warn('Failed to clean up test directory:', error.message);
  46. }
  47. }
  48. });
  49. describe('Binary Availability and Version Checking', () => {
  50. it('should verify yt-dlp binary exists and is executable', async () => {
  51. const binaryExists = fs.existsSync(mockBinaryPaths.ytDlp);
  52. if (binaryExists) {
  53. // Test binary execution
  54. const versionCheck = await new Promise((resolve, reject) => {
  55. const process = spawn(mockBinaryPaths.ytDlp, ['--version'], {
  56. stdio: ['pipe', 'pipe', 'pipe']
  57. });
  58. let output = '';
  59. process.stdout.on('data', (data) => {
  60. output += data.toString();
  61. });
  62. process.on('close', (code) => {
  63. if (code === 0) {
  64. resolve(output.trim());
  65. } else {
  66. reject(new Error(`yt-dlp version check failed with code ${code}`));
  67. }
  68. });
  69. process.on('error', reject);
  70. });
  71. expect(versionCheck).toMatch(/^\d{4}\.\d{2}\.\d{2}/);
  72. } else {
  73. console.warn('yt-dlp binary not found, skipping execution test');
  74. expect(binaryExists).toBe(false); // Document the missing binary
  75. }
  76. });
  77. it('should verify ffmpeg binary exists and is executable', async () => {
  78. const binaryExists = fs.existsSync(mockBinaryPaths.ffmpeg);
  79. if (binaryExists) {
  80. // Test binary execution
  81. const versionCheck = await new Promise((resolve, reject) => {
  82. const process = spawn(mockBinaryPaths.ffmpeg, ['-version'], {
  83. stdio: ['pipe', 'pipe', 'pipe']
  84. });
  85. let output = '';
  86. process.stdout.on('data', (data) => {
  87. output += data.toString();
  88. });
  89. process.on('close', (code) => {
  90. if (code === 0) {
  91. resolve(output.trim());
  92. } else {
  93. reject(new Error(`ffmpeg version check failed with code ${code}`));
  94. }
  95. });
  96. process.on('error', reject);
  97. });
  98. expect(versionCheck).toMatch(/ffmpeg version/i);
  99. } else {
  100. console.warn('ffmpeg binary not found, skipping execution test');
  101. expect(binaryExists).toBe(false); // Document the missing binary
  102. }
  103. });
  104. it('should handle missing binaries gracefully', () => {
  105. const nonExistentPath = path.join(process.cwd(), 'binaries', 'nonexistent-binary');
  106. expect(() => {
  107. if (!fs.existsSync(nonExistentPath)) {
  108. throw new Error('Binary not found');
  109. }
  110. }).toThrow('Binary not found');
  111. });
  112. });
  113. describe('Video Metadata Extraction', () => {
  114. it('should extract metadata from YouTube URL using yt-dlp', async () => {
  115. const binaryExists = fs.existsSync(mockBinaryPaths.ytDlp);
  116. if (!binaryExists) {
  117. console.warn('yt-dlp binary not found, skipping metadata test');
  118. return;
  119. }
  120. // Use a known stable YouTube video for testing
  121. const testUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
  122. const metadata = await new Promise((resolve, reject) => {
  123. const process = spawn(mockBinaryPaths.ytDlp, [
  124. '--dump-json',
  125. '--no-download',
  126. testUrl
  127. ], {
  128. stdio: ['pipe', 'pipe', 'pipe']
  129. });
  130. let output = '';
  131. let errorOutput = '';
  132. process.stdout.on('data', (data) => {
  133. output += data.toString();
  134. });
  135. process.stderr.on('data', (data) => {
  136. errorOutput += data.toString();
  137. });
  138. process.on('close', (code) => {
  139. if (code === 0 && output.trim()) {
  140. try {
  141. const metadata = JSON.parse(output.trim());
  142. resolve(metadata);
  143. } catch (parseError) {
  144. reject(new Error(`Failed to parse metadata: ${parseError.message}`));
  145. }
  146. } else {
  147. reject(new Error(`Metadata extraction failed: ${errorOutput || 'Unknown error'}`));
  148. }
  149. });
  150. process.on('error', reject);
  151. });
  152. expect(metadata).toHaveProperty('title');
  153. expect(metadata).toHaveProperty('duration');
  154. expect(metadata).toHaveProperty('thumbnail');
  155. expect(metadata.title).toBeTruthy();
  156. }, 30000); // 30 second timeout for network operations
  157. it('should handle invalid URLs gracefully', async () => {
  158. const binaryExists = fs.existsSync(mockBinaryPaths.ytDlp);
  159. if (!binaryExists) {
  160. console.warn('yt-dlp binary not found, skipping invalid URL test');
  161. return;
  162. }
  163. const invalidUrl = 'https://example.com/not-a-video';
  164. await expect(async () => {
  165. await new Promise((resolve, reject) => {
  166. const process = spawn(mockBinaryPaths.ytDlp, [
  167. '--dump-json',
  168. '--no-download',
  169. invalidUrl
  170. ], {
  171. stdio: ['pipe', 'pipe', 'pipe']
  172. });
  173. let errorOutput = '';
  174. process.stderr.on('data', (data) => {
  175. errorOutput += data.toString();
  176. });
  177. process.on('close', (code) => {
  178. if (code !== 0) {
  179. reject(new Error(`Invalid URL: ${errorOutput}`));
  180. } else {
  181. resolve();
  182. }
  183. });
  184. process.on('error', reject);
  185. });
  186. }).rejects.toThrow();
  187. });
  188. });
  189. describe('Download Process Integration', () => {
  190. it('should simulate download workflow with progress tracking', async () => {
  191. // Mock download process simulation
  192. const downloadOptions = {
  193. url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
  194. quality: '720p',
  195. format: 'mp4',
  196. savePath: testDownloadDir
  197. };
  198. const progressUpdates = [];
  199. const mockProgressCallback = (data) => {
  200. progressUpdates.push(data);
  201. };
  202. // Simulate download stages
  203. mockProgressCallback({ stage: 'metadata', progress: 10, status: 'fetching metadata' });
  204. mockProgressCallback({ stage: 'download', progress: 35, status: 'downloading' });
  205. mockProgressCallback({ stage: 'download', progress: 70, status: 'downloading' });
  206. mockProgressCallback({ stage: 'complete', progress: 100, status: 'completed' });
  207. expect(progressUpdates).toHaveLength(4);
  208. expect(progressUpdates[0].stage).toBe('metadata');
  209. expect(progressUpdates[1].stage).toBe('download');
  210. expect(progressUpdates[3].stage).toBe('complete');
  211. expect(progressUpdates[3].progress).toBe(100);
  212. });
  213. it('should handle download cancellation', () => {
  214. const mockProcess = {
  215. pid: 12345,
  216. kill: vi.fn(),
  217. killed: false
  218. };
  219. // Simulate cancellation
  220. mockProcess.kill('SIGTERM');
  221. mockProcess.killed = true;
  222. expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
  223. expect(mockProcess.killed).toBe(true);
  224. });
  225. it('should handle download errors and retry logic', async () => {
  226. const maxRetries = 3;
  227. let attemptCount = 0;
  228. const mockDownload = async () => {
  229. attemptCount++;
  230. if (attemptCount < maxRetries) {
  231. throw new Error('Network error');
  232. }
  233. return { success: true, filename: 'test.mp4' };
  234. };
  235. // Simulate retry logic
  236. let result;
  237. let lastError;
  238. for (let i = 0; i < maxRetries; i++) {
  239. try {
  240. result = await mockDownload();
  241. break;
  242. } catch (error) {
  243. lastError = error;
  244. if (i === maxRetries - 1) {
  245. throw error;
  246. }
  247. }
  248. }
  249. expect(attemptCount).toBe(maxRetries);
  250. expect(result.success).toBe(true);
  251. });
  252. });
  253. describe('Format Conversion Integration', () => {
  254. it('should simulate format conversion workflow', async () => {
  255. const conversionOptions = {
  256. inputPath: path.join(testDownloadDir, 'input.mp4'),
  257. outputPath: path.join(testDownloadDir, 'output_h264.mp4'),
  258. format: 'H264',
  259. quality: '1080p'
  260. };
  261. // Create mock input file
  262. fs.writeFileSync(conversionOptions.inputPath, 'mock video data');
  263. const progressUpdates = [];
  264. const mockProgressCallback = (data) => {
  265. progressUpdates.push(data);
  266. };
  267. // Simulate conversion stages
  268. mockProgressCallback({ stage: 'conversion', progress: 25, status: 'converting' });
  269. mockProgressCallback({ stage: 'conversion', progress: 50, status: 'converting' });
  270. mockProgressCallback({ stage: 'conversion', progress: 75, status: 'converting' });
  271. mockProgressCallback({ stage: 'conversion', progress: 100, status: 'completed' });
  272. // Simulate output file creation
  273. fs.writeFileSync(conversionOptions.outputPath, 'mock converted video data');
  274. expect(progressUpdates).toHaveLength(4);
  275. expect(progressUpdates.every(update => update.stage === 'conversion')).toBe(true);
  276. expect(progressUpdates[3].progress).toBe(100);
  277. expect(fs.existsSync(conversionOptions.outputPath)).toBe(true);
  278. });
  279. it('should handle conversion errors', () => {
  280. const conversionError = new Error('FFmpeg conversion failed: Invalid codec');
  281. expect(() => {
  282. throw conversionError;
  283. }).toThrow('FFmpeg conversion failed: Invalid codec');
  284. });
  285. });
  286. describe('End-to-End Workflow Simulation', () => {
  287. it('should complete full download and conversion workflow', async () => {
  288. const workflowSteps = [];
  289. const mockWorkflow = {
  290. async validateUrl(url) {
  291. workflowSteps.push('url_validation');
  292. return { valid: true, platform: 'youtube' };
  293. },
  294. async extractMetadata(url) {
  295. workflowSteps.push('metadata_extraction');
  296. return {
  297. title: 'Test Video',
  298. duration: '00:03:30',
  299. thumbnail: 'https://example.com/thumb.jpg'
  300. };
  301. },
  302. async downloadVideo(options) {
  303. workflowSteps.push('video_download');
  304. return {
  305. success: true,
  306. filename: 'test_video.mp4',
  307. filePath: path.join(testDownloadDir, 'test_video.mp4')
  308. };
  309. },
  310. async convertVideo(options) {
  311. workflowSteps.push('video_conversion');
  312. return {
  313. success: true,
  314. outputPath: path.join(testDownloadDir, 'test_video_h264.mp4')
  315. };
  316. }
  317. };
  318. // Execute full workflow
  319. const url = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
  320. const validation = await mockWorkflow.validateUrl(url);
  321. expect(validation.valid).toBe(true);
  322. const metadata = await mockWorkflow.extractMetadata(url);
  323. expect(metadata.title).toBeTruthy();
  324. const downloadResult = await mockWorkflow.downloadVideo({
  325. url,
  326. quality: '720p',
  327. savePath: testDownloadDir
  328. });
  329. expect(downloadResult.success).toBe(true);
  330. const conversionResult = await mockWorkflow.convertVideo({
  331. inputPath: downloadResult.filePath,
  332. format: 'H264'
  333. });
  334. expect(conversionResult.success).toBe(true);
  335. expect(workflowSteps).toEqual([
  336. 'url_validation',
  337. 'metadata_extraction',
  338. 'video_download',
  339. 'video_conversion'
  340. ]);
  341. });
  342. it('should handle workflow interruption and cleanup', () => {
  343. const activeProcesses = [
  344. { pid: 12345, type: 'download' },
  345. { pid: 12346, type: 'conversion' }
  346. ];
  347. const cleanup = () => {
  348. activeProcesses.forEach(proc => {
  349. // Simulate process termination
  350. proc.killed = true;
  351. });
  352. return activeProcesses.length;
  353. };
  354. const cleanedCount = cleanup();
  355. expect(cleanedCount).toBe(2);
  356. expect(activeProcesses.every(proc => proc.killed)).toBe(true);
  357. });
  358. });
  359. describe('Performance and Resource Management', () => {
  360. it('should handle concurrent downloads efficiently', async () => {
  361. const maxConcurrentDownloads = 3;
  362. const downloadQueue = [
  363. 'https://www.youtube.com/watch?v=video1',
  364. 'https://www.youtube.com/watch?v=video2',
  365. 'https://www.youtube.com/watch?v=video3',
  366. 'https://www.youtube.com/watch?v=video4',
  367. 'https://www.youtube.com/watch?v=video5'
  368. ];
  369. const activeDownloads = [];
  370. const completedDownloads = [];
  371. const processDownload = async (url) => {
  372. return new Promise((resolve) => {
  373. setTimeout(() => {
  374. resolve({ url, success: true });
  375. }, 100);
  376. });
  377. };
  378. // Simulate concurrent download management
  379. while (downloadQueue.length > 0 || activeDownloads.length > 0) {
  380. // Start new downloads up to the limit
  381. while (activeDownloads.length < maxConcurrentDownloads && downloadQueue.length > 0) {
  382. const url = downloadQueue.shift();
  383. const downloadPromise = processDownload(url);
  384. activeDownloads.push(downloadPromise);
  385. }
  386. // Wait for at least one download to complete
  387. if (activeDownloads.length > 0) {
  388. const completed = await Promise.race(activeDownloads);
  389. completedDownloads.push(completed);
  390. // Remove completed download from active list
  391. const completedIndex = activeDownloads.findIndex(p => p === Promise.resolve(completed));
  392. if (completedIndex > -1) {
  393. activeDownloads.splice(completedIndex, 1);
  394. }
  395. }
  396. }
  397. expect(completedDownloads).toHaveLength(5);
  398. expect(completedDownloads.every(result => result.success)).toBe(true);
  399. });
  400. it('should monitor memory usage during large operations', () => {
  401. const initialMemory = process.memoryUsage();
  402. // Simulate memory-intensive operation
  403. const largeArray = new Array(1000000).fill('test data');
  404. const currentMemory = process.memoryUsage();
  405. const memoryIncrease = currentMemory.heapUsed - initialMemory.heapUsed;
  406. expect(memoryIncrease).toBeGreaterThan(0);
  407. // Cleanup
  408. largeArray.length = 0;
  409. });
  410. });
  411. });