// Copyright (c) 2026 Gordon Bolton. MIT License. import SABnzbdClient from '../../../server/clients/SABnzbdClient.js'; import axios from 'axios'; import { vi } from 'vitest'; // Mock axios vi.mock('axios'); vi.mock('../../../server/utils/logger', () => ({ logToFile: vi.fn() })); describe('SABnzbdClient', () => { let client; let mockConfig; beforeEach(() => { mockConfig = { id: 'test-sab', name: 'Test SABnzbd', url: 'http://localhost:8080', apiKey: 'test-api-key' }; client = new SABnzbdClient(mockConfig); // Clear all mocks vi.clearAllMocks(); }); describe('Constructor', () => { it('should initialize with correct properties', () => { expect(client.getClientType()).toBe('sabnzbd'); expect(client.getInstanceId()).toBe('test-sab'); expect(client.name).toBe('Test SABnzbd'); expect(client.url).toBe('http://localhost:8080'); expect(client.apiKey).toBe('test-api-key'); }); }); describe('Connection Test', () => { it('should test connection successfully', async () => { const mockResponse = { data: { version: '3.6.1' } }; client.makeRequest = vi.fn().mockResolvedValue(mockResponse); const result = await client.testConnection(); expect(result).toBe(true); expect(client.makeRequest).toHaveBeenCalledWith({ mode: 'version' }); }); it('should handle connection test failure', async () => { client.makeRequest = vi.fn().mockRejectedValue(new Error('Connection failed')); const result = await client.testConnection(); expect(result).toBe(false); }); }); describe('API Requests', () => { it('should make API request with correct parameters', async () => { const mockResponse = { data: { result: 'success' } }; axios.get.mockResolvedValue(mockResponse); const result = await client.makeRequest({ mode: 'queue', limit: 10 }); expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/api', { params: { output: 'json', apikey: 'test-api-key', mode: 'queue', limit: 10 } }); expect(result).toEqual(mockResponse); }); it('should handle API request errors', async () => { const error = new Error('API Error'); axios.get.mockRejectedValue(error); await expect(client.makeRequest({ mode: 'queue' })).rejects.toThrow('API Error'); }); }); describe('Download Normalization', () => { it('should normalize queue download correctly', () => { const slot = { nzo_id: 'test123', filename: 'Test Movie.mkv', status: 'Downloading', progress: 75.5, mb: 1000, mbleft: 245, kbpersec: 1024, timeleft: '0:15:30', cat: 'movies', labels: 'movie,hd', added: 1640995200 }; const normalized = client.normalizeDownload(slot, 'queue'); expect(normalized).toEqual({ id: 'test123', title: 'Test Movie.mkv', type: 'usenet', client: 'sabnzbd', instanceId: 'test-sab', instanceName: 'Test SABnzbd', status: 'Downloading', progress: 76, size: 1000 * 1024 * 1024, downloaded: 755 * 1024 * 1024, speed: 1024 * 1024, eta: 930, // 15 minutes 30 seconds category: 'movies', tags: ['movie', 'hd'], savePath: undefined, addedOn: '2022-01-01T00:00:00.000Z', raw: { ...slot, source: 'queue' } }); }); it('should normalize history download correctly', () => { const slot = { nzo_id: 'test456', filename: 'Test Series S01E01.mkv', status: 'Completed', mb: 500, cat: 'tv', added: 1640995200 }; const normalized = client.normalizeDownload(slot, 'history'); expect(normalized.status).toBe('Completed'); expect(normalized.progress).toBe(100); expect(normalized.downloaded).toBe(500 * 1024 * 1024); expect(normalized.speed).toBe(0); expect(normalized.eta).toBeNull(); expect(normalized.raw.source).toBe('history'); }); it('should parse time strings correctly', () => { const testCases = [ { input: '0:05:30', expected: 330 }, // 5m 30s { input: '15:30', expected: 930 }, // 15m 30s { input: '330', expected: 330 }, // 330 seconds { input: 'unknown', expected: null }, // unknown { input: '0:00', expected: null } // zero ]; testCases.forEach(({ input, expected }) => { const slot = { nzo_id: 'test', filename: 'Test', status: 'Downloading', progress: 50, mb: 1000, mbleft: 500, timeleft: input }; const normalized = client.normalizeDownload(slot, 'queue'); expect(normalized.eta).toBe(expected); }); }); it('should extract Sonarr/Radarr info from filename', () => { const testCases = [ { filename: 'Show Name - S01E02 - Episode Title', expectedType: 'series' }, { filename: 'Movie Title (2023) 1080p', expectedType: 'movie' }, { filename: 'Random File Name.mkv', expectedType: undefined } ]; testCases.forEach(({ filename, expectedType }) => { const slot = { nzo_id: 'test', filename: filename, status: 'Downloading', progress: 50, mb: 1000, mbleft: 500 }; const normalized = client.normalizeDownload(slot, 'queue'); expect(normalized.arrType).toBe(expectedType); }); }); it('should handle size parsing from strings', () => { const slot = { nzo_id: 'test', filename: 'Test', status: 'Downloading', progress: 50, size: '1.5 GB', sizeleft: '750 MB' }; const normalized = client.normalizeDownload(slot, 'queue'); expect(normalized.size).toBe(1.5 * 1024 * 1024 * 1024); expect(normalized.downloaded).toBe(0.75 * 1024 * 1024 * 1024); expect(normalized.progress).toBe(50); }); }); describe('Unit Multipliers', () => { it('should return correct multipliers for different units', () => { expect(client.getUnitMultiplier('b')).toBe(1); expect(client.getUnitMultiplier('KB')).toBe(1024); expect(client.getUnitMultiplier('mb')).toBe(1024 * 1024); expect(client.getUnitMultiplier('GB')).toBe(1024 * 1024 * 1024); expect(client.getUnitMultiplier('tb')).toBe(1024 * 1024 * 1024 * 1024); expect(client.getUnitMultiplier('unknown')).toBe(1); }); }); describe('Active Downloads', () => { it('should fetch and normalize downloads from queue and history', async () => { const mockQueueResponse = { data: { queue: { slots: [ { nzo_id: 'queue1', filename: 'Queue Item', status: 'Downloading' } ] } } }; const mockHistoryResponse = { data: { history: { slots: [ { nzo_id: 'hist1', filename: 'History Item', status: 'Completed' } ] } } }; client.makeRequest = vi.fn() .mockResolvedValueOnce(mockQueueResponse) .mockResolvedValueOnce(mockHistoryResponse); const downloads = await client.getActiveDownloads(); expect(client.makeRequest).toHaveBeenCalledWith({ mode: 'queue' }); expect(client.makeRequest).toHaveBeenCalledWith({ mode: 'history', limit: 10 }); expect(downloads).toHaveLength(2); expect(downloads[0].id).toBe('queue1'); expect(downloads[1].id).toBe('hist1'); }); it('should handle API errors gracefully', async () => { client.makeRequest = vi.fn().mockRejectedValue(new Error('API Error')); const downloads = await client.getActiveDownloads(); expect(downloads).toEqual([]); }); }); describe('Client Status', () => { it('should get client status', async () => { const mockResponse = { data: { queue: { status: 'Active', speed: 1048576, kbpersec: 1024, sizeleft: 500000000, mbleft: 500 } } }; client.makeRequest = vi.fn().mockResolvedValue(mockResponse); const status = await client.getClientStatus(); expect(status).toEqual({ status: 'Active', speed: 1048576, kbpersec: 1024, sizeleft: 500000000, mbleft: 500 }); }); it('should handle status request errors', async () => { client.makeRequest = vi.fn().mockRejectedValue(new Error('Status error')); const status = await client.getClientStatus(); expect(status).toBeNull(); }); }); });