@@ -39,10 +39,17 @@ describe('VideoDeletionModule', () => {
3939 promises : mockFs
4040 } ) ) ;
4141
42- // Mock the filesystem module (isVideoDirectory)
42+ // Mock the filesystem module (isVideoDirectory, cleanupEmptyChannelDirectory, cleanupEmptyParents )
4343 // Default to true (nested structure) for backwards compatibility with existing tests
4444 jest . doMock ( '../filesystem' , ( ) => ( {
45- isVideoDirectory : jest . fn ( ( ) => true )
45+ isVideoDirectory : jest . fn ( ( ) => true ) ,
46+ cleanupEmptyChannelDirectory : jest . fn ( ) . mockResolvedValue ( false ) ,
47+ cleanupEmptyParents : jest . fn ( ) . mockResolvedValue ( )
48+ } ) ) ;
49+
50+ // Mock configModule for _tryCleanupChannelDirectory
51+ jest . doMock ( '../configModule' , ( ) => ( {
52+ directoryPath : '/test/output'
4653 } ) ) ;
4754
4855 // Require the module after mocks are in place
@@ -270,6 +277,158 @@ describe('VideoDeletionModule', () => {
270277 } ) ;
271278 } ) ;
272279
280+ describe ( '_tryCleanupChannelDirectory' , ( ) => {
281+ let mockFilesystem ;
282+
283+ beforeEach ( ( ) => {
284+ mockFilesystem = require ( '../filesystem' ) ;
285+ } ) ;
286+
287+ test ( 'should call cleanupEmptyChannelDirectory with grandparent path for nested deletion' , async ( ) => {
288+ const mockVideoRecord = {
289+ id : 1 ,
290+ youtubeId : 'abc123' ,
291+ filePath : '/test/output/Channel Name/Channel Name - Video Title - abc123/video.mp4' ,
292+ removed : false ,
293+ update : jest . fn ( ) . mockResolvedValue ( )
294+ } ;
295+
296+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
297+ mockFs . rm . mockResolvedValue ( ) ;
298+
299+ await VideoDeletionModule . deleteVideoById ( 1 ) ;
300+
301+ expect ( mockFilesystem . cleanupEmptyChannelDirectory ) . toHaveBeenCalledWith (
302+ '/test/output/Channel Name' ,
303+ '/test/output' ,
304+ { includeIgnorableFiles : true }
305+ ) ;
306+ } ) ;
307+
308+ test ( 'should call cleanupEmptyChannelDirectory with parent path for flat deletion' , async ( ) => {
309+ // Override isVideoDirectory to return false for flat mode
310+ mockFilesystem . isVideoDirectory . mockReturnValue ( false ) ;
311+
312+ const mockVideoRecord = {
313+ id : 1 ,
314+ youtubeId : 'abc123' ,
315+ filePath : '/test/output/Channel Name/Channel Name - Video Title [abc123].mp4' ,
316+ removed : false ,
317+ update : jest . fn ( ) . mockResolvedValue ( )
318+ } ;
319+
320+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
321+ mockFs . readdir . mockResolvedValue ( [ ] ) ;
322+
323+ await VideoDeletionModule . deleteVideoById ( 1 ) ;
324+
325+ expect ( mockFilesystem . cleanupEmptyChannelDirectory ) . toHaveBeenCalledWith (
326+ '/test/output/Channel Name' ,
327+ '/test/output' ,
328+ { includeIgnorableFiles : true }
329+ ) ;
330+ } ) ;
331+
332+ test ( 'should NOT call cleanup when video has no filePath' , async ( ) => {
333+ const mockVideoRecord = {
334+ id : 1 ,
335+ youtubeId : 'abc123' ,
336+ filePath : null ,
337+ removed : false ,
338+ update : jest . fn ( ) . mockResolvedValue ( )
339+ } ;
340+
341+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
342+
343+ await VideoDeletionModule . deleteVideoById ( 1 ) ;
344+
345+ expect ( mockFilesystem . cleanupEmptyChannelDirectory ) . not . toHaveBeenCalled ( ) ;
346+ } ) ;
347+
348+ test ( 'should NOT call cleanup when file deletion fails' , async ( ) => {
349+ const mockVideoRecord = {
350+ id : 1 ,
351+ youtubeId : 'abc123' ,
352+ filePath : '/test/output/Channel/Channel - Video - abc123/video.mp4' ,
353+ removed : false
354+ } ;
355+
356+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
357+
358+ const permissionError = new Error ( 'EACCES' ) ;
359+ permissionError . code = 'EACCES' ;
360+ mockFs . rm . mockRejectedValue ( permissionError ) ;
361+
362+ await VideoDeletionModule . deleteVideoById ( 1 ) ;
363+
364+ expect ( mockFilesystem . cleanupEmptyChannelDirectory ) . not . toHaveBeenCalled ( ) ;
365+ } ) ;
366+
367+ test ( 'should not affect success response when cleanup throws' , async ( ) => {
368+ mockFilesystem . cleanupEmptyChannelDirectory . mockRejectedValueOnce ( new Error ( 'cleanup failed' ) ) ;
369+
370+ const mockVideoRecord = {
371+ id : 1 ,
372+ youtubeId : 'abc123' ,
373+ filePath : '/test/output/Channel Name/Channel Name - Video Title - abc123/video.mp4' ,
374+ removed : false ,
375+ update : jest . fn ( ) . mockResolvedValue ( )
376+ } ;
377+
378+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
379+ mockFs . rm . mockResolvedValue ( ) ;
380+
381+ const result = await VideoDeletionModule . deleteVideoById ( 1 ) ;
382+
383+ expect ( result . success ) . toBe ( true ) ;
384+ expect ( mockLogger . warn ) . toHaveBeenCalledWith (
385+ expect . objectContaining ( { err : expect . any ( Error ) } ) ,
386+ expect . stringContaining ( 'non-fatal' )
387+ ) ;
388+ } ) ;
389+
390+ test ( 'should call cleanupEmptyParents when channel dir was removed' , async ( ) => {
391+ mockFilesystem . cleanupEmptyChannelDirectory . mockResolvedValueOnce ( true ) ;
392+
393+ const mockVideoRecord = {
394+ id : 1 ,
395+ youtubeId : 'abc123' ,
396+ filePath : '/test/output/__subfolder/Channel Name/Channel Name - Video Title - abc123/video.mp4' ,
397+ removed : false ,
398+ update : jest . fn ( ) . mockResolvedValue ( )
399+ } ;
400+
401+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
402+ mockFs . rm . mockResolvedValue ( ) ;
403+
404+ await VideoDeletionModule . deleteVideoById ( 1 ) ;
405+
406+ expect ( mockFilesystem . cleanupEmptyParents ) . toHaveBeenCalledWith (
407+ '/test/output/__subfolder' ,
408+ '/test/output'
409+ ) ;
410+ } ) ;
411+
412+ test ( 'should NOT call cleanupEmptyParents when channel dir was not removed' , async ( ) => {
413+ mockFilesystem . cleanupEmptyChannelDirectory . mockResolvedValueOnce ( false ) ;
414+
415+ const mockVideoRecord = {
416+ id : 1 ,
417+ youtubeId : 'abc123' ,
418+ filePath : '/test/output/Channel Name/Channel Name - Video Title - abc123/video.mp4' ,
419+ removed : false ,
420+ update : jest . fn ( ) . mockResolvedValue ( )
421+ } ;
422+
423+ mockVideo . findByPk . mockResolvedValue ( mockVideoRecord ) ;
424+ mockFs . rm . mockResolvedValue ( ) ;
425+
426+ await VideoDeletionModule . deleteVideoById ( 1 ) ;
427+
428+ expect ( mockFilesystem . cleanupEmptyParents ) . not . toHaveBeenCalled ( ) ;
429+ } ) ;
430+ } ) ;
431+
273432 describe ( 'deleteVideos' , ( ) => {
274433 test ( 'should successfully delete multiple videos' , async ( ) => {
275434 const mockVideo1 = {
0 commit comments