mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add Transmission/NZBGet and per-client paths and much more
Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
This commit is contained in:
@@ -14,6 +14,9 @@ const requireAdminMock = vi.hoisted(() => vi.fn());
|
||||
const configServiceMock = vi.hoisted(() => ({ get: vi.fn() }));
|
||||
const qbittorrentMock = vi.hoisted(() => ({ getTorrent: vi.fn() }));
|
||||
const sabnzbdMock = vi.hoisted(() => ({ getNZB: vi.fn() }));
|
||||
const downloadClientManagerMock = vi.hoisted(() => ({
|
||||
getClientServiceForProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
prisma: prismaMock,
|
||||
@@ -36,12 +39,17 @@ vi.mock('@/lib/integrations/sabnzbd.service', () => ({
|
||||
getSABnzbdService: async () => sabnzbdMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/download-client-manager.service', () => ({
|
||||
getDownloadClientManager: () => downloadClientManagerMock,
|
||||
}));
|
||||
|
||||
describe('Admin downloads route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
authRequest = { user: { id: 'admin-1', role: 'admin' } };
|
||||
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
|
||||
requireAdminMock.mockImplementation((_req: any, handler: any) => handler());
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockReset();
|
||||
});
|
||||
|
||||
it('returns formatted active downloads', async () => {
|
||||
@@ -53,11 +61,15 @@ describe('Admin downloads route', () => {
|
||||
updatedAt: new Date(),
|
||||
audiobook: { title: 'Title', author: 'Author' },
|
||||
user: { plexUsername: 'user' },
|
||||
downloadHistory: [{ torrentHash: 'hash', torrentName: 'Torrent', downloadStatus: 'downloading' }],
|
||||
downloadHistory: [{ torrentHash: 'hash', torrentName: 'Torrent', downloadStatus: 'downloading', downloadClient: 'qbittorrent' }],
|
||||
},
|
||||
]);
|
||||
configServiceMock.get.mockResolvedValueOnce('qbittorrent');
|
||||
qbittorrentMock.getTorrent.mockResolvedValueOnce({ dlspeed: 123, eta: 60 });
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValueOnce({
|
||||
getDownload: vi.fn().mockResolvedValue({
|
||||
downloadSpeed: 123,
|
||||
eta: 60,
|
||||
}),
|
||||
});
|
||||
|
||||
const { GET } = await import('@/app/api/admin/downloads/active/route');
|
||||
const response = await GET({} as any);
|
||||
@@ -76,11 +88,15 @@ describe('Admin downloads route', () => {
|
||||
updatedAt: new Date(),
|
||||
audiobook: { title: 'Title', author: 'Author' },
|
||||
user: { plexUsername: 'user' },
|
||||
downloadHistory: [{ nzbId: 'nzb-1', torrentName: 'NZB', downloadStatus: 'downloading' }],
|
||||
downloadHistory: [{ nzbId: 'nzb-1', torrentName: 'NZB', downloadStatus: 'downloading', downloadClient: 'sabnzbd' }],
|
||||
},
|
||||
]);
|
||||
configServiceMock.get.mockResolvedValueOnce('sabnzbd');
|
||||
sabnzbdMock.getNZB.mockResolvedValueOnce({ downloadSpeed: 555, timeLeft: 120 });
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValueOnce({
|
||||
getDownload: vi.fn().mockResolvedValue({
|
||||
downloadSpeed: 555,
|
||||
eta: 120,
|
||||
}),
|
||||
});
|
||||
|
||||
const { GET } = await import('@/app/api/admin/downloads/active/route');
|
||||
const response = await GET({} as any);
|
||||
@@ -99,11 +115,12 @@ describe('Admin downloads route', () => {
|
||||
updatedAt: new Date(),
|
||||
audiobook: { title: 'Title', author: 'Author' },
|
||||
user: { plexUsername: 'user' },
|
||||
downloadHistory: [{ torrentHash: 'hash', torrentName: 'Torrent', downloadStatus: 'downloading' }],
|
||||
downloadHistory: [{ torrentHash: 'hash', torrentName: 'Torrent', downloadStatus: 'downloading', downloadClient: 'qbittorrent' }],
|
||||
},
|
||||
]);
|
||||
configServiceMock.get.mockResolvedValueOnce('qbittorrent');
|
||||
qbittorrentMock.getTorrent.mockRejectedValueOnce(new Error('client down'));
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValueOnce({
|
||||
getDownload: vi.fn().mockRejectedValue(new Error('client down')),
|
||||
});
|
||||
|
||||
const { GET } = await import('@/app/api/admin/downloads/active/route');
|
||||
const response = await GET({} as any);
|
||||
|
||||
@@ -152,8 +152,8 @@ describe('Admin settings core routes', () => {
|
||||
it('rejects invalid download client types', async () => {
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({
|
||||
type: 'transmission',
|
||||
url: 'http://transmission',
|
||||
type: 'deluge',
|
||||
url: 'http://deluge',
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const configServiceMock = vi.hoisted(() => ({
|
||||
}));
|
||||
const downloadClientManagerMock = vi.hoisted(() => ({
|
||||
getAllClients: vi.fn(),
|
||||
testConnection: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
@@ -70,6 +71,13 @@ vi.mock('@/lib/integrations/sabnzbd.service', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/integrations/transmission.service', () => ({
|
||||
TransmissionService: class {
|
||||
constructor() {}
|
||||
testConnection = vi.fn();
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/ebook-scraper', () => ({
|
||||
testFlareSolverrConnection: testFlareSolverrMock,
|
||||
}));
|
||||
@@ -189,7 +197,10 @@ describe('Admin settings test routes', () => {
|
||||
});
|
||||
|
||||
it('tests download client connection', async () => {
|
||||
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.0.0');
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Successfully connected to qbittorrent (v4.0.0)',
|
||||
});
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({ type: 'qbittorrent', url: 'http://qbt', username: 'user', password: 'pass' }),
|
||||
};
|
||||
@@ -199,7 +210,7 @@ describe('Admin settings test routes', () => {
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.version).toBe('4.0.0');
|
||||
expect(payload.message).toContain('4.0.0');
|
||||
});
|
||||
|
||||
it('validates required fields for download client testing', async () => {
|
||||
@@ -229,7 +240,10 @@ describe('Admin settings test routes', () => {
|
||||
downloadClientManagerMock.getAllClients.mockResolvedValueOnce([
|
||||
{ type: 'qbittorrent', password: 'stored-pass' },
|
||||
]);
|
||||
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.1.0');
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Successfully connected to qbittorrent (v4.1.0)',
|
||||
});
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({
|
||||
type: 'qbittorrent',
|
||||
@@ -244,12 +258,6 @@ describe('Admin settings test routes', () => {
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(qbtMock.testConnectionWithCredentials).toHaveBeenCalledWith(
|
||||
'http://qbt',
|
||||
'user',
|
||||
'stored-pass',
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error when masked password is missing in storage', async () => {
|
||||
@@ -273,7 +281,10 @@ describe('Admin settings test routes', () => {
|
||||
});
|
||||
|
||||
it('returns error when SABnzbd connection fails', async () => {
|
||||
sabnzbdMock.testConnection.mockResolvedValueOnce({ success: false, error: 'bad key' });
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: false,
|
||||
message: 'bad key',
|
||||
});
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({ type: 'sabnzbd', url: 'http://sab', password: 'key' }),
|
||||
};
|
||||
@@ -282,12 +293,15 @@ describe('Admin settings test routes', () => {
|
||||
const response = await POST(request as any);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload.error).toMatch(/bad key/);
|
||||
});
|
||||
|
||||
it('requires path mapping values when enabled', async () => {
|
||||
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.0.0');
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Connected',
|
||||
});
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({
|
||||
type: 'qbittorrent',
|
||||
@@ -307,7 +321,10 @@ describe('Admin settings test routes', () => {
|
||||
});
|
||||
|
||||
it('rejects inaccessible local path when mapping is enabled', async () => {
|
||||
qbtMock.testConnectionWithCredentials.mockResolvedValueOnce('4.0.0');
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Connected',
|
||||
});
|
||||
fsMock.access.mockRejectedValueOnce(new Error('missing'));
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('Audiobooks search torrents route', () => {
|
||||
.mockResolvedValueOnce(JSON.stringify([{ id: 1, name: 'Indexer', protocol: 'torrent', priority: 10 }]))
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
groupIndexersMock.mockReturnValue([{ categories: [1], indexerIds: [1] }]);
|
||||
groupIndexersMock.mockReturnValue({ groups: [{ categories: [1], indexerIds: [1] }], skippedIndexers: [] });
|
||||
prowlarrMock.search.mockResolvedValue([{ title: 'Result', size: 100, indexer: 'Indexer', indexerId: 1 }]);
|
||||
rankTorrentsMock.mockReturnValue([
|
||||
{
|
||||
|
||||
@@ -79,6 +79,10 @@ describe('Request action routes', () => {
|
||||
userId: 'user-1',
|
||||
audiobook: { title: 'Title', author: 'Author' },
|
||||
});
|
||||
prismaMock.user.findUnique.mockResolvedValueOnce({
|
||||
role: 'user',
|
||||
interactiveSearchAccess: null,
|
||||
});
|
||||
configServiceMock.get.mockResolvedValueOnce(JSON.stringify([{ id: 1, priority: 10 }]));
|
||||
configServiceMock.get.mockResolvedValueOnce(null);
|
||||
prowlarrMock.search.mockResolvedValueOnce([{ title: 'Result', size: 100 }]);
|
||||
|
||||
@@ -468,6 +468,7 @@ describe('Request Approval Workflow', () => {
|
||||
plexUsername: true,
|
||||
role: true,
|
||||
autoApproveRequests: true,
|
||||
interactiveSearchAccess: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ const jobQueueMock = vi.hoisted(() => ({ addSearchJob: vi.fn(), addOrganizeJob:
|
||||
const requireAuthMock = vi.hoisted(() => vi.fn());
|
||||
const qbtMock = vi.hoisted(() => ({ getTorrent: vi.fn() }));
|
||||
const sabnzbdMock = vi.hoisted(() => ({ getNZB: vi.fn() }));
|
||||
const downloadClientManagerMock = vi.hoisted(() => ({
|
||||
getClientServiceForProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
prisma: prismaMock,
|
||||
@@ -30,6 +33,14 @@ vi.mock('@/lib/integrations/sabnzbd.service', () => ({
|
||||
getSABnzbdService: async () => sabnzbdMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/download-client-manager.service', () => ({
|
||||
getDownloadClientManager: () => downloadClientManagerMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/config.service', () => ({
|
||||
getConfigService: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/middleware/auth', () => ({
|
||||
requireAuth: requireAuthMock,
|
||||
}));
|
||||
@@ -43,6 +54,7 @@ describe('Request by ID API routes', () => {
|
||||
json: vi.fn(),
|
||||
};
|
||||
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockReset();
|
||||
});
|
||||
|
||||
it('returns 403 when user is not authorized to view the request', async () => {
|
||||
@@ -200,9 +212,14 @@ describe('Request by ID API routes', () => {
|
||||
id: 'req-5',
|
||||
userId: 'user-1',
|
||||
audiobook: { id: 'ab-5' },
|
||||
downloadHistory: [{ torrentHash: 'hash-1', selected: true }],
|
||||
downloadHistory: [{ torrentHash: 'hash-1', selected: true, downloadClient: 'qbittorrent' }],
|
||||
});
|
||||
qbtMock.getTorrent.mockResolvedValue({ save_path: '/downloads', name: 'Book' });
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValue({
|
||||
clientType: 'qbittorrent',
|
||||
getDownload: vi.fn().mockResolvedValue({
|
||||
downloadPath: '/downloads/Book',
|
||||
}),
|
||||
});
|
||||
prismaMock.request.update.mockResolvedValueOnce({
|
||||
id: 'req-5',
|
||||
status: 'processing',
|
||||
@@ -230,9 +247,14 @@ describe('Request by ID API routes', () => {
|
||||
id: 'req-6',
|
||||
userId: 'user-1',
|
||||
audiobook: { id: 'ab-6' },
|
||||
downloadHistory: [{ nzbId: 'nzb-1', selected: true }],
|
||||
downloadHistory: [{ nzbId: 'nzb-1', selected: true, downloadClient: 'sabnzbd' }],
|
||||
});
|
||||
sabnzbdMock.getNZB.mockResolvedValue({ downloadPath: '/usenet/book' });
|
||||
downloadClientManagerMock.getClientServiceForProtocol.mockResolvedValue({
|
||||
clientType: 'sabnzbd',
|
||||
getDownload: vi.fn().mockResolvedValue({
|
||||
downloadPath: '/usenet/book',
|
||||
}),
|
||||
});
|
||||
prismaMock.request.update.mockResolvedValueOnce({
|
||||
id: 'req-6',
|
||||
status: 'processing',
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Component: Setup Route Guard Tests
|
||||
* Documentation: documentation/testing.md
|
||||
*
|
||||
* Verifies that all setup API endpoints return 403 after setup is complete.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
configuration: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
// Mock all external dependencies that setup routes import
|
||||
vi.mock('@/lib/integrations/plex.service', () => ({
|
||||
getPlexService: () => ({
|
||||
testConnection: vi.fn(),
|
||||
getLibraries: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/integrations/prowlarr.service', () => ({
|
||||
ProwlarrService: class {
|
||||
constructor() {}
|
||||
getIndexers = vi.fn();
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('openid-client', () => ({
|
||||
Issuer: { discover: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
default: {
|
||||
access: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
},
|
||||
access: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
constants: { R_OK: 4 },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/config.service', () => ({
|
||||
getConfigService: () => ({ get: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/download-client-manager.service', () => ({
|
||||
getDownloadClientManager: () => ({ testConnection: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/encryption.service', () => ({
|
||||
getEncryptionService: () => ({ encrypt: vi.fn((v: string) => `enc-${v}`) }),
|
||||
}));
|
||||
|
||||
vi.mock('bcrypt', () => ({
|
||||
default: { hash: vi.fn() },
|
||||
hash: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/utils/jwt', () => ({
|
||||
generateAccessToken: vi.fn(() => 'token'),
|
||||
generateRefreshToken: vi.fn(() => 'token'),
|
||||
}));
|
||||
|
||||
function mockSetupComplete() {
|
||||
prismaMock.configuration.findUnique.mockResolvedValue({ key: 'setup_completed', value: 'true' });
|
||||
}
|
||||
|
||||
function makeRequest(body: Record<string, unknown> = {}) {
|
||||
return {
|
||||
json: vi.fn().mockResolvedValue(body),
|
||||
nextUrl: { pathname: '/api/setup/test' },
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe('Setup route guard - blocks access after setup is complete', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSetupComplete();
|
||||
});
|
||||
|
||||
it('POST /api/setup/complete returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/complete/route');
|
||||
const response = await POST(makeRequest({ backendMode: 'plex' }));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
expect(payload.message).toMatch(/Setup has already been completed/);
|
||||
});
|
||||
|
||||
it('POST /api/setup/test-download-client returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST(makeRequest({ type: 'qbittorrent', url: 'http://qbt' }));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('POST /api/setup/test-plex returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-plex/route');
|
||||
const response = await POST(makeRequest({ url: 'http://plex', token: 'token' }));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('POST /api/setup/test-prowlarr returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-prowlarr/route');
|
||||
const response = await POST(makeRequest({ url: 'http://prowlarr', apiKey: 'key' }));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('POST /api/setup/test-paths returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-paths/route');
|
||||
const response = await POST(makeRequest({ downloadDir: '/downloads', mediaDir: '/media' }));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('POST /api/setup/test-abs returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-abs/route');
|
||||
const response = await POST(makeRequest({ serverUrl: 'http://abs', apiToken: 'token' }));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('POST /api/setup/test-oidc returns 403 when setup is already complete', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-oidc/route');
|
||||
const response = await POST(makeRequest({
|
||||
issuerUrl: 'http://issuer',
|
||||
clientId: 'client',
|
||||
clientSecret: 'secret',
|
||||
}));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(payload.error).toBe('Forbidden');
|
||||
});
|
||||
|
||||
it('allows requests through when setup is not yet complete', async () => {
|
||||
// Override: setup not complete
|
||||
prismaMock.configuration.findUnique.mockResolvedValue(null);
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST(makeRequest({ type: 'qbittorrent', url: 'http://qbt' }));
|
||||
const payload = await response.json();
|
||||
|
||||
// Should reach the handler (not 403), even if the actual test fails
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
|
||||
it('allows requests through when database is not ready', async () => {
|
||||
// Override: database error
|
||||
prismaMock.configuration.findUnique.mockRejectedValue(new Error('DB not ready'));
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST(makeRequest({ type: 'qbittorrent', url: 'http://qbt' }));
|
||||
const payload = await response.json();
|
||||
|
||||
// Should reach the handler (not 403) — DB not ready means setup hasn't happened
|
||||
expect(response.status).not.toBe(403);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,12 @@
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const setupGuardPrismaMock = vi.hoisted(() => ({
|
||||
configuration: {
|
||||
findUnique: vi.fn(), // returns undefined by default = setup not complete
|
||||
},
|
||||
}));
|
||||
|
||||
const plexServiceMock = vi.hoisted(() => ({
|
||||
testConnection: vi.fn(),
|
||||
getLibraries: vi.fn(),
|
||||
@@ -30,6 +36,9 @@ const fsMock = vi.hoisted(() => ({
|
||||
const configServiceMock = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
}));
|
||||
const downloadClientManagerMock = vi.hoisted(() => ({
|
||||
testConnection: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/integrations/plex.service', () => ({
|
||||
getPlexService: () => plexServiceMock,
|
||||
@@ -48,6 +57,13 @@ vi.mock('@/lib/integrations/sabnzbd.service', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/integrations/transmission.service', () => ({
|
||||
TransmissionService: class {
|
||||
constructor() {}
|
||||
testConnection = vi.fn();
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/integrations/prowlarr.service', () => ({
|
||||
ProwlarrService: class {
|
||||
constructor() {}
|
||||
@@ -59,12 +75,20 @@ vi.mock('openid-client', () => ({
|
||||
Issuer: issuerMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
prisma: setupGuardPrismaMock,
|
||||
}));
|
||||
|
||||
vi.mock('fs/promises', () => ({ default: fsMock, ...fsMock, constants: { R_OK: 4 } }));
|
||||
|
||||
vi.mock('@/lib/services/config.service', () => ({
|
||||
getConfigService: () => configServiceMock,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/download-client-manager.service', () => ({
|
||||
getDownloadClientManager: () => downloadClientManagerMock,
|
||||
}));
|
||||
|
||||
describe('Setup test routes', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -135,7 +159,10 @@ describe('Setup test routes', () => {
|
||||
});
|
||||
|
||||
it('tests qBittorrent credentials', async () => {
|
||||
qbtMock.testConnectionWithCredentials.mockResolvedValue('4.0.0');
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Successfully connected to qBittorrent (v4.0.0)',
|
||||
});
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST({
|
||||
@@ -149,13 +176,13 @@ describe('Setup test routes', () => {
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.version).toBe('4.0.0');
|
||||
expect(payload.message).toContain('4.0.0');
|
||||
});
|
||||
|
||||
it('rejects invalid download client type', async () => {
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST({
|
||||
json: vi.fn().mockResolvedValue({ type: 'transmission', url: 'http://transmission' }),
|
||||
json: vi.fn().mockResolvedValue({ type: 'deluge', url: 'http://deluge' }),
|
||||
} as any);
|
||||
const payload = await response.json();
|
||||
|
||||
@@ -163,7 +190,33 @@ describe('Setup test routes', () => {
|
||||
expect(payload.error).toMatch(/Invalid client type/);
|
||||
});
|
||||
|
||||
it('tests Transmission credentials', async () => {
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Successfully connected to Transmission (v4.0.5)',
|
||||
});
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST({
|
||||
json: vi.fn().mockResolvedValue({
|
||||
type: 'transmission',
|
||||
url: 'http://transmission:9091',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
}),
|
||||
} as any);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.message).toContain('Transmission');
|
||||
});
|
||||
|
||||
it('rejects missing SABnzbd API key', async () => {
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: false,
|
||||
message: 'API key is required for SABnzbd',
|
||||
});
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST({
|
||||
json: vi.fn().mockResolvedValue({ type: 'sabnzbd', url: 'http://sab' }),
|
||||
@@ -175,7 +228,10 @@ describe('Setup test routes', () => {
|
||||
});
|
||||
|
||||
it('tests SABnzbd connection', async () => {
|
||||
sabnzbdMock.testConnection.mockResolvedValue({ success: true, version: '3.0' });
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: true,
|
||||
message: 'Successfully connected to SABnzbd (v3.0)',
|
||||
});
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST({
|
||||
@@ -188,11 +244,14 @@ describe('Setup test routes', () => {
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.version).toBe('3.0');
|
||||
expect(payload.message).toContain('3.0');
|
||||
});
|
||||
|
||||
it('returns error when SABnzbd connection fails', async () => {
|
||||
sabnzbdMock.testConnection.mockResolvedValue({ success: false, error: 'bad key' });
|
||||
downloadClientManagerMock.testConnection.mockResolvedValueOnce({
|
||||
success: false,
|
||||
message: 'bad key',
|
||||
});
|
||||
|
||||
const { POST } = await import('@/app/api/setup/test-download-client/route');
|
||||
const response = await POST({
|
||||
@@ -204,7 +263,7 @@ describe('Setup test routes', () => {
|
||||
} as any);
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.status).toBe(400);
|
||||
expect(payload.error).toMatch(/bad key/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user