fix: delete requests using stored media path (#276)

This commit is contained in:
kikootwo
2026-08-18 17:26:02 -04:00
committed by GitHub
parent 425129e8c9
commit ae75c914c9
4 changed files with 184 additions and 70 deletions
@@ -38,13 +38,6 @@ vi.mock('@/lib/services/audiobookshelf/api', () => ({
deleteABSItem: vi.fn(),
}));
vi.mock('@/lib/utils/file-organizer', () => ({
buildAudiobookPath: vi.fn((mediaDir: string, template: string, data: any) => {
// Simple mock implementation that mimics the real behavior for tests
return path.join(mediaDir, data.author, `${data.title} ${data.asin}`);
}),
}));
describe('deleteRequest', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -74,6 +67,7 @@ describe('deleteRequest', () => {
audibleAsin: 'ASIN1',
plexGuid: 'plex-1',
absItemId: null,
filePath: path.join('/media', 'Author', 'Book ASIN1'),
},
downloadHistory: [
{
@@ -154,6 +148,7 @@ describe('deleteRequest', () => {
audibleAsin: null,
plexGuid: 'plex-2',
absItemId: null,
filePath: path.join('/media', 'Author', 'Book Two'),
},
downloadHistory: [
{
@@ -219,6 +214,7 @@ describe('deleteRequest', () => {
audibleAsin: 'ASIN3',
plexGuid: 'plex-3',
absItemId: null,
filePath: path.join('/media', 'Author Name', 'Book Three ASIN3'),
},
downloadHistory: [
{
@@ -291,6 +287,7 @@ describe('deleteRequest', () => {
audibleAsin: null,
plexGuid: 'plex-4',
absItemId: null,
filePath: path.join('/media', 'Author', 'Book Four'),
},
downloadHistory: [
{
@@ -349,6 +346,7 @@ describe('deleteRequest', () => {
audibleAsin: null,
plexGuid: null,
absItemId: 'abs-5',
filePath: path.join('/media', 'Author', 'Book Five'),
},
downloadHistory: [
{
@@ -401,6 +399,7 @@ describe('deleteRequest', () => {
audibleAsin: 'ASIN6',
plexGuid: null,
absItemId: 'abs-item-123',
filePath: path.join('/media', 'Author Six', 'Book Six ASIN6'),
},
downloadHistory: [],
});
@@ -447,6 +446,7 @@ describe('deleteRequest', () => {
audibleAsin: null,
plexGuid: null,
absItemId: 'abs-item-456',
filePath: path.join('/media', 'Author Seven', 'Book Seven'),
},
downloadHistory: [],
});
@@ -0,0 +1,130 @@
/**
* Component: Request Delete Stored Path Regression Tests
* Documentation: documentation/admin-features/request-deletion.md
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { createPrismaMock } from '../helpers/prisma';
const prismaMock = createPrismaMock();
const configServiceMock = {
get: vi.fn(),
getBackendMode: vi.fn(),
};
vi.mock('@/lib/db', () => ({
prisma: prismaMock,
}));
vi.mock('@/lib/services/config.service', () => ({
getConfigService: () => configServiceMock,
}));
vi.mock('@/lib/utils/logger', () => ({
RMABLogger: {
create: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
},
}));
describe('deleteRequest stored media path', () => {
let tempRoot: string;
beforeEach(async () => {
vi.clearAllMocks();
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rmab-request-delete-'));
configServiceMock.get.mockRejectedValue(
new Error('Media deletion must not read current path configuration')
);
configServiceMock.getBackendMode.mockResolvedValue('plex');
prismaMock.request.findMany.mockResolvedValue([]);
prismaMock.request.updateMany.mockResolvedValue({ count: 0 });
prismaMock.request.update.mockResolvedValue({});
prismaMock.audiobook.update.mockResolvedValue({});
prismaMock.plexLibrary.findMany.mockResolvedValue([]);
});
afterEach(async () => {
await fs.rm(tempRoot, { recursive: true, force: true });
});
it('deletes the stored directory and preserves the path rendered by the current template', async () => {
const storedPath = path.join(tempRoot, 'Arthur Conan Doyle', 'The Valley of Fear');
const currentTemplatePath = path.join(
tempRoot,
'Arthur Conan Doyle',
'1914 - The Valley of Fear'
);
const unrelatedFile = path.join(currentTemplatePath, 'my own rip.m4b');
await fs.mkdir(storedPath, { recursive: true });
await fs.mkdir(currentTemplatePath, { recursive: true });
await fs.writeFile(path.join(storedPath, 'book.m4b'), 'organized by RMAB');
await fs.writeFile(unrelatedFile, 'user-owned file');
prismaMock.request.findFirst.mockResolvedValue({
id: 'req-stored-path',
type: 'audiobook',
audiobook: {
id: 'ab-stored-path',
title: 'The Valley of Fear',
author: 'Arthur Conan Doyle',
narrator: null,
audibleAsin: null,
plexGuid: null,
absItemId: null,
filePath: storedPath,
fileFormat: 'm4b',
},
downloadHistory: [],
});
const { deleteRequest } = await import('@/lib/services/request-delete.service');
const result = await deleteRequest('req-stored-path', 'admin-1');
expect(result.success).toBe(true);
expect(result.filesDeleted).toBe(true);
await expect(fs.access(storedPath)).rejects.toThrow();
await expect(fs.readFile(unrelatedFile, 'utf8')).resolves.toBe('user-owned file');
expect(configServiceMock.get).not.toHaveBeenCalled();
});
it('skips file deletion when a legacy row has no stored path', async () => {
const unrelatedPath = path.join(tempRoot, 'Arthur Conan Doyle', 'The Valley of Fear');
const unrelatedFile = path.join(unrelatedPath, 'my own rip.m4b');
await fs.mkdir(unrelatedPath, { recursive: true });
await fs.writeFile(unrelatedFile, 'user-owned file');
prismaMock.request.findFirst.mockResolvedValue({
id: 'req-no-stored-path',
type: 'audiobook',
audiobook: {
id: 'ab-no-stored-path',
title: 'The Valley of Fear',
author: 'Arthur Conan Doyle',
narrator: null,
audibleAsin: null,
plexGuid: null,
absItemId: null,
filePath: null,
fileFormat: null,
},
downloadHistory: [],
});
const { deleteRequest } = await import('@/lib/services/request-delete.service');
const result = await deleteRequest('req-no-stored-path', 'admin-1');
expect(result.success).toBe(true);
expect(result.filesDeleted).toBe(false);
await expect(fs.readFile(unrelatedFile, 'utf8')).resolves.toBe('user-owned file');
expect(configServiceMock.get).not.toHaveBeenCalled();
});
});