mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
3a9ae4a439
Implements admin approval workflow for user requests with global and per-user auto-approve controls. Adds new request statuses ('awaiting_approval', 'denied'), related API endpoints, and UI for pending approvals. Introduces configurable audiobook organization path template with validation and preview in settings, updates database schema and migrations for new fields.
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
/**
|
|
* Component: Organize Files Processor Tests
|
|
* Documentation: documentation/phase3/file-organization.md
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createPrismaMock } from '../helpers/prisma';
|
|
|
|
const prismaMock = createPrismaMock();
|
|
const organizerMock = vi.hoisted(() => ({ organize: vi.fn() }));
|
|
const libraryServiceMock = vi.hoisted(() => ({ triggerLibraryScan: vi.fn() }));
|
|
const configMock = vi.hoisted(() => ({
|
|
getBackendMode: vi.fn(),
|
|
get: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/db', () => ({
|
|
prisma: prismaMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/utils/file-organizer', () => ({
|
|
getFileOrganizer: () => organizerMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/library', () => ({
|
|
getLibraryService: () => libraryServiceMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/config.service', () => ({
|
|
getConfigService: () => configMock,
|
|
}));
|
|
|
|
describe('processOrganizeFiles', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('organizes files and triggers filesystem scan when enabled', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.audiobook.findUnique.mockResolvedValue({
|
|
id: 'a1',
|
|
title: 'Book',
|
|
author: 'Author',
|
|
narrator: null,
|
|
coverArtUrl: null,
|
|
audibleAsin: 'ASIN1',
|
|
});
|
|
organizerMock.organize.mockResolvedValue({
|
|
success: true,
|
|
targetPath: '/media/Author/Book',
|
|
filesMovedCount: 1,
|
|
errors: [],
|
|
audioFiles: ['/media/Author/Book/Book.m4b'],
|
|
});
|
|
prismaMock.audiobook.update.mockResolvedValue({});
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
configMock.getBackendMode.mockResolvedValue('plex');
|
|
configMock.get.mockImplementation(async (key: string) => {
|
|
if (key === 'plex.trigger_scan_after_import') return 'true';
|
|
if (key === 'plex_audiobook_library_id') return 'lib-1';
|
|
if (key === 'audiobook_path_template') return '{author}/{title} {asin}';
|
|
return null;
|
|
});
|
|
|
|
const { processOrganizeFiles } = await import('@/lib/processors/organize-files.processor');
|
|
const result = await processOrganizeFiles({
|
|
requestId: 'req-1',
|
|
audiobookId: 'a1',
|
|
downloadPath: '/downloads/book',
|
|
jobId: 'job-1',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(libraryServiceMock.triggerLibraryScan).toHaveBeenCalledWith('lib-1');
|
|
});
|
|
|
|
it('queues retry when a retryable error occurs', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.audiobook.findUnique.mockResolvedValue({
|
|
id: 'a2',
|
|
title: 'Book',
|
|
author: 'Author',
|
|
narrator: null,
|
|
coverArtUrl: null,
|
|
audibleAsin: 'ASIN2',
|
|
});
|
|
organizerMock.organize.mockResolvedValue({
|
|
success: false,
|
|
targetPath: '',
|
|
filesMovedCount: 0,
|
|
errors: ['No audiobook files found in download'],
|
|
audioFiles: [],
|
|
});
|
|
prismaMock.request.findFirst.mockResolvedValue({
|
|
importAttempts: 0,
|
|
maxImportRetries: 3,
|
|
deletedAt: null,
|
|
});
|
|
configMock.get.mockImplementation(async (key: string) => {
|
|
if (key === 'audiobook_path_template') return '{author}/{title} {asin}';
|
|
return null;
|
|
});
|
|
|
|
const { processOrganizeFiles } = await import('@/lib/processors/organize-files.processor');
|
|
const result = await processOrganizeFiles({
|
|
requestId: 'req-2',
|
|
audiobookId: 'a2',
|
|
downloadPath: '/downloads/book',
|
|
jobId: 'job-2',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ status: 'awaiting_import' }),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
|