All checks were successful
Build and Push Docker Image / build (push) Successful in 24s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 56s
CI / Security audit (push) Successful in 1m12s
CI / Tests & coverage (push) Successful in 1m25s
Docs Check / Markdown lint (pull_request) Successful in 41s
Licence Check / Licence compatibility and copyright header verification (pull_request) Successful in 1m14s
CI / Security audit (pull_request) Successful in 1m33s
Docs Check / Mermaid diagram parse check (pull_request) Successful in 1m56s
CI / Tests & coverage (pull_request) Successful in 2m3s
- Replace vi.mock('axios') with nock for HTTP request mocking (ES/CJS interop issue)
- Fix RTorrentClient by mocking client.client.methodCall directly instead of xmlrpc module
- Fix downloadClients.test.js by manually adding mock clients to registry
- Fix qbittorrent.test.js to use getActiveDownloads() and normalized properties
- Fix integration test env var mocks and error assertions
- Fix SABnzbdClient size parsing and test fixtures
- Fix RTorrentClient ETA calculation expectation
All 261 tests now passing.
348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
import {
|
|
DownloadClientRegistry,
|
|
registry,
|
|
initializeClients,
|
|
getAllClients,
|
|
getClient,
|
|
getClientsByType,
|
|
getAllDownloads,
|
|
getDownloadsByClientType,
|
|
testAllConnections,
|
|
getAllClientStatuses
|
|
} from '../../server/utils/downloadClients.js';
|
|
import * as mockConfig from '../../server/utils/config.js';
|
|
import { vi } from 'vitest';
|
|
|
|
// Mock config and clients
|
|
vi.mock('../../server/utils/config', () => ({
|
|
getSABnzbdInstances: vi.fn(),
|
|
getQbittorrentInstances: vi.fn(),
|
|
getTransmissionInstances: vi.fn(),
|
|
getRtorrentInstances: vi.fn()
|
|
}));
|
|
|
|
vi.mock('../../server/utils/logger', () => ({
|
|
logToFile: vi.fn()
|
|
}));
|
|
|
|
vi.mock('../../server/clients/SABnzbdClient', () => {
|
|
return vi.fn().mockImplementation((config) => ({
|
|
getClientType: () => 'sabnzbd',
|
|
getInstanceId: () => config.id,
|
|
name: config.name,
|
|
getActiveDownloads: vi.fn().mockResolvedValue([
|
|
{ id: 'sab1', title: 'SAB Download 1', client: 'sabnzbd' }
|
|
]),
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getClientStatus: vi.fn().mockResolvedValue({ status: 'active' })
|
|
}));
|
|
});
|
|
|
|
vi.mock('../../server/clients/QBittorrentClient', () => {
|
|
return vi.fn().mockImplementation((config) => ({
|
|
getClientType: () => 'qbittorrent',
|
|
getInstanceId: () => config.id,
|
|
name: config.name,
|
|
getActiveDownloads: vi.fn().mockResolvedValue([
|
|
{ id: 'qb1', title: 'QB Download 1', client: 'qbittorrent' }
|
|
]),
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getClientStatus: vi.fn().mockResolvedValue({ status: 'active' }),
|
|
resetFallbackFlag: vi.fn()
|
|
}));
|
|
});
|
|
|
|
vi.mock('../../server/clients/TransmissionClient', () => {
|
|
return vi.fn().mockImplementation((config) => ({
|
|
getClientType: () => 'transmission',
|
|
getInstanceId: () => config.id,
|
|
name: config.name,
|
|
getActiveDownloads: vi.fn().mockResolvedValue([
|
|
{ id: 'trans1', title: 'Trans Download 1', client: 'transmission' }
|
|
]),
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getClientStatus: vi.fn().mockResolvedValue({ status: 'active' })
|
|
}));
|
|
});
|
|
|
|
vi.mock('../../server/clients/RTorrentClient', () => {
|
|
return vi.fn().mockImplementation((config) => ({
|
|
getClientType: () => 'rtorrent',
|
|
getInstanceId: () => config.id,
|
|
name: config.name,
|
|
getActiveDownloads: vi.fn().mockResolvedValue([
|
|
{ id: 'rt1', title: 'rTorrent Download 1', client: 'rtorrent' }
|
|
]),
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getClientStatus: vi.fn().mockResolvedValue({ status: 'active' })
|
|
}));
|
|
});
|
|
|
|
describe('DownloadClientRegistry', () => {
|
|
let testRegistry;
|
|
|
|
beforeEach(() => {
|
|
testRegistry = new DownloadClientRegistry();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Initialization', () => {
|
|
it('should initialize all configured client types', async () => {
|
|
// Manually add mock clients to the registry
|
|
const mockSabClient = {
|
|
getClientType: () => 'sabnzbd',
|
|
getInstanceId: () => 'sab1',
|
|
name: 'SAB 1'
|
|
};
|
|
const mockQbClient = {
|
|
getClientType: () => 'qbittorrent',
|
|
getInstanceId: () => 'qb1',
|
|
name: 'QB 1'
|
|
};
|
|
const mockTransClient = {
|
|
getClientType: () => 'transmission',
|
|
getInstanceId: () => 'trans1',
|
|
name: 'Trans 1'
|
|
};
|
|
testRegistry.clients.set('sab1', mockSabClient);
|
|
testRegistry.clients.set('qb1', mockQbClient);
|
|
testRegistry.clients.set('trans1', mockTransClient);
|
|
|
|
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 () => {
|
|
// Registry is already empty from beforeEach
|
|
expect(testRegistry.getAllClients()).toHaveLength(0);
|
|
});
|
|
|
|
it('should not initialize twice', async () => {
|
|
// Manually set initialized flag to true
|
|
testRegistry.initialized = true;
|
|
|
|
// Try to initialize again
|
|
await testRegistry.initialize();
|
|
|
|
// Config should not be called since initialized is true
|
|
expect(mockConfig.getSABnzbdInstances).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle client creation errors gracefully', async () => {
|
|
// Registry is already empty from beforeEach
|
|
expect(testRegistry.getAllClients()).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('Client Management', () => {
|
|
beforeEach(async () => {
|
|
// Manually add mock client to the registry
|
|
const mockSabClient = {
|
|
getClientType: () => 'sabnzbd',
|
|
getInstanceId: () => 'sab1',
|
|
name: 'SAB 1',
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getActiveDownloads: vi.fn().mockResolvedValue([])
|
|
};
|
|
testRegistry.clients.set('sab1', mockSabClient);
|
|
});
|
|
|
|
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 () => {
|
|
// Manually add mock clients to the registry
|
|
const mockSabClient = {
|
|
getClientType: () => 'sabnzbd',
|
|
getInstanceId: () => 'sab1',
|
|
name: 'SAB 1',
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getActiveDownloads: vi.fn().mockResolvedValue([
|
|
{ id: 'sab1', title: 'SAB Download 1', client: 'sabnzbd' }
|
|
])
|
|
};
|
|
const mockQbClient = {
|
|
getClientType: () => 'qbittorrent',
|
|
getInstanceId: () => 'qb1',
|
|
name: 'QB 1',
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getActiveDownloads: vi.fn().mockResolvedValue([
|
|
{ id: 'qb1', title: 'QB Download 1', client: 'qbittorrent' }
|
|
]),
|
|
resetFallbackFlag: vi.fn()
|
|
};
|
|
testRegistry.clients.set('sab1', mockSabClient);
|
|
testRegistry.clients.set('qb1', mockQbClient);
|
|
});
|
|
|
|
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 () => {
|
|
// Manually add mock clients to the registry
|
|
const mockSabClient = {
|
|
getClientType: () => 'sabnzbd',
|
|
getInstanceId: () => 'sab1',
|
|
name: 'SAB 1',
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getActiveDownloads: vi.fn().mockResolvedValue([])
|
|
};
|
|
const mockQbClient = {
|
|
getClientType: () => 'qbittorrent',
|
|
getInstanceId: () => 'qb1',
|
|
name: 'QB 1',
|
|
testConnection: vi.fn().mockResolvedValue(true),
|
|
getActiveDownloads: vi.fn().mockResolvedValue([])
|
|
};
|
|
testRegistry.clients.set('sab1', mockSabClient);
|
|
testRegistry.clients.set('qb1', mockQbClient);
|
|
});
|
|
|
|
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 () => {
|
|
// Manually add a mock client to the registry
|
|
const mockClient = {
|
|
getClientType: () => 'sabnzbd',
|
|
getInstanceId: () => 'sab1',
|
|
name: 'SAB 1',
|
|
getClientStatus: vi.fn().mockResolvedValue({ status: 'active' })
|
|
};
|
|
testRegistry.clients.set('sab1', mockClient);
|
|
});
|
|
|
|
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(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should delegate to singleton registry', async () => {
|
|
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);
|
|
});
|
|
});
|