Use .gl for Anna's Archive; add manual-import test

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.
This commit is contained in:
kikootwo
2026-03-05 12:20:00 -05:00
parent 832a8ad00b
commit 09e1a0db3a
23 changed files with 338 additions and 48 deletions
+5 -5
View File
@@ -51,7 +51,7 @@ Ebooks are first-class citizens in RMAB, with their own request type, tracking,
| Key | Default | Description | | Key | Default | Description |
|-----|---------|-------------| |-----|---------|-------------|
| `ebook_annas_archive_enabled` | `false` | Enable Anna's Archive downloads | | `ebook_annas_archive_enabled` | `false` | Enable Anna's Archive downloads |
| `ebook_sidecar_base_url` | `https://annas-archive.li` | Base URL for mirror | | `ebook_sidecar_base_url` | `https://annas-archive.gl` | Base URL for mirror |
| `ebook_sidecar_flaresolverr_url` | `` (empty) | FlareSolverr proxy URL (optional) | | `ebook_sidecar_flaresolverr_url` | `` (empty) | FlareSolverr proxy URL (optional) |
#### Section 2: Indexer Search #### Section 2: Indexer Search
@@ -180,18 +180,18 @@ Configure URL in Admin Settings → E-book Sidecar: `http://localhost:8191`
### Method 1: ASIN Search (exact match) ### Method 1: ASIN Search (exact match)
``` ```
Search: https://annas-archive.li/search?ext=epub&lang=en&q="asin:B09TWSRMCB" Search: https://annas-archive.gl/search?ext=epub&lang=en&q="asin:B09TWSRMCB"
MD5 Page: https://annas-archive.li/md5/[md5] MD5 Page: https://annas-archive.gl/md5/[md5]
Slow Download: https://annas-archive.li/slow_download/[md5]/0/5 Slow Download: https://annas-archive.gl/slow_download/[md5]/0/5
File Server: http://[server]/path/to/file.epub File Server: http://[server]/path/to/file.epub
``` ```
### Method 2: Title + Author (fallback) ### Method 2: Title + Author (fallback)
``` ```
Search: https://annas-archive.li/search?q=Title+Author&ext=epub&lang=en Search: https://annas-archive.gl/search?q=Title+Author&ext=epub&lang=en
↓ (Same flow from MD5 page) ↓ (Same flow from MD5 page)
``` ```
+2 -2
View File
@@ -81,7 +81,7 @@ src/app/admin/settings/
1. **Anna's Archive Section** 1. **Anna's Archive Section**
- Enable toggle for Anna's Archive downloads - Enable toggle for Anna's Archive downloads
- Base URL (default: `https://annas-archive.li`) - Base URL (default: `https://annas-archive.gl`)
- FlareSolverr URL (optional, for Cloudflare bypass) - FlareSolverr URL (optional, for Cloudflare bypass)
2. **Indexer Search Section** 2. **Indexer Search Section**
@@ -101,7 +101,7 @@ src/app/admin/settings/
| `ebook_sidecar_preferred_format` | `epub` | Preferred format | | `ebook_sidecar_preferred_format` | `epub` | Preferred format |
| `ebook_auto_grab_enabled` | `true` | Auto-create ebook requests after audiobook downloads | | `ebook_auto_grab_enabled` | `true` | Auto-create ebook requests after audiobook downloads |
| `ebook_kindle_fix_enabled` | `false` | Apply Kindle compatibility fixes to EPUB files | | `ebook_kindle_fix_enabled` | `false` | Apply Kindle compatibility fixes to EPUB files |
| `ebook_sidecar_base_url` | `https://annas-archive.li` | Anna's Archive mirror | | `ebook_sidecar_base_url` | `https://annas-archive.gl` | Anna's Archive mirror |
| `ebook_sidecar_flaresolverr_url` | `` | FlareSolverr URL | | `ebook_sidecar_flaresolverr_url` | `` | FlareSolverr URL |
**Behavior:** **Behavior:**
@@ -163,7 +163,7 @@ function getInitialParams(): {
}; };
} }
export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveBaseUrl = 'https://annas-archive.li' }: RecentRequestsTableProps) { export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveBaseUrl = 'https://annas-archive.gl' }: RecentRequestsTableProps) {
const toast = useToast(); const toast = useToast();
// Get initial filter state from URL (only evaluated once due to lazy init) // Get initial filter state from URL (only evaluated once due to lazy init)
@@ -47,7 +47,7 @@ export function RequestActionsDropdown({
onFetchEbook, onFetchEbook,
onSearchTermsUpdated, onSearchTermsUpdated,
ebookSidecarEnabled = false, ebookSidecarEnabled = false,
annasArchiveBaseUrl = 'https://annas-archive.li', annasArchiveBaseUrl = 'https://annas-archive.gl',
isLoading = false, isLoading = false,
}: RequestActionsDropdownProps) { }: RequestActionsDropdownProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -90,9 +90,9 @@ export function EbookTab({ ebook, onChange, onSuccess, onError, markAsSaved }: E
</label> </label>
<Input <Input
type="text" type="text"
value={ebook.baseUrl || 'https://annas-archive.li'} value={ebook.baseUrl || 'https://annas-archive.gl'}
onChange={(e) => updateEbook('baseUrl', e.target.value)} onChange={(e) => updateEbook('baseUrl', e.target.value)}
placeholder="https://annas-archive.li" placeholder="https://annas-archive.gl"
className="font-mono" className="font-mono"
/> />
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1"> <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
@@ -53,7 +53,7 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
url: ebook.flaresolverrUrl, url: ebook.flaresolverrUrl,
baseUrl: ebook.baseUrl || 'https://annas-archive.li', baseUrl: ebook.baseUrl || 'https://annas-archive.gl',
}), }),
}); });
@@ -83,7 +83,7 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
annasArchiveEnabled: ebook.annasArchiveEnabled || false, annasArchiveEnabled: ebook.annasArchiveEnabled || false,
indexerSearchEnabled: ebook.indexerSearchEnabled || false, indexerSearchEnabled: ebook.indexerSearchEnabled || false,
format: ebook.preferredFormat || 'epub', format: ebook.preferredFormat || 'epub',
baseUrl: ebook.baseUrl || 'https://annas-archive.li', baseUrl: ebook.baseUrl || 'https://annas-archive.gl',
flaresolverrUrl: ebook.flaresolverrUrl || '', flaresolverrUrl: ebook.flaresolverrUrl || '',
autoGrabEnabled: ebook.autoGrabEnabled ?? true, autoGrabEnabled: ebook.autoGrabEnabled ?? true,
kindleFixEnabled: ebook.kindleFixEnabled ?? false, kindleFixEnabled: ebook.kindleFixEnabled ?? false,
+32
View File
@@ -154,12 +154,44 @@ export async function POST(request: NextRequest) {
}); });
audiobookId = newBook.id; audiobookId = newBook.id;
logger.info(`Created audiobook record from cache for ASIN ${asin}: ${newBook.id}`); logger.info(`Created audiobook record from cache for ASIN ${asin}: ${newBook.id}`);
} else {
// Not in DB — fetch live from Audnexus and create a record
try {
const audibleService = getAudibleService();
const liveData = await audibleService.getAudiobookDetails(asin);
if (liveData) {
const newBook = await prisma.audiobook.create({
data: {
audibleAsin: asin,
title: liveData.title,
author: liveData.author,
coverArtUrl: liveData.coverArtUrl,
narrator: liveData.narrator,
series: liveData.series,
seriesPart: liveData.seriesPart,
seriesAsin: liveData.seriesAsin,
year: liveData.releaseDate
? new Date(liveData.releaseDate).getFullYear() || undefined
: undefined,
status: 'pending',
},
});
audiobookId = newBook.id;
logger.info(`Created audiobook record from Audnexus for ASIN ${asin}: ${newBook.id}`);
} else { } else {
return NextResponse.json( return NextResponse.json(
{ error: 'Audiobook not found for the given ASIN' }, { error: 'Audiobook not found for the given ASIN' },
{ status: 404 } { status: 404 }
); );
} }
} catch (audnexusError) {
logger.error(`Failed to fetch ASIN ${asin} from Audnexus: ${audnexusError instanceof Error ? audnexusError.message : String(audnexusError)}`);
return NextResponse.json(
{ error: 'Audiobook not found for the given ASIN' },
{ status: 404 }
);
}
}
} }
} }
+1 -1
View File
@@ -78,7 +78,7 @@ export async function PUT(request: NextRequest) {
// Anna's Archive specific settings // Anna's Archive specific settings
{ {
key: 'ebook_sidecar_base_url', key: 'ebook_sidecar_base_url',
value: baseUrl || 'https://annas-archive.li', value: baseUrl || 'https://annas-archive.gl',
category: 'ebook', category: 'ebook',
description: 'Base URL for Anna\'s Archive', description: 'Base URL for Anna\'s Archive',
}, },
+1 -1
View File
@@ -138,7 +138,7 @@ export async function GET(request: NextRequest) {
(configMap.get('ebook_annas_archive_enabled') === undefined && configMap.get('ebook_sidecar_enabled') === 'true'), (configMap.get('ebook_annas_archive_enabled') === undefined && configMap.get('ebook_sidecar_enabled') === 'true'),
indexerSearchEnabled: configMap.get('ebook_indexer_search_enabled') === 'true', indexerSearchEnabled: configMap.get('ebook_indexer_search_enabled') === 'true',
// Anna's Archive specific settings // Anna's Archive specific settings
baseUrl: configMap.get('ebook_sidecar_base_url') || 'https://annas-archive.li', baseUrl: configMap.get('ebook_sidecar_base_url') || 'https://annas-archive.gl',
flaresolverrUrl: configMap.get('ebook_sidecar_flaresolverr_url') || '', flaresolverrUrl: configMap.get('ebook_sidecar_flaresolverr_url') || '',
// General settings // General settings
preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub', preferredFormat: configMap.get('ebook_sidecar_preferred_format') || 'epub',
@@ -227,7 +227,7 @@ export async function POST(
const isAnnasArchiveEnabled = annasArchiveEnabled === 'true'; const isAnnasArchiveEnabled = annasArchiveEnabled === 'true';
const isIndexerSearchEnabled = indexerSearchEnabled === 'true'; const isIndexerSearchEnabled = indexerSearchEnabled === 'true';
const format = preferredFormat || 'epub'; const format = preferredFormat || 'epub';
const annasBaseUrl = baseUrl || 'https://annas-archive.li'; const annasBaseUrl = baseUrl || 'https://annas-archive.gl';
// Get language code from Audible region config // Get language code from Audible region config
const region = await configService.getAudibleRegion() as AudibleRegion; const region = await configService.getAudibleRegion() as AudibleRegion;
@@ -136,7 +136,7 @@ export async function POST(
const isAnnasArchiveEnabled = annasArchiveEnabled === 'true'; const isAnnasArchiveEnabled = annasArchiveEnabled === 'true';
const isIndexerSearchEnabled = indexerSearchEnabled === 'true'; const isIndexerSearchEnabled = indexerSearchEnabled === 'true';
const format = preferredFormat || 'epub'; const format = preferredFormat || 'epub';
const annasBaseUrl = baseUrl || 'https://annas-archive.li'; const annasBaseUrl = baseUrl || 'https://annas-archive.gl';
// Get language code from Audible region config // Get language code from Audible region config
const region = await configService.getAudibleRegion() as AudibleRegion; const region = await configService.getAudibleRegion() as AudibleRegion;
@@ -79,7 +79,7 @@ export async function processStartDirectDownload(payload: StartDirectDownloadPay
// Get download configuration // Get download configuration
const configService = getConfigService(); const configService = getConfigService();
const downloadsDir = await configService.get('download_dir') || '/downloads'; const downloadsDir = await configService.get('download_dir') || '/downloads';
const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.li'; const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.gl';
const preferredFormat = await configService.get('ebook_sidecar_preferred_format') || 'epub'; const preferredFormat = await configService.get('ebook_sidecar_preferred_format') || 'epub';
const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined; const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined;
+1 -1
View File
@@ -150,7 +150,7 @@ async function searchAnnasArchive(
logger: RMABLogger logger: RMABLogger
): Promise<EbookSearchResult | null> { ): Promise<EbookSearchResult | null> {
const configService = getConfigService(); const configService = getConfigService();
const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.li'; const baseUrl = await configService.get('ebook_sidecar_base_url') || 'https://annas-archive.gl';
const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined; const flaresolverrUrl = await configService.get('ebook_sidecar_flaresolverr_url') || undefined;
// Get language code from Audible region config // Get language code from Audible region config
+2 -2
View File
@@ -128,7 +128,7 @@ async function fetchHtml(
*/ */
export async function testFlareSolverrConnection( export async function testFlareSolverrConnection(
flaresolverrUrl: string, flaresolverrUrl: string,
baseUrl: string = 'https://annas-archive.li' baseUrl: string = 'https://annas-archive.gl'
): Promise<{ success: boolean; message: string; responseTime?: number }> { ): Promise<{ success: boolean; message: string; responseTime?: number }> {
const startTime = Date.now(); const startTime = Date.now();
@@ -168,7 +168,7 @@ export async function downloadEbook(
author: string, author: string,
targetDir: string, targetDir: string,
preferredFormat: string = 'epub', preferredFormat: string = 'epub',
baseUrl: string = 'https://annas-archive.li', baseUrl: string = 'https://annas-archive.gl',
logger?: RMABLogger, logger?: RMABLogger,
flaresolverrUrl?: string, flaresolverrUrl?: string,
languageCode: string = 'en' languageCode: string = 'en'
@@ -0,0 +1,258 @@
/**
* Component: Admin Manual Import API Route Tests
* Documentation: documentation/features/manual-import.md
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPrismaMock } from '../helpers/prisma';
let authRequest: any;
let requestBody: any;
const prismaMock = createPrismaMock();
const requireAuthMock = vi.hoisted(() => vi.fn());
const requireAdminMock = vi.hoisted(() => vi.fn());
const jobQueueMock = vi.hoisted(() => ({
addOrganizeJob: vi.fn(() => Promise.resolve()),
}));
const audibleServiceMock = vi.hoisted(() => ({
getAudiobookDetails: vi.fn(),
}));
// fs mock
const fsMock = vi.hoisted(() => ({
stat: vi.fn(),
readdir: vi.fn(),
}));
vi.mock('@/lib/db', () => ({
prisma: prismaMock,
}));
vi.mock('@/lib/middleware/auth', () => ({
requireAuth: requireAuthMock,
requireAdmin: requireAdminMock,
AuthenticatedRequest: {},
}));
vi.mock('@/lib/services/job-queue.service', () => ({
getJobQueueService: () => jobQueueMock,
}));
vi.mock('@/lib/integrations/audible.service', () => ({
getAudibleService: () => audibleServiceMock,
}));
vi.mock('fs/promises', () => fsMock);
vi.mock('path', async () => {
const actual = await vi.importActual<typeof import('path')>('path');
return {
...actual,
default: actual,
resolve: (...args: string[]) => actual.posix.resolve(...args),
extname: actual.posix.extname,
};
});
describe('Admin manual-import route', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
authRequest = { user: { id: 'admin-1', role: 'admin' } };
requestBody = { asin: 'B00TEST0001', folderPath: '/bookdrop/author/title' };
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
requireAdminMock.mockImplementation((_req: any, handler: any) => handler());
// Default: download_dir and media_dir not configured, bookdrop exists
prismaMock.configuration.findUnique.mockResolvedValue(null);
fsMock.stat.mockImplementation(async (p: string) => {
if (p === '/bookdrop') return { isDirectory: () => true };
if (p === '/bookdrop/author/title') return { isDirectory: () => true };
throw new Error('ENOENT');
});
fsMock.readdir.mockResolvedValue([
{ name: 'chapter1.m4b', isFile: () => true },
]);
});
it('creates audiobook from Audnexus when ASIN is not in DB or cache', async () => {
// Neither audiobook nor audibleCache has this ASIN
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
prismaMock.audibleCache.findUnique.mockResolvedValueOnce(null);
// Audnexus returns live data
audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce({
asin: 'B00TEST0001',
title: 'Live Title',
author: 'Live Author',
coverArtUrl: 'https://example.com/cover.jpg',
narrator: 'Live Narrator',
series: 'Test Series',
seriesPart: '1',
seriesAsin: 'SERIES0001',
releaseDate: '2024-01-15',
});
// audiobook.create returns the new record
prismaMock.audiobook.create.mockResolvedValueOnce({
id: 'ab-new',
audibleAsin: 'B00TEST0001',
title: 'Live Title',
author: 'Live Author',
status: 'pending',
});
// audiobook.findUnique for the verification step
prismaMock.audiobook.findUnique.mockResolvedValueOnce({
id: 'ab-new',
audibleAsin: 'B00TEST0001',
title: 'Live Title',
author: 'Live Author',
status: 'pending',
});
// No existing request
prismaMock.request.findFirst.mockResolvedValueOnce(null);
prismaMock.request.create.mockResolvedValueOnce({ id: 'req-new' });
const { POST } = await import('@/app/api/admin/manual-import/route');
const request = {
json: vi.fn().mockResolvedValue(requestBody),
nextUrl: new URL('http://localhost/api/admin/manual-import'),
};
const response = await POST(request as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
expect(audibleServiceMock.getAudiobookDetails).toHaveBeenCalledWith('B00TEST0001');
expect(prismaMock.audiobook.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
audibleAsin: 'B00TEST0001',
title: 'Live Title',
author: 'Live Author',
}),
})
);
});
it('returns 404 when ASIN is not in DB, cache, or Audnexus', async () => {
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
prismaMock.audibleCache.findUnique.mockResolvedValueOnce(null);
audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce(null);
const { POST } = await import('@/app/api/admin/manual-import/route');
const request = {
json: vi.fn().mockResolvedValue(requestBody),
nextUrl: new URL('http://localhost/api/admin/manual-import'),
};
const response = await POST(request as any);
const payload = await response.json();
expect(response.status).toBe(404);
expect(payload.error).toBe('Audiobook not found for the given ASIN');
});
it('returns 404 when Audnexus lookup throws an error', async () => {
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
prismaMock.audibleCache.findUnique.mockResolvedValueOnce(null);
audibleServiceMock.getAudiobookDetails.mockRejectedValueOnce(new Error('Network timeout'));
const { POST } = await import('@/app/api/admin/manual-import/route');
const request = {
json: vi.fn().mockResolvedValue(requestBody),
nextUrl: new URL('http://localhost/api/admin/manual-import'),
};
const response = await POST(request as any);
const payload = await response.json();
expect(response.status).toBe(404);
expect(payload.error).toBe('Audiobook not found for the given ASIN');
});
it('uses existing audiobook record when ASIN is in DB', async () => {
prismaMock.audiobook.findFirst.mockResolvedValueOnce({
id: 'ab-existing',
audibleAsin: 'B00TEST0001',
});
prismaMock.audiobook.findUnique.mockResolvedValueOnce({
id: 'ab-existing',
audibleAsin: 'B00TEST0001',
title: 'Existing Title',
author: 'Existing Author',
status: 'pending',
});
prismaMock.request.findFirst.mockResolvedValueOnce(null);
prismaMock.request.create.mockResolvedValueOnce({ id: 'req-1' });
const { POST } = await import('@/app/api/admin/manual-import/route');
const request = {
json: vi.fn().mockResolvedValue(requestBody),
nextUrl: new URL('http://localhost/api/admin/manual-import'),
};
const response = await POST(request as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
// Should NOT have queried audibleCache for ASIN resolution
expect(prismaMock.audibleCache.findUnique).not.toHaveBeenCalled();
});
it('uses audibleCache when ASIN is not in audiobook table but is cached', async () => {
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
prismaMock.audibleCache.findUnique.mockResolvedValueOnce({
asin: 'B00TEST0001',
title: 'Cached Title',
author: 'Cached Author',
coverArtUrl: 'https://example.com/cached.jpg',
narrator: 'Cached Narrator',
});
// audiobook.create from cache
prismaMock.audiobook.create.mockResolvedValueOnce({
id: 'ab-from-cache',
audibleAsin: 'B00TEST0001',
title: 'Cached Title',
author: 'Cached Author',
status: 'pending',
});
prismaMock.audiobook.findUnique.mockResolvedValueOnce({
id: 'ab-from-cache',
audibleAsin: 'B00TEST0001',
title: 'Cached Title',
author: 'Cached Author',
status: 'pending',
});
prismaMock.request.findFirst.mockResolvedValueOnce(null);
prismaMock.request.create.mockResolvedValueOnce({ id: 'req-2' });
const { POST } = await import('@/app/api/admin/manual-import/route');
const request = {
json: vi.fn().mockResolvedValue(requestBody),
nextUrl: new URL('http://localhost/api/admin/manual-import'),
};
const response = await POST(request as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
// audiobook.create should have used cache data, not Audnexus
expect(prismaMock.audiobook.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
title: 'Cached Title',
author: 'Cached Author',
}),
})
);
});
});
+1 -1
View File
@@ -355,7 +355,7 @@ describe('Admin settings core routes', () => {
it('updates ebook settings', async () => { it('updates ebook settings', async () => {
const request = { const request = {
json: vi.fn().mockResolvedValue({ enabled: true, format: 'epub', baseUrl: 'https://annas-archive.li' }), json: vi.fn().mockResolvedValue({ enabled: true, format: 'epub', baseUrl: 'https://annas-archive.gl' }),
}; };
const { PUT } = await import('@/app/api/admin/settings/ebook/route'); const { PUT } = await import('@/app/api/admin/settings/ebook/route');
@@ -348,14 +348,14 @@ describe('Admin settings test routes', () => {
it('tests FlareSolverr connection', async () => { it('tests FlareSolverr connection', async () => {
testFlareSolverrMock.mockResolvedValueOnce({ success: true }); testFlareSolverrMock.mockResolvedValueOnce({ success: true });
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.li' }) }; const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.gl' }) };
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route'); const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
const response = await POST(request as any); const response = await POST(request as any);
const payload = await response.json(); const payload = await response.json();
expect(payload.success).toBe(true); expect(payload.success).toBe(true);
expect(testFlareSolverrMock).toHaveBeenCalledWith('http://flare', 'https://annas-archive.li'); expect(testFlareSolverrMock).toHaveBeenCalledWith('http://flare', 'https://annas-archive.gl');
}); });
it('rejects FlareSolverr test when URL is missing', async () => { it('rejects FlareSolverr test when URL is missing', async () => {
@@ -382,7 +382,7 @@ describe('Admin settings test routes', () => {
it('returns error when FlareSolverr test throws', async () => { it('returns error when FlareSolverr test throws', async () => {
testFlareSolverrMock.mockRejectedValueOnce(new Error('flare down')); testFlareSolverrMock.mockRejectedValueOnce(new Error('flare down'));
const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.li' }) }; const request = { json: vi.fn().mockResolvedValue({ url: 'http://flare', baseUrl: 'https://annas-archive.gl' }) };
const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route'); const { POST } = await import('@/app/api/admin/settings/ebook/test-flaresolverr/route');
const response = await POST(request as any); const response = await POST(request as any);
@@ -87,7 +87,7 @@ describe('RequestActionsDropdown', () => {
author: 'Author', author: 'Author',
status: 'downloaded', status: 'downloaded',
type: 'ebook', type: 'ebook',
torrentUrl: JSON.stringify(['https://annas-archive.li/slow_download/abc123def456abc123def456abc123de/0/5']), torrentUrl: JSON.stringify(['https://annas-archive.gl/slow_download/abc123def456abc123def456abc123de/0/5']),
}} }}
onManualSearch={vi.fn().mockResolvedValue(undefined)} onManualSearch={vi.fn().mockResolvedValue(undefined)}
onCancel={vi.fn().mockResolvedValue(undefined)} onCancel={vi.fn().mockResolvedValue(undefined)}
@@ -28,7 +28,7 @@ const renderHook = <T,>(hook: () => T) => {
const baseEbook = { const baseEbook = {
enabled: true, enabled: true,
preferredFormat: 'epub', preferredFormat: 'epub',
baseUrl: 'https://annas-archive.li', baseUrl: 'https://annas-archive.gl',
flaresolverrUrl: 'http://flare', flaresolverrUrl: 'http://flare',
}; };
@@ -93,7 +93,7 @@ describe('useEbookSettings', () => {
expect(result.current.flaresolverrTestResult?.success).toBe(true); expect(result.current.flaresolverrTestResult?.success).toBe(true);
// Verify baseUrl is included in the request body // Verify baseUrl is included in the request body
const callBody = JSON.parse(fetchWithAuthMock.mock.calls[0][1].body); const callBody = JSON.parse(fetchWithAuthMock.mock.calls[0][1].body);
expect(callBody.baseUrl).toBe('https://annas-archive.li'); expect(callBody.baseUrl).toBe('https://annas-archive.gl');
expect(callBody.url).toBe('http://flare'); expect(callBody.url).toBe('http://flare');
}); });
@@ -115,7 +115,7 @@ describe('IndexersTab - Auto-load Indexers on Tab Activation', () => {
ebook: { ebook: {
enabled: false, enabled: false,
preferredFormat: 'epub', preferredFormat: 'epub',
baseUrl: 'https://annas-archive.li', baseUrl: 'https://annas-archive.gl',
flaresolverrUrl: '', flaresolverrUrl: '',
}, },
}; };
@@ -63,7 +63,7 @@ describe('processStartDirectDownload', () => {
vi.clearAllMocks(); vi.clearAllMocks();
configServiceMock.get.mockImplementation(async (key: string) => { configServiceMock.get.mockImplementation(async (key: string) => {
if (key === 'downloads_dir') return '/downloads'; if (key === 'downloads_dir') return '/downloads';
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li'; if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
if (key === 'ebook_sidecar_preferred_format') return 'epub'; if (key === 'ebook_sidecar_preferred_format') return 'epub';
return null; return null;
}); });
@@ -238,7 +238,7 @@ describe('processStartDirectDownload', () => {
configServiceMock.get.mockImplementation(async (key: string) => { configServiceMock.get.mockImplementation(async (key: string) => {
if (key === 'downloads_dir') return '/downloads'; if (key === 'downloads_dir') return '/downloads';
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li'; if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
if (key === 'ebook_sidecar_preferred_format') return 'epub'; if (key === 'ebook_sidecar_preferred_format') return 'epub';
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191'; if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
return null; return null;
@@ -286,7 +286,7 @@ describe('processStartDirectDownload', () => {
expect(ebookScraperMock.extractDownloadUrl).toHaveBeenCalledWith( expect(ebookScraperMock.extractDownloadUrl).toHaveBeenCalledWith(
'https://slow.example.com/book', 'https://slow.example.com/book',
'https://annas-archive.li', 'https://annas-archive.gl',
'epub', 'epub',
expect.anything(), expect.anything(),
'http://flaresolverr:8191' 'http://flaresolverr:8191'
@@ -43,7 +43,7 @@ describe('processSearchEbook', () => {
configServiceMock.getAudibleRegion.mockResolvedValue('us'); configServiceMock.getAudibleRegion.mockResolvedValue('us');
configServiceMock.get.mockImplementation(async (key: string) => { configServiceMock.get.mockImplementation(async (key: string) => {
if (key === 'ebook_sidecar_preferred_format') return 'epub'; if (key === 'ebook_sidecar_preferred_format') return 'epub';
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li'; if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
if (key === 'ebook_annas_archive_enabled') return 'true'; if (key === 'ebook_annas_archive_enabled') return 'true';
if (key === 'ebook_indexer_search_enabled') return 'false'; if (key === 'ebook_indexer_search_enabled') return 'false';
return null; return null;
@@ -79,7 +79,7 @@ describe('processSearchEbook', () => {
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith( expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
'B001ASIN', 'B001ASIN',
'epub', 'epub',
'https://annas-archive.li', 'https://annas-archive.gl',
expect.anything(), expect.anything(),
undefined, undefined,
'en' 'en'
@@ -124,7 +124,7 @@ describe('processSearchEbook', () => {
'Another Book', 'Another Book',
'Another Author', 'Another Author',
'epub', 'epub',
'https://annas-archive.li', 'https://annas-archive.gl',
expect.anything(), expect.anything(),
undefined, undefined,
'en' 'en'
@@ -229,7 +229,7 @@ describe('processSearchEbook', () => {
configServiceMock.get.mockImplementation(async (key: string) => { configServiceMock.get.mockImplementation(async (key: string) => {
if (key === 'ebook_sidecar_preferred_format') return 'epub'; if (key === 'ebook_sidecar_preferred_format') return 'epub';
if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.li'; if (key === 'ebook_sidecar_base_url') return 'https://annas-archive.gl';
if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191'; if (key === 'ebook_sidecar_flaresolverr_url') return 'http://flaresolverr:8191';
if (key === 'ebook_annas_archive_enabled') return 'true'; if (key === 'ebook_annas_archive_enabled') return 'true';
if (key === 'ebook_indexer_search_enabled') return 'false'; if (key === 'ebook_indexer_search_enabled') return 'false';
@@ -255,7 +255,7 @@ describe('processSearchEbook', () => {
expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith( expect(ebookScraperMock.searchByAsin).toHaveBeenCalledWith(
'B006ASIN', 'B006ASIN',
'epub', 'epub',
'https://annas-archive.li', 'https://annas-archive.gl',
expect.anything(), expect.anything(),
'http://flaresolverr:8191', 'http://flaresolverr:8191',
'en' 'en'
+7 -7
View File
@@ -63,7 +63,7 @@ describe('E-book sidecar', () => {
}, },
}); });
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li'); const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.responseTime).toBeTypeOf('number'); expect(result.responseTime).toBeTypeOf('number');
@@ -95,7 +95,7 @@ describe('E-book sidecar', () => {
}, },
}); });
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li'); const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
expect(result.success).toBe(false); expect(result.success).toBe(false);
}); });
@@ -103,7 +103,7 @@ describe('E-book sidecar', () => {
it('returns error details when FlareSolverr request fails', async () => { it('returns error details when FlareSolverr request fails', async () => {
axiosMock.post.mockRejectedValue(new Error('flare down')); axiosMock.post.mockRejectedValue(new Error('flare down'));
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li'); const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.message).toContain('flare down'); expect(result.message).toContain('flare down');
@@ -117,7 +117,7 @@ describe('E-book sidecar', () => {
}, },
}); });
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li'); const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.message).toContain('FlareSolverr error'); expect(result.message).toContain('FlareSolverr error');
@@ -132,7 +132,7 @@ describe('E-book sidecar', () => {
}, },
}); });
const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.li'); const result = await testFlareSolverrConnection('http://flare', 'https://annas-archive.gl');
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.message).toContain('FlareSolverr returned HTTP 403'); expect(result.message).toContain('FlareSolverr returned HTTP 403');
@@ -221,7 +221,7 @@ describe('E-book sidecar', () => {
throw new Error(`Unexpected URL: ${url}`); throw new Error(`Unexpected URL: ${url}`);
}); });
const promise = downloadEbook('ASIN-NO', 'Title', 'Author', '/downloads', 'pdf', 'https://annas-archive.li', undefined, 'http://flare'); const promise = downloadEbook('ASIN-NO', 'Title', 'Author', '/downloads', 'pdf', 'https://annas-archive.gl', undefined, 'http://flare');
await vi.runAllTimersAsync(); await vi.runAllTimersAsync();
const result = await promise; const result = await promise;
@@ -417,7 +417,7 @@ describe('E-book sidecar', () => {
'Author', 'Author',
'/downloads', '/downloads',
'epub', 'epub',
'https://annas-archive.li', 'https://annas-archive.gl',
undefined, undefined,
'http://flare' 'http://flare'
); );