// Copyright (c) 2026 Gordon Bolton. MIT License. const { DownloadClientRegistry, registry, initializeClients, getAllClients, getClient, getClientsByType, getAllDownloads, getDownloadsByClientType, testAllConnections, getAllClientStatuses } = require('../../server/utils/downloadClients'); // Mock config and clients jest.mock('../../server/utils/config', () => ({ getSABnzbdInstances: jest.fn(), getQbittorrentInstances: jest.fn(), getTransmissionInstances: jest.fn() })); jest.mock('../../server/utils/logger', () => ({ logToFile: jest.fn() })); jest.mock('../../server/clients/SABnzbdClient', () => { return jest.fn().mockImplementation((config) => ({ getClientType: () => 'sabnzbd', getInstanceId: () => config.id, name: config.name, getActiveDownloads: jest.fn().mockResolvedValue([ { id: 'sab1', title: 'SAB Download 1', client: 'sabnzbd' } ]), testConnection: jest.fn().mockResolvedValue(true), getClientStatus: jest.fn().mockResolvedValue({ status: 'active' }) })); }); jest.mock('../../server/clients/QBittorrentClient', () => { return jest.fn().mockImplementation((config) => ({ getClientType: () => 'qbittorrent', getInstanceId: () => config.id, name: config.name, getActiveDownloads: jest.fn().mockResolvedValue([ { id: 'qb1', title: 'QB Download 1', client: 'qbittorrent' } ]), testConnection: jest.fn().mockResolvedValue(true), getClientStatus: jest.fn().mockResolvedValue({ status: 'active' }), resetFallbackFlag: jest.fn() })); }); jest.mock('../../server/clients/TransmissionClient', () => { return jest.fn().mockImplementation((config) => ({ getClientType: () => 'transmission', getInstanceId: () => config.id, name: config.name, getActiveDownloads: jest.fn().mockResolvedValue([ { id: 'trans1', title: 'Trans Download 1', client: 'transmission' } ]), testConnection: jest.fn().mockResolvedValue(true), getClientStatus: jest.fn().mockResolvedValue({ status: 'active' }) })); }); describe('DownloadClientRegistry', () => { let testRegistry; const mockConfig = require('../../server/utils/config'); beforeEach(() => { testRegistry = new DownloadClientRegistry(); jest.clearAllMocks(); }); describe('Initialization', () => { it('should initialize clients from config', async () => { mockConfig.getSABnzbdInstances.mockReturnValue([ { id: 'sab1', name: 'SAB 1', url: 'http://sab1', apiKey: 'key1' } ]); mockConfig.getQbittorrentInstances.mockReturnValue([ { id: 'qb1', name: 'QB 1', url: 'http://qb1', username: 'user', password: 'pass' } ]); mockConfig.getTransmissionInstances.mockReturnValue([ { id: 'trans1', name: 'Trans 1', url: 'http://trans1', username: 'user', password: 'pass' } ]); await testRegistry.initialize(); expect(testRegistry.getAllClients()).toHaveLength(3); expect(testRegistry.getClient('sab1')).toBeTruthy(); expect(testRegistry.getClient('qb1')).toBeTruthy(); expect(testRegistry.getClient('trans1')).toBeTruthy(); }); it('should handle empty config', async () => { mockConfig.getSABnzbdInstances.mockReturnValue([]); mockConfig.getQbittorrentInstances.mockReturnValue([]); mockConfig.getTransmissionInstances.mockReturnValue([]); await testRegistry.initialize(); expect(testRegistry.getAllClients()).toHaveLength(0); }); it('should not initialize twice', async () => { mockConfig.getSABnzbdInstances.mockReturnValue([]); mockConfig.getQbittorrentInstances.mockReturnValue([]); mockConfig.getTransmissionInstances.mockReturnValue([]); await testRegistry.initialize(); await testRegistry.initialize(); // Should not call config again expect(mockConfig.getSABnzbdInstances).toHaveBeenCalledTimes(1); }); it('should handle client creation errors gracefully', async () => { mockConfig.getSABnzbdInstances.mockReturnValue([ { id: 'invalid-sab', name: 'Invalid SAB' } // Missing required fields ]); await testRegistry.initialize(); expect(testRegistry.getAllClients()).toHaveLength(0); }); }); describe('Client Management', () => { beforeEach(async () => { mockConfig.getSABnzbdInstances.mockReturnValue([ { id: 'sab1', name: 'SAB 1', url: 'http://sab1', apiKey: 'key1' } ]); mockConfig.getQbittorrentInstances.mockReturnValue([]); mockConfig.getTransmissionInstances.mockReturnValue([]); await testRegistry.initialize(); }); it('should get all clients', () => { const clients = testRegistry.getAllClients(); expect(clients).toHaveLength(1); expect(clients[0].getClientType()).toBe('sabnzbd'); }); it('should get client by ID', () => { const client = testRegistry.getClient('sab1'); expect(client).toBeTruthy(); expect(client.getInstanceId()).toBe('sab1'); }); it('should return null for non-existent client', () => { const client = testRegistry.getClient('nonexistent'); expect(client).toBeNull(); }); it('should get clients by type', () => { const sabClients = testRegistry.getClientsByType('sabnzbd'); expect(sabClients).toHaveLength(1); const qbClients = testRegistry.getClientsByType('qbittorrent'); expect(qbClients).toHaveLength(0); }); }); describe('Download Management', () => { beforeEach(async () => { mockConfig.getSABnzbdInstances.mockReturnValue([ { id: 'sab1', name: 'SAB 1', url: 'http://sab1', apiKey: 'key1' } ]); mockConfig.getQbittorrentInstances.mockReturnValue([ { id: 'qb1', name: 'QB 1', url: 'http://qb1', username: 'user', password: 'pass' } ]); mockConfig.getTransmissionInstances.mockReturnValue([]); await testRegistry.initialize(); }); it('should get all downloads from all clients', async () => { const downloads = await testRegistry.getAllDownloads(); expect(downloads).toHaveLength(2); expect(downloads[0].client).toBe('sabnzbd'); expect(downloads[1].client).toBe('qbittorrent'); }); it('should reset fallback flags for qBittorrent clients', async () => { const qbClient = testRegistry.getClient('qb1'); await testRegistry.getAllDownloads(); expect(qbClient.resetFallbackFlag).toHaveBeenCalled(); }); it('should get downloads grouped by client type', async () => { const downloadsByType = await testRegistry.getDownloadsByClientType(); expect(downloadsByType.sabnzbd).toHaveLength(1); expect(downloadsByType.qbittorrent).toHaveLength(1); expect(downloadsByType.transmission).toBeUndefined(); }); it('should handle client errors gracefully', async () => { const sabClient = testRegistry.getClient('sab1'); sabClient.getActiveDownloads.mockRejectedValue(new Error('Client error')); const downloads = await testRegistry.getAllDownloads(); expect(downloads).toHaveLength(1); // Only qBittorrent succeeds }); }); describe('Connection Testing', () => { beforeEach(async () => { mockConfig.getSABnzbdInstances.mockReturnValue([ { id: 'sab1', name: 'SAB 1', url: 'http://sab1', apiKey: 'key1' } ]); mockConfig.getQbittorrentInstances.mockReturnValue([ { id: 'qb1', name: 'QB 1', url: 'http://qb1', username: 'user', password: 'pass' } ]); mockConfig.getTransmissionInstances.mockReturnValue([]); await testRegistry.initialize(); }); it('should test all connections', async () => { const results = await testRegistry.testAllConnections(); expect(results).toHaveLength(2); expect(results[0]).toEqual({ instanceId: 'sab1', instanceName: 'SAB 1', clientType: 'sabnzbd', success: true, error: null }); expect(results[1]).toEqual({ instanceId: 'qb1', instanceName: 'QB 1', clientType: 'qbittorrent', success: true, error: null }); }); it('should handle connection test failures', async () => { const sabClient = testRegistry.getClient('sab1'); sabClient.testConnection.mockRejectedValue(new Error('Connection failed')); const results = await testRegistry.testAllConnections(); expect(results[0].success).toBe(false); expect(results[0].error).toBe('Connection failed'); }); }); describe('Client Status', () => { beforeEach(async () => { mockConfig.getSABnzbdInstances.mockReturnValue([ { id: 'sab1', name: 'SAB 1', url: 'http://sab1', apiKey: 'key1' } ]); mockConfig.getQbittorrentInstances.mockReturnValue([]); mockConfig.getTransmissionInstances.mockReturnValue([]); await testRegistry.initialize(); }); it('should get all client statuses', async () => { const statuses = await testRegistry.getAllClientStatuses(); expect(statuses).toHaveLength(1); expect(statuses[0]).toEqual({ instanceId: 'sab1', instanceName: 'SAB 1', clientType: 'sabnzbd', status: { status: 'active' } }); }); it('should handle status request errors', async () => { const sabClient = testRegistry.getClient('sab1'); sabClient.getClientStatus.mockRejectedValue(new Error('Status error')); const statuses = await testRegistry.getAllClientStatuses(); expect(statuses[0].status).toBeNull(); expect(statuses[0].error).toBe('Status error'); }); }); }); describe('Convenience Functions', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should delegate to singleton registry', async () => { const mockConfig = require('../../server/utils/config'); mockConfig.getSABnzbdInstances.mockReturnValue([]); mockConfig.getQbittorrentInstances.mockReturnValue([]); mockConfig.getTransmissionInstances.mockReturnValue([]); await initializeClients(); expect(getAllClients()).toBeInstanceOf(Array); expect(getClient('test')).toBeNull(); expect(getClientsByType('sabnzbd')).toBeInstanceOf(Array); expect(await getAllDownloads()).toBeInstanceOf(Array); expect(await getDownloadsByClientType()).toBeInstanceOf(Object); expect(await testAllConnections()).toBeInstanceOf(Array); expect(await getAllClientStatuses()).toBeInstanceOf(Array); }); });