mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
94dbaf073b
Introduced a Vitest-based backend unit testing framework with supporting scripts, helpers, and GitHub Actions integration. Refactored the admin settings page to a modular architecture, splitting monolithic logic into feature-specific tabs and hooks for improved maintainability and testability. Updated documentation to reflect the new testing setup and settings architecture, and added new dependencies for testing utilities.
191 lines
5.8 KiB
TypeScript
191 lines
5.8 KiB
TypeScript
/**
|
|
* Component: Match Library Processor Tests
|
|
* Documentation: documentation/phase3/README.md
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createPrismaMock } from '../helpers/prisma';
|
|
|
|
const prismaMock = createPrismaMock();
|
|
const libraryServiceMock = vi.hoisted(() => ({ searchItems: vi.fn() }));
|
|
const configMock = vi.hoisted(() => ({
|
|
getBackendMode: vi.fn(),
|
|
get: vi.fn(),
|
|
getPlexConfig: vi.fn(),
|
|
}));
|
|
const compareTwoStringsMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('@/lib/db', () => ({
|
|
prisma: prismaMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/library', () => ({
|
|
getLibraryService: async () => libraryServiceMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/config.service', () => ({
|
|
getConfigService: () => configMock,
|
|
}));
|
|
|
|
vi.mock('string-similarity', () => ({
|
|
compareTwoStrings: compareTwoStringsMock,
|
|
}));
|
|
|
|
describe('processMatchPlex', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('completes request when no library results are found', async () => {
|
|
configMock.getBackendMode.mockResolvedValue('plex');
|
|
configMock.getPlexConfig.mockResolvedValue({ libraryId: 'plex-lib' });
|
|
libraryServiceMock.searchItems.mockResolvedValue([]);
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
const { processMatchPlex } = await import('@/lib/processors/match-plex.processor');
|
|
const result = await processMatchPlex({
|
|
requestId: 'req-1',
|
|
audiobookId: 'ab-1',
|
|
title: 'Missing Title',
|
|
author: 'Author',
|
|
jobId: 'job-1',
|
|
});
|
|
|
|
expect(result.matched).toBe(false);
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: 'req-1' },
|
|
data: expect.objectContaining({ status: 'completed' }),
|
|
})
|
|
);
|
|
expect(prismaMock.audiobook.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('updates audiobook and request when a high-score match is found (plex)', async () => {
|
|
configMock.getBackendMode.mockResolvedValue('plex');
|
|
configMock.getPlexConfig.mockResolvedValue({ libraryId: 'plex-lib' });
|
|
libraryServiceMock.searchItems.mockResolvedValue([
|
|
{
|
|
id: 'item-1',
|
|
externalId: 'guid-1',
|
|
title: 'Best Match',
|
|
author: 'Author',
|
|
},
|
|
]);
|
|
compareTwoStringsMock.mockReturnValue(0.95);
|
|
prismaMock.audiobook.update.mockResolvedValue({});
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
const { processMatchPlex } = await import('@/lib/processors/match-plex.processor');
|
|
const result = await processMatchPlex({
|
|
requestId: 'req-2',
|
|
audiobookId: 'ab-2',
|
|
title: 'Best Match',
|
|
author: 'Author',
|
|
jobId: 'job-2',
|
|
});
|
|
|
|
expect(result.matched).toBe(true);
|
|
expect(prismaMock.audiobook.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: 'ab-2' },
|
|
data: expect.objectContaining({ plexGuid: 'guid-1' }),
|
|
})
|
|
);
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: 'req-2' },
|
|
data: expect.objectContaining({ status: 'completed' }),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('uses audiobookshelf IDs when backend mode is audiobookshelf', async () => {
|
|
configMock.getBackendMode.mockResolvedValue('audiobookshelf');
|
|
configMock.get.mockResolvedValue('abs-lib');
|
|
libraryServiceMock.searchItems.mockResolvedValue([
|
|
{
|
|
id: 'item-abs',
|
|
externalId: 'abs-1',
|
|
title: 'Shelf Match',
|
|
author: 'Author',
|
|
},
|
|
]);
|
|
compareTwoStringsMock.mockReturnValue(0.9);
|
|
prismaMock.audiobook.update.mockResolvedValue({});
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
const { processMatchPlex } = await import('@/lib/processors/match-plex.processor');
|
|
const result = await processMatchPlex({
|
|
requestId: 'req-3',
|
|
audiobookId: 'ab-3',
|
|
title: 'Shelf Match',
|
|
author: 'Author',
|
|
jobId: 'job-3',
|
|
});
|
|
|
|
expect(result.matched).toBe(true);
|
|
expect(prismaMock.audiobook.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ absItemId: 'abs-1' }),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('completes request without match when score is too low', async () => {
|
|
configMock.getBackendMode.mockResolvedValue('plex');
|
|
configMock.getPlexConfig.mockResolvedValue({ libraryId: 'plex-lib' });
|
|
libraryServiceMock.searchItems.mockResolvedValue([
|
|
{
|
|
id: 'item-low',
|
|
externalId: 'guid-low',
|
|
title: 'Low Match',
|
|
author: 'Author',
|
|
},
|
|
]);
|
|
compareTwoStringsMock.mockReturnValue(0.1);
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
const { processMatchPlex } = await import('@/lib/processors/match-plex.processor');
|
|
const result = await processMatchPlex({
|
|
requestId: 'req-4',
|
|
audiobookId: 'ab-4',
|
|
title: 'Low Match',
|
|
author: 'Author',
|
|
jobId: 'job-4',
|
|
});
|
|
|
|
expect(result.matched).toBe(false);
|
|
expect(prismaMock.audiobook.update).not.toHaveBeenCalled();
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ status: 'completed' }),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('marks request completed with error when matching fails', async () => {
|
|
configMock.getBackendMode.mockResolvedValue('plex');
|
|
configMock.getPlexConfig.mockResolvedValue({ libraryId: null });
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
const { processMatchPlex } = await import('@/lib/processors/match-plex.processor');
|
|
const result = await processMatchPlex({
|
|
requestId: 'req-5',
|
|
audiobookId: 'ab-5',
|
|
title: 'Error Title',
|
|
author: 'Author',
|
|
jobId: 'job-5',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({ status: 'completed' }),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
|