const fs = require('fs'); const GroupRepository = require('../../src/repositories/GroupRepository'); const DeletionLogRepository = require('../../src/repositories/DeletionLogRepository'); const GroupCleanupService = require('../../src/services/GroupCleanupService'); describe('GroupCleanupService', () => { beforeEach(() => { jest.clearAllMocks(); }); afterEach(() => { jest.restoreAllMocks(); }); describe('getDaysUntilDeletion', () => { const NOW = new Date('2024-01-10T00:00:00Z'); beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(NOW); }); afterAll(() => { jest.useRealTimers(); }); it('returns remaining days when future deletion date is ahead', () => { const days = GroupCleanupService.getDaysUntilDeletion(new Date('2024-01-05T00:00:00Z')); expect(days).toBe(2); }); it('clamps negative differences to zero', () => { const days = GroupCleanupService.getDaysUntilDeletion(new Date('2023-12-01T00:00:00Z')); expect(days).toBe(0); }); }); describe('deletePhysicalFiles', () => { it('counts successful deletions and ignores missing files', async () => { const unlinkMock = jest.spyOn(fs.promises, 'unlink'); unlinkMock .mockResolvedValueOnce() .mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' })) .mockRejectedValueOnce(new Error('boom')) .mockResolvedValueOnce(); const result = await GroupCleanupService.deletePhysicalFiles([ { file_path: 'images/one.jpg', preview_path: 'previews/one.jpg' }, { file_path: 'images/two.jpg', preview_path: 'previews/two.jpg' } ]); expect(result).toEqual({ success: 2, failed: 1 }); expect(unlinkMock).toHaveBeenCalledTimes(4); }); }); describe('findGroupsForDeletion', () => { it('fetches unapproved groups older than default threshold', async () => { const groups = [{ group_id: 'abc' }]; const findSpy = jest .spyOn(GroupRepository, 'findUnapprovedGroupsOlderThan') .mockResolvedValue(groups); const result = await GroupCleanupService.findGroupsForDeletion(); expect(findSpy).toHaveBeenCalledWith(GroupCleanupService.CLEANUP_DAYS); expect(result).toBe(groups); }); }); describe('deleteGroupCompletely', () => { it('returns null when statistics are missing', async () => { jest.spyOn(GroupRepository, 'getGroupStatistics').mockResolvedValue(null); const deleteSpy = jest.spyOn(GroupRepository, 'deleteGroupCompletely').mockResolvedValue({}); const result = await GroupCleanupService.deleteGroupCompletely('missing-group'); expect(result).toBeNull(); expect(deleteSpy).not.toHaveBeenCalled(); }); it('removes group, files and logs deletion', async () => { jest.spyOn(GroupRepository, 'getGroupStatistics').mockResolvedValue({ groupId: 'group-1', year: 2024, imageCount: 3, uploadDate: '2024-01-01', totalFileSize: 1234 }); jest.spyOn(GroupRepository, 'deleteGroupCompletely').mockResolvedValue({ imagePaths: [{ file_path: 'images/a.jpg', preview_path: 'previews/a.jpg' }], deletedImages: 3 }); const deleteFilesSpy = jest .spyOn(GroupCleanupService, 'deletePhysicalFiles') .mockResolvedValue({ success: 2, failed: 0 }); const logSpy = jest.spyOn(GroupCleanupService, 'logDeletion').mockResolvedValue(); const result = await GroupCleanupService.deleteGroupCompletely('group-1'); expect(deleteFilesSpy).toHaveBeenCalledWith([{ file_path: 'images/a.jpg', preview_path: 'previews/a.jpg' }]); expect(logSpy).toHaveBeenCalledWith( expect.objectContaining({ groupId: 'group-1', imageCount: 3, totalFileSize: 1234 }) ); expect(result).toEqual({ groupId: 'group-1', imagesDeleted: 3, filesDeleted: 2 }); }); }); describe('logDeletion', () => { it('swallows repository errors so cleanup continues', async () => { jest.spyOn(DeletionLogRepository, 'createDeletionEntry').mockRejectedValue(new Error('db down')); await expect( GroupCleanupService.logDeletion({ groupId: 'g1', year: 2024, imageCount: 1, uploadDate: '2024-01-01' }) ).resolves.toBeUndefined(); }); }); describe('performScheduledCleanup', () => { it('returns early when there is nothing to delete', async () => { const findSpy = jest.spyOn(GroupCleanupService, 'findGroupsForDeletion').mockResolvedValue([]); const result = await GroupCleanupService.performScheduledCleanup(); expect(findSpy).toHaveBeenCalled(); expect(result).toEqual({ success: true, deletedGroups: 0, message: 'No groups to delete' }); }); it('keeps track of successes and failures', async () => { const findSpy = jest .spyOn(GroupCleanupService, 'findGroupsForDeletion') .mockResolvedValue([{ group_id: 'g1' }, { group_id: 'g2' }]); const deleteSpy = jest .spyOn(GroupCleanupService, 'deleteGroupCompletely') .mockResolvedValueOnce() .mockRejectedValueOnce(new Error('boom')); const result = await GroupCleanupService.performScheduledCleanup(); expect(findSpy).toHaveBeenCalled(); expect(deleteSpy).toHaveBeenCalledTimes(2); expect(result.success).toBe(true); expect(result.deletedGroups).toBe(1); expect(result.failedGroups).toBe(1); expect(result.duration).toBeDefined(); }); }); });