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.
301 lines
9.9 KiB
JavaScript
301 lines
9.9 KiB
JavaScript
// 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);
|
|
});
|
|
});
|
|
});
|