// Copyright (c) 2026 Gordon Bolton. MIT License. const QBittorrentClient = require('../../../server/clients/QBittorrentClient'); const axios = require('axios'); // Mock axios jest.mock('axios'); jest.mock('../../../server/utils/logger', () => ({ logToFile: jest.fn() })); describe('QBittorrentClient', () => { let client; let mockConfig; beforeEach(() => { mockConfig = { id: 'test-qb', name: 'Test qBittorrent', url: 'http://localhost:8080', username: 'admin', password: 'adminadmin' }; client = new QBittorrentClient(mockConfig); // Clear all mocks jest.clearAllMocks(); }); describe('Constructor', () => { it('should initialize with correct properties', () => { expect(client.getClientType()).toBe('qbittorrent'); expect(client.getInstanceId()).toBe('test-qb'); expect(client.name).toBe('Test qBittorrent'); expect(client.url).toBe('http://localhost:8080'); expect(client.authCookie).toBeNull(); expect(client.lastRid).toBe(0); expect(client.torrentMap).toBeInstanceOf(Map); expect(client.fallbackThisCycle).toBe(false); }); }); describe('Authentication', () => { it('should login successfully with valid credentials', async () => { const mockResponse = { headers: { 'set-cookie': ['SID=test-cookie'] } }; axios.post.mockResolvedValue(mockResponse); const result = await client.login(); expect(result).toBe(true); expect(client.authCookie).toBe('SID=test-cookie'); expect(axios.post).toHaveBeenCalledWith( 'http://localhost:8080/api/v2/auth/login', 'username=admin&password=adminadmin', expect.objectContaining({ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }) ); }); it('should handle login failure', async () => { const mockResponse = { headers: {} }; axios.post.mockResolvedValue(mockResponse); const result = await client.login(); expect(result).toBe(false); expect(client.authCookie).toBeNull(); }); it('should handle login error', async () => { axios.post.mockRejectedValue(new Error('Network error')); const result = await client.login(); expect(result).toBe(false); expect(client.authCookie).toBeNull(); }); }); describe('Connection Test', () => { it('should test connection successfully', async () => { // Mock login success client.login = jest.fn().mockResolvedValue(true); // Mock version request const mockResponse = { data: 'v4.3.5' }; client.makeRequest = jest.fn().mockResolvedValue(mockResponse); const result = await client.testConnection(); expect(result).toBe(true); expect(client.makeRequest).toHaveBeenCalledWith('/api/v2/app/version'); }); it('should handle connection test failure', async () => { client.login = jest.fn().mockRejectedValue(new Error('Auth failed')); const result = await client.testConnection(); expect(result).toBe(false); }); }); describe('Download Normalization', () => { it('should normalize torrent data correctly', () => { const torrent = { hash: 'abc123', name: 'Test Torrent', state: 'downloading', progress: 0.75, size: 1000000000, completed: 750000000, dlspeed: 1048576, eta: 3600, category: 'movies', tags: 'movie,hd', content_path: '/downloads/test', added_on: 1640995200 }; const normalized = client.normalizeDownload(torrent); expect(normalized).toEqual({ id: 'abc123', title: 'Test Torrent', type: 'torrent', client: 'qbittorrent', instanceId: 'test-qb', instanceName: 'Test qBittorrent', status: 'Downloading', progress: 75, size: 1000000000, downloaded: 750000000, speed: 1048576, eta: 3600, category: 'movies', tags: ['movie', 'hd'], savePath: '/downloads/test', addedOn: '2022-01-01T00:00:00.000Z', raw: torrent }); }); it('should handle unknown torrent states', () => { const torrent = { hash: 'abc123', name: 'Test Torrent', state: 'unknown_state', progress: 0.5, size: 1000000, completed: 500000, dlspeed: 0, eta: -1 }; const normalized = client.normalizeDownload(torrent); expect(normalized.status).toBe('unknown_state'); expect(normalized.eta).toBeNull(); }); it('should handle missing completed field', () => { const torrent = { hash: 'abc123', name: 'Test Torrent', state: 'downloading', progress: 0.5, size: 1000000, dlspeed: 0 }; const normalized = client.normalizeDownload(torrent); expect(normalized.downloaded).toBe(500000); }); }); describe('Fallback Flag Management', () => { it('should reset fallback flag', () => { client.fallbackThisCycle = true; client.resetFallbackFlag(); expect(client.fallbackThisCycle).toBe(false); }); }); describe('Error Handling', () => { it('should handle makeRequest authentication failure', async () => { client.authCookie = 'invalid-cookie'; // First call fails with 403 const authError = { response: { status: 403 } }; // Second login attempt succeeds client.login = jest.fn() .mockResolvedValueOnce(false) .mockResolvedValueOnce(true); // Retry request succeeds const successResponse = { data: 'success' }; axios.get = jest.fn() .mockRejectedValueOnce(authError) .mockResolvedValueOnce(successResponse); const result = await client.makeRequest('/test'); expect(result).toEqual(successResponse); expect(client.login).toHaveBeenCalledTimes(2); }); }); });