mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
09e1a0db3a
Replace default Anna's Archive base URL from https://annas-archive.li to https://annas-archive.gl across docs, UI components, API routes, processors, services, and tests. Add comprehensive tests for the admin manual-import API route and enhance the manual-import route to fetch missing ASIN details from Audnexus and create audiobook records with proper error handling and logging. Update related test expectations and FlareSolverr test usages to reflect the new default URL.
340 lines
10 KiB
TypeScript
340 lines
10 KiB
TypeScript
/**
|
|
* Component: Search Ebook Processor Tests
|
|
* Documentation: documentation/integrations/ebook-sidecar.md
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createPrismaMock } from '../helpers/prisma';
|
|
|
|
const prismaMock = createPrismaMock();
|
|
|
|
const configServiceMock = vi.hoisted(() => ({
|
|
get: vi.fn(),
|
|
getAudibleRegion: vi.fn().mockResolvedValue('us'),
|
|
}));
|
|
|
|
const jobQueueMock = vi.hoisted(() => ({
|
|
addStartDirectDownloadJob: vi.fn(() => Promise.resolve()),
|
|
}));
|
|
|
|
const ebookScraperMock = vi.hoisted(() => ({
|
|
searchByAsin: vi.fn(),
|
|
searchByTitle: vi.fn(),
|
|
getSlowDownloadLinks: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/db', () => ({
|
|
prisma: prismaMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/config.service', () => ({
|
|
getConfigService: () => configServiceMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/job-queue.service', () => ({
|
|
getJobQueueService: () => jobQueueMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/services/ebook-scraper', () => ebookScraperMock);
|
|
|
|
describe('processSearchEbook', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
configServiceMock.getAudibleRegion.mockResolvedValue('us');
|
|
configServiceMock.get.mockImplementation(async (key: string) => {
|
|
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
|
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
|
|
if (key === 'ebook_annas_archive_enabled') return 'true';
|
|
if (key === 'ebook_indexer_search_enabled') return 'false';
|
|
return null;
|
|
});
|
|
});
|
|
|
|
it('searches by ASIN when available and triggers download', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-1' });
|
|
prismaMock.downloadHistory.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByAsin.mockResolvedValue('abc123md5');
|
|
ebookScraperMock.getSlowDownloadLinks.mockResolvedValue([
|
|
'https://slow1.example.com/abc123',
|
|
'https://slow2.example.com/abc123',
|
|
]);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
const result = await processSearchEbook({
|
|
requestId: 'req-1',
|
|
audiobook: {
|
|
id: 'ab-1',
|
|
title: 'Test Book',
|
|
author: 'Test Author',
|
|
asin: 'B001ASIN',
|
|
},
|
|
jobId: 'job-1',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain("Anna's Archive");
|
|
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
|
'B001ASIN',
|
|
'epub',
|
|
'https://annas-archive.gl',
|
|
expect.anything(),
|
|
undefined,
|
|
'en'
|
|
);
|
|
expect(jobQueueMock.addStartDirectDownloadJob).toHaveBeenCalledWith(
|
|
'req-1',
|
|
'dh-1',
|
|
'https://slow1.example.com/abc123',
|
|
'Test Book - Test Author.epub',
|
|
undefined
|
|
);
|
|
});
|
|
|
|
it('falls back to title search when ASIN search fails', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-2' });
|
|
prismaMock.downloadHistory.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByAsin.mockResolvedValue(null);
|
|
ebookScraperMock.searchByTitle.mockResolvedValue('xyz789md5');
|
|
ebookScraperMock.getSlowDownloadLinks.mockResolvedValue([
|
|
'https://slow1.example.com/xyz789',
|
|
]);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
const result = await processSearchEbook({
|
|
requestId: 'req-2',
|
|
audiobook: {
|
|
id: 'ab-2',
|
|
title: 'Another Book',
|
|
author: 'Another Author',
|
|
asin: 'B002ASIN',
|
|
},
|
|
jobId: 'job-2',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain("Anna's Archive");
|
|
expect(ebookScraperMock.searchByAsin).toHaveBeenCalled();
|
|
expect(ebookScraperMock.searchByTitle).toHaveBeenCalledWith(
|
|
'Another Book',
|
|
'Another Author',
|
|
'epub',
|
|
'https://annas-archive.gl',
|
|
expect.anything(),
|
|
undefined,
|
|
'en'
|
|
);
|
|
});
|
|
|
|
it('searches by title when no ASIN is available', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-3' });
|
|
prismaMock.downloadHistory.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByTitle.mockResolvedValue('noasin123');
|
|
ebookScraperMock.getSlowDownloadLinks.mockResolvedValue([
|
|
'https://slow.example.com/noasin123',
|
|
]);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
const result = await processSearchEbook({
|
|
requestId: 'req-3',
|
|
audiobook: {
|
|
id: 'ab-3',
|
|
title: 'No ASIN Book',
|
|
author: 'No ASIN Author',
|
|
// No asin field
|
|
},
|
|
jobId: 'job-3',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(ebookScraperMock.searchByAsin).not.toHaveBeenCalled();
|
|
expect(ebookScraperMock.searchByTitle).toHaveBeenCalled();
|
|
});
|
|
|
|
it('marks request as awaiting_search when no ebook found', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByAsin.mockResolvedValue(null);
|
|
ebookScraperMock.searchByTitle.mockResolvedValue(null);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
const result = await processSearchEbook({
|
|
requestId: 'req-4',
|
|
audiobook: {
|
|
id: 'ab-4',
|
|
title: 'Unfindable Book',
|
|
author: 'Unknown Author',
|
|
asin: 'B004ASIN',
|
|
},
|
|
jobId: 'job-4',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('re-search');
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith({
|
|
where: { id: 'req-4' },
|
|
data: expect.objectContaining({
|
|
status: 'awaiting_search',
|
|
errorMessage: expect.stringContaining('No ebook found'),
|
|
lastSearchAt: expect.any(Date),
|
|
}),
|
|
});
|
|
expect(jobQueueMock.addStartDirectDownloadJob).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('marks request as awaiting_search when no download links available', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByAsin.mockResolvedValue('md5nolinks');
|
|
ebookScraperMock.getSlowDownloadLinks.mockResolvedValue([]);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
const result = await processSearchEbook({
|
|
requestId: 'req-5',
|
|
audiobook: {
|
|
id: 'ab-5',
|
|
title: 'Book No Links',
|
|
author: 'Author No Links',
|
|
asin: 'B005ASIN',
|
|
},
|
|
jobId: 'job-5',
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('re-search');
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith({
|
|
where: { id: 'req-5' },
|
|
data: expect.objectContaining({
|
|
status: 'awaiting_search',
|
|
errorMessage: expect.stringContaining('No ebook found'),
|
|
lastSearchAt: expect.any(Date),
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('uses FlareSolverr when configured', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-6' });
|
|
prismaMock.downloadHistory.update.mockResolvedValue({});
|
|
|
|
configServiceMock.get.mockImplementation(async (key: string) => {
|
|
if (key === 'ebook_sidecar_preferred_format') return 'epub';
|
|
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
|
|
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
|
|
if (key === 'ebook_annas_archive_enabled') return 'true';
|
|
if (key === 'ebook_indexer_search_enabled') return 'false';
|
|
return null;
|
|
});
|
|
|
|
ebookScraperMock.searchByAsin.mockResolvedValue('md5withflare');
|
|
ebookScraperMock.getSlowDownloadLinks.mockResolvedValue(['https://slow.example.com/flare']);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
await processSearchEbook({
|
|
requestId: 'req-6',
|
|
audiobook: {
|
|
id: 'ab-6',
|
|
title: 'Flare Book',
|
|
author: 'Flare Author',
|
|
asin: 'B006ASIN',
|
|
},
|
|
jobId: 'job-6',
|
|
});
|
|
|
|
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
|
|
'B006ASIN',
|
|
'epub',
|
|
'https://annas-archive.gl',
|
|
expect.anything(),
|
|
'http://flaresolverr:8191',
|
|
'en'
|
|
);
|
|
});
|
|
|
|
it('fails request on unexpected errors', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByAsin.mockRejectedValue(new Error('Network error'));
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
await expect(processSearchEbook({
|
|
requestId: 'req-7',
|
|
audiobook: {
|
|
id: 'ab-7',
|
|
title: 'Error Book',
|
|
author: 'Error Author',
|
|
asin: 'B007ASIN',
|
|
},
|
|
jobId: 'job-7',
|
|
})).rejects.toThrow('Network error');
|
|
|
|
expect(prismaMock.request.update).toHaveBeenCalledWith({
|
|
where: { id: 'req-7' },
|
|
data: expect.objectContaining({
|
|
status: 'failed',
|
|
errorMessage: 'Network error',
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('creates download history with correct metadata', async () => {
|
|
prismaMock.request.update.mockResolvedValue({});
|
|
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-8' });
|
|
prismaMock.downloadHistory.update.mockResolvedValue({});
|
|
|
|
ebookScraperMock.searchByAsin.mockResolvedValue('md5metadata');
|
|
ebookScraperMock.getSlowDownloadLinks.mockResolvedValue([
|
|
'https://link1.example.com',
|
|
'https://link2.example.com',
|
|
]);
|
|
|
|
const { processSearchEbook } = await import('@/lib/processors/search-ebook.processor');
|
|
|
|
await processSearchEbook({
|
|
requestId: 'req-8',
|
|
audiobook: {
|
|
id: 'ab-8',
|
|
title: 'Metadata Book',
|
|
author: 'Metadata Author',
|
|
asin: 'B008ASIN',
|
|
},
|
|
jobId: 'job-8',
|
|
});
|
|
|
|
expect(prismaMock.downloadHistory.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
requestId: 'req-8',
|
|
indexerName: "Anna's Archive",
|
|
torrentName: 'Metadata Book - Metadata Author.epub',
|
|
downloadClient: 'direct',
|
|
downloadStatus: 'queued',
|
|
selected: true,
|
|
qualityScore: 100, // ASIN match = 100
|
|
}),
|
|
});
|
|
|
|
// Check that all URLs are stored
|
|
expect(prismaMock.downloadHistory.update).toHaveBeenCalledWith({
|
|
where: { id: 'dh-8' },
|
|
data: {
|
|
torrentUrl: JSON.stringify([
|
|
'https://link1.example.com',
|
|
'https://link2.example.com',
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
});
|