Project-Image-Uploader/backend/tests/unit/groupCleanupService.test.js
matthias.lotz 6332b82c6a Feature Request: admin session security
- replace bearer auth with session+CSRF flow and add admin user directory

- update frontend moderation flow, force password change gate, and new CLI

- refresh changelog/docs/feature plan + ensure swagger dev experience
2025-11-23 21:18:42 +01:00

154 lines
6.1 KiB
JavaScript

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();
});
});
});