download-manager.test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. /**
  2. * Download Manager Tests
  3. * Tests for parallel download queue, priority system, retry logic
  4. */
  5. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
  6. import DownloadManager from '../src/download-manager.js'
  7. const { PRIORITY } = DownloadManager
  8. describe('DownloadManager - Parallel Processing', () => {
  9. let manager
  10. let mockDownloadFn
  11. beforeEach(() => {
  12. manager = new DownloadManager({ maxConcurrent: 2, maxRetries: 2 })
  13. // Mock download function
  14. mockDownloadFn = vi.fn(async ({ url }) => {
  15. // Simulate download delay
  16. await new Promise(resolve => setTimeout(resolve, 100))
  17. return { success: true, url }
  18. })
  19. })
  20. afterEach(() => {
  21. if (manager) {
  22. manager.cancelAll()
  23. }
  24. })
  25. describe('Queue Management', () => {
  26. it('should initialize with correct settings', () => {
  27. expect(manager.maxConcurrent).toBeGreaterThan(0)
  28. expect(manager.maxRetries).toBe(2)
  29. expect(manager.activeDownloads.size).toBe(0)
  30. expect(manager.queuedDownloads.length).toBe(0)
  31. })
  32. it('should get stats correctly', () => {
  33. const stats = manager.getStats()
  34. expect(stats).toHaveProperty('active')
  35. expect(stats).toHaveProperty('queued')
  36. expect(stats).toHaveProperty('maxConcurrent')
  37. expect(stats).toHaveProperty('completed')
  38. expect(stats).toHaveProperty('canAcceptMore')
  39. expect(stats.active).toBe(0)
  40. expect(stats.queued).toBe(0)
  41. })
  42. it('should detect if video is already downloading', async () => {
  43. const downloadPromise = manager.addDownload({
  44. videoId: 'test1',
  45. url: 'https://youtube.com/watch?v=test1',
  46. quality: '720p',
  47. format: 'mp4',
  48. savePath: '/tmp',
  49. downloadFn: mockDownloadFn
  50. })
  51. expect(manager.isDownloading('test1')).toBe(true)
  52. await downloadPromise
  53. expect(manager.isDownloading('test1')).toBe(false)
  54. })
  55. it('should prevent duplicate video downloads', async () => {
  56. const downloadPromise = manager.addDownload({
  57. videoId: 'test1',
  58. url: 'https://youtube.com/watch?v=test1',
  59. quality: '720p',
  60. format: 'mp4',
  61. savePath: '/tmp',
  62. downloadFn: mockDownloadFn
  63. })
  64. await expect(async () => {
  65. await manager.addDownload({
  66. videoId: 'test1',
  67. url: 'https://youtube.com/watch?v=test1',
  68. quality: '720p',
  69. format: 'mp4',
  70. savePath: '/tmp',
  71. downloadFn: mockDownloadFn
  72. })
  73. }).rejects.toThrow('already being downloaded')
  74. await downloadPromise
  75. })
  76. })
  77. describe('Priority System', () => {
  78. it('should add downloads with default NORMAL priority', async () => {
  79. // Fill up active downloads first
  80. const slowDownload = vi.fn(async () => {
  81. await new Promise(resolve => setTimeout(resolve, 500))
  82. return { success: true }
  83. })
  84. manager.addDownload({
  85. videoId: 'active1',
  86. url: 'https://youtube.com/watch?v=active1',
  87. quality: '720p',
  88. format: 'mp4',
  89. savePath: '/tmp',
  90. downloadFn: slowDownload
  91. })
  92. manager.addDownload({
  93. videoId: 'active2',
  94. url: 'https://youtube.com/watch?v=active2',
  95. quality: '720p',
  96. format: 'mp4',
  97. savePath: '/tmp',
  98. downloadFn: slowDownload
  99. })
  100. // Now this one goes to queue
  101. manager.addDownload({
  102. videoId: 'test1',
  103. url: 'https://youtube.com/watch?v=test1',
  104. quality: '720p',
  105. format: 'mp4',
  106. savePath: '/tmp',
  107. downloadFn: mockDownloadFn
  108. })
  109. expect(manager.queuedDownloads[0].priority).toBe(PRIORITY.NORMAL)
  110. manager.cancelAll()
  111. })
  112. it('should accept custom priority', async () => {
  113. // Fill up active downloads first
  114. const slowDownload = vi.fn(async () => {
  115. await new Promise(resolve => setTimeout(resolve, 500))
  116. return { success: true }
  117. })
  118. manager.addDownload({
  119. videoId: 'active1',
  120. url: 'https://youtube.com/watch?v=active1',
  121. quality: '720p',
  122. format: 'mp4',
  123. savePath: '/tmp',
  124. downloadFn: slowDownload
  125. })
  126. manager.addDownload({
  127. videoId: 'active2',
  128. url: 'https://youtube.com/watch?v=active2',
  129. quality: '720p',
  130. format: 'mp4',
  131. savePath: '/tmp',
  132. downloadFn: slowDownload
  133. })
  134. // Now this one goes to queue with HIGH priority
  135. manager.addDownload({
  136. videoId: 'test1',
  137. url: 'https://youtube.com/watch?v=test1',
  138. quality: '720p',
  139. format: 'mp4',
  140. savePath: '/tmp',
  141. downloadFn: mockDownloadFn
  142. }, PRIORITY.HIGH)
  143. expect(manager.queuedDownloads[0].priority).toBe(PRIORITY.HIGH)
  144. manager.cancelAll()
  145. })
  146. it('should sort queue by priority', async () => {
  147. // Fill up active downloads first (maxConcurrent = 2)
  148. const slowDownload = vi.fn(async () => {
  149. await new Promise(resolve => setTimeout(resolve, 500))
  150. return { success: true }
  151. })
  152. manager.addDownload({
  153. videoId: 'active1',
  154. url: 'https://youtube.com/watch?v=active1',
  155. quality: '720p',
  156. format: 'mp4',
  157. savePath: '/tmp',
  158. downloadFn: slowDownload
  159. })
  160. manager.addDownload({
  161. videoId: 'active2',
  162. url: 'https://youtube.com/watch?v=active2',
  163. quality: '720p',
  164. format: 'mp4',
  165. savePath: '/tmp',
  166. downloadFn: slowDownload
  167. })
  168. // Now add to queue with different priorities
  169. manager.addDownload({
  170. videoId: 'low',
  171. url: 'https://youtube.com/watch?v=low',
  172. quality: '720p',
  173. format: 'mp4',
  174. savePath: '/tmp',
  175. downloadFn: mockDownloadFn
  176. }, PRIORITY.LOW)
  177. manager.addDownload({
  178. videoId: 'high',
  179. url: 'https://youtube.com/watch?v=high',
  180. quality: '720p',
  181. format: 'mp4',
  182. savePath: '/tmp',
  183. downloadFn: mockDownloadFn
  184. }, PRIORITY.HIGH)
  185. manager.addDownload({
  186. videoId: 'normal',
  187. url: 'https://youtube.com/watch?v=normal',
  188. quality: '720p',
  189. format: 'mp4',
  190. savePath: '/tmp',
  191. downloadFn: mockDownloadFn
  192. }, PRIORITY.NORMAL)
  193. // Check queue order
  194. expect(manager.queuedDownloads[0].videoId).toBe('high')
  195. expect(manager.queuedDownloads[1].videoId).toBe('normal')
  196. expect(manager.queuedDownloads[2].videoId).toBe('low')
  197. // Clean up
  198. manager.cancelAll()
  199. })
  200. it('should allow changing priority of queued download', async () => {
  201. // Fill active downloads
  202. const slowDownload = vi.fn(async () => {
  203. await new Promise(resolve => setTimeout(resolve, 500))
  204. return { success: true }
  205. })
  206. manager.addDownload({
  207. videoId: 'active1',
  208. url: 'https://youtube.com/watch?v=active1',
  209. quality: '720p',
  210. format: 'mp4',
  211. savePath: '/tmp',
  212. downloadFn: slowDownload
  213. })
  214. manager.addDownload({
  215. videoId: 'active2',
  216. url: 'https://youtube.com/watch?v=active2',
  217. quality: '720p',
  218. format: 'mp4',
  219. savePath: '/tmp',
  220. downloadFn: slowDownload
  221. })
  222. // Add low priority download
  223. manager.addDownload({
  224. videoId: 'test1',
  225. url: 'https://youtube.com/watch?v=test1',
  226. quality: '720p',
  227. format: 'mp4',
  228. savePath: '/tmp',
  229. downloadFn: mockDownloadFn
  230. }, PRIORITY.LOW)
  231. // Change to high priority
  232. const changed = manager.setPriority('test1', PRIORITY.HIGH)
  233. expect(changed).toBe(true)
  234. const request = manager.queuedDownloads.find(r => r.videoId === 'test1')
  235. expect(request.priority).toBe(PRIORITY.HIGH)
  236. // Clean up
  237. manager.cancelAll()
  238. })
  239. })
  240. describe('Retry Logic', () => {
  241. it('should have retry configuration', () => {
  242. expect(manager.maxRetries).toBe(2)
  243. })
  244. it('should identify retryable errors', () => {
  245. const retryableErrors = [
  246. new Error('network timeout'),
  247. new Error('ECONNRESET'),
  248. new Error('ETIMEDOUT'),
  249. new Error('ENOTFOUND'),
  250. new Error('503 Service Unavailable')
  251. ]
  252. retryableErrors.forEach(error => {
  253. expect(manager.isRetryableError(error)).toBe(true)
  254. })
  255. })
  256. it('should identify non-retryable errors', () => {
  257. const nonRetryableErrors = [
  258. new Error('Video unavailable'),
  259. new Error('Permission denied'),
  260. new Error('Invalid URL')
  261. ]
  262. nonRetryableErrors.forEach(error => {
  263. expect(manager.isRetryableError(error)).toBe(false)
  264. })
  265. })
  266. })
  267. describe('Cancellation', () => {
  268. it('should cancel queued download', async () => {
  269. // Fill active slots
  270. const slowDownload = vi.fn(async () => {
  271. await new Promise(resolve => setTimeout(resolve, 500))
  272. return { success: true }
  273. })
  274. manager.addDownload({
  275. videoId: 'active1',
  276. url: 'https://youtube.com/watch?v=active1',
  277. quality: '720p',
  278. format: 'mp4',
  279. savePath: '/tmp',
  280. downloadFn: slowDownload
  281. })
  282. manager.addDownload({
  283. videoId: 'active2',
  284. url: 'https://youtube.com/watch?v=active2',
  285. quality: '720p',
  286. format: 'mp4',
  287. savePath: '/tmp',
  288. downloadFn: slowDownload
  289. })
  290. // Add to queue
  291. const queuedPromise = manager.addDownload({
  292. videoId: 'queued1',
  293. url: 'https://youtube.com/watch?v=queued1',
  294. quality: '720p',
  295. format: 'mp4',
  296. savePath: '/tmp',
  297. downloadFn: mockDownloadFn
  298. })
  299. // Cancel it
  300. const cancelled = manager.cancelDownload('queued1')
  301. expect(cancelled).toBe(true)
  302. expect(manager.queuedDownloads.find(r => r.videoId === 'queued1')).toBeUndefined()
  303. await expect(queuedPromise).rejects.toThrow('cancelled')
  304. // Clean up
  305. manager.cancelAll()
  306. })
  307. it('should cancel all downloads', () => {
  308. // Add some downloads (catch rejections to avoid unhandled errors)
  309. manager.addDownload({
  310. videoId: 'test1',
  311. url: 'https://youtube.com/watch?v=test1',
  312. quality: '720p',
  313. format: 'mp4',
  314. savePath: '/tmp',
  315. downloadFn: mockDownloadFn
  316. }).catch(() => {})
  317. manager.addDownload({
  318. videoId: 'test2',
  319. url: 'https://youtube.com/watch?v=test2',
  320. quality: '720p',
  321. format: 'mp4',
  322. savePath: '/tmp',
  323. downloadFn: mockDownloadFn
  324. }).catch(() => {})
  325. const result = manager.cancelAll()
  326. expect(result.total).toBeGreaterThanOrEqual(0)
  327. expect(manager.queuedDownloads.length).toBe(0)
  328. expect(manager.activeDownloads.size).toBe(0)
  329. })
  330. })
  331. describe('Event Emission', () => {
  332. it('should emit queueUpdated event', async () => {
  333. return new Promise((resolve) => {
  334. manager.on('queueUpdated', (stats) => {
  335. expect(stats).toHaveProperty('active')
  336. expect(stats).toHaveProperty('queued')
  337. resolve()
  338. })
  339. manager.addDownload({
  340. videoId: 'test1',
  341. url: 'https://youtube.com/watch?v=test1',
  342. quality: '720p',
  343. format: 'mp4',
  344. savePath: '/tmp',
  345. downloadFn: mockDownloadFn
  346. }).catch(() => {})
  347. })
  348. })
  349. })
  350. })