// Copyright (c) 2026 Gordon Bolton. MIT License. import { initializeClients, getAllDownloads, getDownloadsByClientType, testAllConnections, registry } from '../../server/utils/downloadClients.js'; import axios from 'axios'; import { vi } from 'vitest'; // Mock environment variables for testing process.env.SABNZBD_INSTANCES = JSON.stringify([ { id: 'test-sab', name: 'Test SABnzbd', url: 'http://localhost:8080', apiKey: 'test-api-key' } ]); process.env.QBITTORRENT_INSTANCES = JSON.stringify([ { id: 'test-qb', name: 'Test qBittorrent', url: 'http://localhost:8080', username: 'admin', password: 'adminadmin' } ]); process.env.TRANSMISSION_INSTANCES = JSON.stringify([ { id: 'test-trans', name: 'Test Transmission', url: 'http://localhost:9091', username: 'transmission', password: 'transmission' } ]); process.env.RTORRENT_INSTANCES = JSON.stringify([ { id: 'test-rtorrent', name: 'Test rTorrent', url: 'http://localhost:8080/RPC2', username: 'rtorrent', password: 'rtorrent' } ]); // Mock axios to prevent actual network calls vi.mock('axios', () => { const mockAxios = vi.fn(); mockAxios.post = vi.fn(); mockAxios.get = vi.fn(); return { default: mockAxios, post: vi.fn(), get: vi.fn() }; }); vi.mock('../../server/utils/logger', () => ({ logToFile: vi.fn() })); describe('Download Clients Integration Tests', () => { beforeEach(() => { registry.initialized = false; registry.clients.clear(); vi.clearAllMocks(); }); describe('Client Initialization', () => { it('should initialize all configured client types', async () => { await initializeClients(); // The registry should have clients for all three types const downloadsByType = await getDownloadsByClientType(); // Should have keys for each client type (even if empty due to mocked failures) expect(typeof downloadsByType).toBe('object'); }); it('should handle missing environment variables gracefully', async () => { // Temporarily clear environment variables const originalSab = process.env.SABNZBD_INSTANCES; const originalQb = process.env.QBITTORRENT_INSTANCES; const originalTrans = process.env.TRANSMISSION_INSTANCES; const originalRt = process.env.RTORRENT_INSTANCES; delete process.env.SABNZBD_INSTANCES; delete process.env.QBITTORRENT_INSTANCES; delete process.env.TRANSMISSION_INSTANCES; delete process.env.RTORRENT_INSTANCES; await initializeClients(); const downloadsByType = await getDownloadsByClientType(); expect(Object.keys(downloadsByType)).toHaveLength(0); // Restore environment variables process.env.SABNZBD_INSTANCES = originalSab; process.env.QBITTORRENT_INSTANCES = originalQb; process.env.TRANSMISSION_INSTANCES = originalTrans; process.env.RTORRENT_INSTANCES = originalRt; }); }); describe('Download Aggregation', () => { it('should aggregate downloads from multiple client types', async () => { await initializeClients(); const downloadsByType = await getDownloadsByClientType(); const allDownloads = await getAllDownloads(); // Should return downloads grouped by type expect(typeof downloadsByType).toBe('object'); // Should return flattened array of all downloads expect(Array.isArray(allDownloads)).toBe(true); // All downloads should have required normalized fields allDownloads.forEach(download => { expect(download).toHaveProperty('id'); expect(download).toHaveProperty('title'); expect(download).toHaveProperty('type'); expect(download).toHaveProperty('client'); expect(download).toHaveProperty('instanceId'); expect(download).toHaveProperty('instanceName'); expect(download).toHaveProperty('status'); expect(download).toHaveProperty('progress'); expect(download).toHaveProperty('size'); expect(download).toHaveProperty('downloaded'); expect(download).toHaveProperty('speed'); expect(download).toHaveProperty('raw'); }); }); it('should maintain type consistency across clients', async () => { await initializeClients(); const downloadsByType = await getDownloadsByClientType(); // Check that each client type returns consistent data structure Object.entries(downloadsByType).forEach(([clientType, downloads]) => { if (downloads.length > 0) { downloads.forEach(download => { expect(download.client).toBe(clientType); expect(download.type).toMatch(/^(usenet|torrent)$/); expect(typeof download.progress).toBe('number'); expect(download.progress).toBeGreaterThanOrEqual(0); expect(download.progress).toBeLessThanOrEqual(100); expect(typeof download.size).toBe('number'); expect(typeof download.downloaded).toBe('number'); expect(typeof download.speed).toBe('number'); }); } }); }); }); describe('Connection Testing', () => { it('should test connections for all configured clients', async () => { await initializeClients(); const results = await testAllConnections(); expect(Array.isArray(results)).toBe(true); results.forEach(result => { expect(result).toHaveProperty('instanceId'); expect(result).toHaveProperty('instanceName'); expect(result).toHaveProperty('clientType'); expect(result).toHaveProperty('success'); expect(typeof result.success).toBe('boolean'); }); }); it('should handle connection failures gracefully', async () => { // This test verifies that connection failures don't crash the system await initializeClients(); const results = await testAllConnections(); // Should still return results even if connections fail expect(results.length).toBeGreaterThan(0); }); }); describe('Error Handling and Resilience', () => { it('should handle individual client failures without affecting others', async () => { await initializeClients(); // Even if some clients fail, others should still work const downloadsByType = await getDownloadsByClientType(); const allDownloads = await getAllDownloads(); expect(typeof downloadsByType).toBe('object'); expect(Array.isArray(allDownloads)).toBe(true); }); it('should handle malformed configuration gracefully', async () => { // Test with malformed JSON const originalSab = process.env.SABNZBD_INSTANCES; process.env.SABNZBD_INSTANCES = 'invalid-json{'; // Should not throw an error await expect(initializeClients()).resolves.not.toThrow(); // Restore process.env.SABNZBD_INSTANCES = originalSab; }); it('should handle network timeouts and errors', async () => { await initializeClients(); // Mock network failures by setting up axios to reject axios.get.mockRejectedValue(new Error('Network timeout')); axios.post.mockRejectedValue(new Error('Network timeout')); // Should handle errors gracefully and return empty results const downloads = await getAllDownloads(); expect(Array.isArray(downloads)).toBe(true); }); }); describe('Backward Compatibility', () => { it('should maintain compatibility with existing cache structure', async () => { await initializeClients(); const downloadsByType = await getDownloadsByClientType(); // SABnzbd downloads should have raw data for legacy compatibility if (downloadsByType.sabnzbd && downloadsByType.sabnzbd.length > 0) { downloadsByType.sabnzbd.forEach(download => { expect(download.raw).toBeTruthy(); expect(download.raw.source).toMatch(/^(queue|history)$/); }); } // qBittorrent downloads should have raw data for legacy compatibility if (downloadsByType.qbittorrent && downloadsByType.qbittorrent.length > 0) { downloadsByType.qbittorrent.forEach(download => { expect(download.raw).toBeTruthy(); expect(download.raw.hash).toBeTruthy(); // qBittorrent specific field }); } }); }); describe('Performance and Scalability', () => { it('should handle multiple instances of the same client type', async () => { // Configure multiple instances process.env.QBITTORRENT_INSTANCES = JSON.stringify([ { id: 'test-qb-1', name: 'Test qBittorrent 1', url: 'http://localhost:8080', username: 'admin', password: 'adminadmin' }, { id: 'test-qb-2', name: 'Test qBittorrent 2', url: 'http://localhost:8081', username: 'admin', password: 'adminadmin' } ]); await initializeClients(); const downloadsByType = await getDownloadsByClientType(); // Should aggregate downloads from both instances expect(Array.isArray(downloadsByType.qbittorrent)).toBe(true); // Each download should have correct instance information downloadsByType.qbittorrent.forEach(download => { expect(download.instanceId).toMatch(/^(test-qb-1|test-qb-2)$/); expect(download.instanceName).toMatch(/^(Test qBittorrent 1|Test qBittorrent 2)$/); }); }); it('should execute client requests in parallel', async () => { const startTime = Date.now(); await initializeClients(); await getAllDownloads(); const endTime = Date.now(); const duration = endTime - startTime; // This is a rough check - in a real scenario with actual network calls, // parallel execution should be significantly faster than sequential expect(duration).toBeGreaterThan(0); }); }); });