mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-09-01 05:08:38 +00:00
fix: try next release after transient grab failure (#275)
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import path from 'path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { QBittorrentService, getQBittorrentService, invalidateQBittorrentService } from '@/lib/integrations/qbittorrent.service';
|
||||
import { DownloadSourceError } from '@/lib/interfaces/download-client.interface';
|
||||
|
||||
const clientMock = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
@@ -693,6 +694,32 @@ describe('QBittorrentService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves upstream HTTP status and forwards source headers', async () => {
|
||||
const service = new QBittorrentService('http://qb', 'user', 'pass');
|
||||
(service as any).cookie = 'SID=source-error';
|
||||
vi.spyOn(service as any, 'ensureCategory').mockResolvedValue(undefined);
|
||||
|
||||
axiosMock.get.mockRejectedValueOnce({
|
||||
isAxiosError: true,
|
||||
response: { status: 500, headers: {} },
|
||||
message: 'Request failed',
|
||||
});
|
||||
|
||||
const error = await service.addDownload(
|
||||
'https://prowlarr/1/download/limited',
|
||||
{ sourceHeaders: { 'X-Api-Key': 'secret' } }
|
||||
).catch((caught) => caught);
|
||||
|
||||
expect(error).toBeInstanceOf(DownloadSourceError);
|
||||
expect(error).toMatchObject({ status: 500 });
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'https://prowlarr/1/download/limited',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'X-Api-Key': 'secret' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for invalid redirect locations when fetching torrents', async () => {
|
||||
const service = new QBittorrentService('http://qb', 'user', 'pass');
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createPrismaMock } from '../helpers/prisma';
|
||||
import { createJobQueueMock } from '../helpers/job-queue';
|
||||
import { DownloadSourceError } from '@/lib/interfaces/download-client.interface';
|
||||
|
||||
const prismaMock = createPrismaMock();
|
||||
const configMock = vi.hoisted(() => ({
|
||||
@@ -214,4 +215,107 @@ describe('processDownloadTorrent', () => {
|
||||
|
||||
expect(downloadClientManagerMock.getClientServiceForProtocol).toHaveBeenCalledWith('usenet');
|
||||
});
|
||||
|
||||
it('tries the next ranked release when an indexer grab returns 500', async () => {
|
||||
const fallbackTorrent = {
|
||||
...torrentPayload.torrent,
|
||||
indexer: 'Healthy Indexer',
|
||||
indexerId: 2,
|
||||
title: 'Book - Author - Fallback',
|
||||
downloadUrl: 'https://prowlarr/2/download/fallback',
|
||||
guid: 'guid-fallback',
|
||||
};
|
||||
const qbtClientMock = {
|
||||
clientType: 'qbittorrent',
|
||||
protocol: 'torrent',
|
||||
addDownload: vi.fn()
|
||||
.mockRejectedValueOnce(new DownloadSourceError(
|
||||
'Grab failed: source returned HTTP 500',
|
||||
500,
|
||||
'https://prowlarr/1/download/limited'
|
||||
))
|
||||
.mockResolvedValueOnce('fallback-hash'),
|
||||
};
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValue(qbtClientMock);
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValue({
|
||||
id: 'client-1',
|
||||
type: 'qbittorrent',
|
||||
enabled: true,
|
||||
category: 'readmeabook',
|
||||
});
|
||||
prismaMock.request.update.mockResolvedValue({
|
||||
type: 'audiobook',
|
||||
user: { plexUsername: 'testuser' },
|
||||
});
|
||||
prismaMock.downloadHistory.create.mockResolvedValue({ id: 'dh-fallback' });
|
||||
|
||||
const { processDownloadTorrent } = await import('@/lib/processors/download-torrent.processor');
|
||||
const result = await processDownloadTorrent({
|
||||
...torrentPayload,
|
||||
torrent: {
|
||||
...torrentPayload.torrent,
|
||||
downloadUrl: 'https://prowlarr/1/download/limited',
|
||||
},
|
||||
alternateTorrents: [fallbackTorrent],
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.torrent.title).toBe(fallbackTorrent.title);
|
||||
expect(qbtClientMock.addDownload).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://prowlarr/1/download/limited',
|
||||
expect.anything()
|
||||
);
|
||||
expect(qbtClientMock.addDownload).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
fallbackTorrent.downloadUrl,
|
||||
expect.anything()
|
||||
);
|
||||
expect(prismaMock.downloadHistory.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
indexerName: 'Healthy Indexer',
|
||||
torrentName: fallbackTorrent.title,
|
||||
downloadClientId: 'fallback-hash',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves the request retryable when every ranked grab is temporarily unavailable', async () => {
|
||||
const qbtClientMock = {
|
||||
clientType: 'qbittorrent',
|
||||
protocol: 'torrent',
|
||||
addDownload: vi.fn().mockRejectedValue(new DownloadSourceError(
|
||||
'Grab failed: source returned HTTP 429',
|
||||
429,
|
||||
'https://prowlarr/1/download/limited'
|
||||
)),
|
||||
};
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValue(qbtClientMock);
|
||||
downloadClientManagerMock.getClientForProtocol.mockResolvedValue({
|
||||
id: 'client-1',
|
||||
type: 'qbittorrent',
|
||||
enabled: true,
|
||||
category: 'readmeabook',
|
||||
});
|
||||
prismaMock.request.update.mockResolvedValue({
|
||||
type: 'audiobook',
|
||||
user: { plexUsername: 'testuser' },
|
||||
});
|
||||
|
||||
const { processDownloadTorrent } = await import('@/lib/processors/download-torrent.processor');
|
||||
await expect(processDownloadTorrent({
|
||||
...torrentPayload,
|
||||
torrent: {
|
||||
...torrentPayload.torrent,
|
||||
downloadUrl: 'https://prowlarr/1/download/limited',
|
||||
},
|
||||
})).rejects.toThrow('Indexer returned HTTP 429');
|
||||
|
||||
expect(prismaMock.request.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ data: expect.objectContaining({ status: 'failed' }) })
|
||||
);
|
||||
expect(prismaMock.downloadHistory.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,6 +88,17 @@ describe('processSearchIndexers', () => {
|
||||
guid: 'guid-1',
|
||||
format: 'M4B',
|
||||
},
|
||||
{
|
||||
indexer: 'Fallback Indexer',
|
||||
indexerId: 2,
|
||||
title: 'Book - Author - Alternate',
|
||||
size: 50 * 1024 * 1024,
|
||||
seeders: 1,
|
||||
publishDate: new Date(),
|
||||
downloadUrl: 'https://prowlarr/2/download/alternate',
|
||||
guid: 'guid-2',
|
||||
format: 'M4B',
|
||||
},
|
||||
]);
|
||||
|
||||
prismaMock.request.update.mockResolvedValue({});
|
||||
@@ -103,7 +114,8 @@ describe('processSearchIndexers', () => {
|
||||
expect(jobQueueMock.addDownloadJob).toHaveBeenCalledWith(
|
||||
'req-2',
|
||||
{ id: 'a2', title: 'Book', author: 'Author' },
|
||||
expect.objectContaining({ title: 'Book - Author' })
|
||||
expect.objectContaining({ title: 'Book - Author' }),
|
||||
[expect.objectContaining({ title: 'Book - Author - Alternate' })]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -179,7 +191,8 @@ describe('processSearchIndexers', () => {
|
||||
expect(jobQueueMock.addDownloadJob).toHaveBeenCalledWith(
|
||||
'req-filter-name',
|
||||
expect.objectContaining({ id: 'a-filter' }),
|
||||
expect.objectContaining({ title: 'Good Release - Author' })
|
||||
expect.objectContaining({ title: 'Good Release - Author' }),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -235,7 +248,8 @@ describe('processSearchIndexers', () => {
|
||||
expect(jobQueueMock.addDownloadJob).toHaveBeenCalledWith(
|
||||
'req-filter-hash',
|
||||
expect.anything(),
|
||||
expect.objectContaining({ title: 'Good Release - Author' })
|
||||
expect.objectContaining({ title: 'Good Release - Author' }),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -202,11 +202,20 @@ describe('JobQueueService', () => {
|
||||
|
||||
const { JobQueueService } = await import('@/lib/services/job-queue.service');
|
||||
const service = new JobQueueService();
|
||||
await service.addDownloadJob('req-1', { id: 'ab-1', title: 'Title', author: 'Author' }, { hash: 'hash' } as any);
|
||||
await service.addDownloadJob(
|
||||
'req-1',
|
||||
{ id: 'ab-1', title: 'Title', author: 'Author' },
|
||||
{ hash: 'hash' } as any,
|
||||
[{ hash: 'fallback-hash' } as any]
|
||||
);
|
||||
|
||||
expect(queueMock.add).toHaveBeenCalledWith(
|
||||
'download_torrent',
|
||||
expect.objectContaining({ requestId: 'req-1', jobId: 'job-2' }),
|
||||
expect.objectContaining({
|
||||
requestId: 'req-1',
|
||||
jobId: 'job-2',
|
||||
alternateTorrents: [{ hash: 'fallback-hash' }],
|
||||
}),
|
||||
expect.objectContaining({ priority: 9 })
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user