- 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
154 lines
6.1 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|