Files
sofarr/tests/unit/clients/RTorrentClient.test.js
Gronod 9343486705
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
Fix all Vitest test failures after migration
- 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.
2026-05-19 13:53:09 +01:00

424 lines
11 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
import RTorrentClient from '../../../server/clients/RTorrentClient.js';
import { vi } from 'vitest';
vi.mock('../../../server/utils/logger', () => ({
logToFile: vi.fn()
}));
describe('RTorrentClient', () => {
let client;
let mockConfig;
let mockMethodCall;
beforeEach(() => {
mockMethodCall = vi.fn();
mockConfig = {
id: 'test-rtorrent',
name: 'Test rTorrent',
url: 'http://localhost:8080',
username: 'rtorrent',
password: 'rtorrent'
};
client = new RTorrentClient(mockConfig);
// Mock the xmlrpc client's methodCall directly
client.client.methodCall = mockMethodCall;
});
describe('Constructor', () => {
it('should initialize with correct properties', () => {
expect(client.getClientType()).toBe('rtorrent');
expect(client.getInstanceId()).toBe('test-rtorrent');
expect(client.name).toBe('Test rTorrent');
expect(client.url).toBe('http://localhost:8080');
});
it('should create xmlrpc client with correct URL', async () => {
expect(client.url).toBe('http://localhost:8080');
expect(client.client).toBeDefined();
});
it('should create xmlrpc client without auth when no credentials', () => {
const noAuthConfig = {
id: 'test-rtorrent-noauth',
name: 'Test rTorrent No Auth',
url: 'http://localhost:8080/RPC2'
};
const clientNoAuth = new RTorrentClient(noAuthConfig);
expect(clientNoAuth.client).toBeDefined();
});
it('should use whatbox.ca-style /xmlrpc path exactly as configured', () => {
const whatboxConfig = {
id: 'test-whatbox',
name: 'Whatbox',
url: 'https://user.whatbox.ca/xmlrpc',
username: 'user',
password: 'pass'
};
const clientWhatbox = new RTorrentClient(whatboxConfig);
expect(clientWhatbox.client).toBeDefined();
});
it('should use custom RPC path exactly as configured', () => {
const customConfig = {
id: 'test-custom',
name: 'Custom',
url: 'https://example.com/custom/rpc/path'
};
const clientCustom = new RTorrentClient(customConfig);
expect(clientCustom.client).toBeDefined();
});
});
describe('Connection Test', () => {
it('should test connection successfully', async () => {
mockMethodCall.mockImplementation((method, params, callback) => {
callback(null, '0.9.8');
});
const result = await client.testConnection();
expect(result).toBe(true);
});
it('should handle connection test failure', async () => {
mockMethodCall.mockImplementation((method, params, callback) => {
callback(new Error('Connection refused'));
});
const result = await client.testConnection();
expect(result).toBe(false);
});
});
describe('getActiveDownloads', () => {
it('should fetch and normalize torrents', async () => {
const mockTorrents = [
[
'abc123def456',
'Test Torrent 1',
1000000000,
750000000,
1048576,
0,
1,
1,
0,
'/downloads/test',
'movies'
],
[
'def789abc012',
'Test Torrent 2',
2000000000,
2000000000,
0,
512000,
1,
1,
0,
'/downloads/complete',
'tv'
]
];
mockMethodCall.mockImplementation((method, params, callback) => {
callback(null, mockTorrents);
});
const downloads = await client.getActiveDownloads();
expect(downloads).toHaveLength(2);
expect(downloads[0].id).toBe('abc123def456');
expect(downloads[0].title).toBe('Test Torrent 1');
expect(downloads[0].status).toBe('Downloading');
expect(downloads[0].progress).toBe(75);
expect(downloads[0].category).toBe('movies');
expect(downloads[1].status).toBe('Seeding');
expect(downloads[1].category).toBe('tv');
});
it('should handle empty torrent list', async () => {
mockMethodCall.mockImplementation((method, params, callback) => {
callback(null, []);
});
const downloads = await client.getActiveDownloads();
expect(downloads).toEqual([]);
});
it('should handle XML-RPC errors gracefully', async () => {
mockMethodCall.mockImplementation((method, params, callback) => {
callback(new Error('XML-RPC fault'));
});
const downloads = await client.getActiveDownloads();
expect(downloads).toEqual([]);
});
});
describe('normalizeDownload', () => {
it('should normalize a downloading torrent', () => {
const torrent = [
'hash123',
'Downloading Torrent',
1000000000,
500000000,
1048576,
0,
1,
1,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized).toEqual({
id: 'hash123',
title: 'Downloading Torrent',
type: 'torrent',
client: 'rtorrent',
instanceId: 'test-rtorrent',
instanceName: 'Test rTorrent',
status: 'Downloading',
progress: 50,
size: 1000000000,
downloaded: 500000000,
speed: 1048576,
eta: 477,
category: undefined,
tags: [],
savePath: '/downloads',
addedOn: undefined,
arrQueueId: undefined,
arrType: undefined,
raw: torrent
});
});
it('should normalize a seeding torrent', () => {
const torrent = [
'hash456',
'Seeding Torrent',
500000000,
500000000,
0,
204800,
1,
1,
0,
'/downloads/complete',
'movies'
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.status).toBe('Seeding');
expect(normalized.progress).toBe(100);
expect(normalized.speed).toBe(204800);
expect(normalized.eta).toBeNull();
expect(normalized.category).toBe('movies');
expect(normalized.tags).toEqual(['movies']);
});
it('should normalize a paused torrent', () => {
const torrent = [
'hash789',
'Paused Torrent',
1000000000,
250000000,
0,
0,
1,
0,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.status).toBe('Paused');
expect(normalized.speed).toBe(0);
expect(normalized.eta).toBeNull();
});
it('should normalize a stopped torrent', () => {
const torrent = [
'hashabc',
'Stopped Torrent',
1000000000,
0,
0,
0,
0,
0,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.status).toBe('Stopped');
});
it('should normalize a checking torrent', () => {
const torrent = [
'hashdef',
'Checking Torrent',
1000000000,
500000000,
0,
0,
1,
0,
1,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.status).toBe('Checking');
});
it('should handle zero-size torrent', () => {
const torrent = [
'hash000',
'Zero Size',
0,
0,
0,
0,
1,
1,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.progress).toBe(0);
expect(normalized.size).toBe(0);
expect(normalized.downloaded).toBe(0);
});
});
describe('Status Mapping', () => {
const testCases = [
{ state: 0, isActive: 0, isHashChecking: 0, completed: 0, size: 100, expected: 'Stopped' },
{ state: 1, isActive: 1, isHashChecking: 0, completed: 50, size: 100, expected: 'Downloading' },
{ state: 1, isActive: 1, isHashChecking: 0, completed: 100, size: 100, expected: 'Seeding' },
{ state: 1, isActive: 0, isHashChecking: 0, completed: 50, size: 100, expected: 'Paused' },
{ state: 1, isActive: 0, isHashChecking: 0, completed: 100, size: 100, expected: 'Paused' },
{ state: 1, isActive: 0, isHashChecking: 1, completed: 50, size: 100, expected: 'Checking' },
{ state: 1, isActive: 1, isHashChecking: 1, completed: 50, size: 100, expected: 'Checking' }
];
testCases.forEach(({ state, isActive, isHashChecking, completed, size, expected }) => {
it(`should map state=${state} isActive=${isActive} isHashChecking=${isHashChecking} to ${expected}`, () => {
const status = client._mapStatus(state, isActive, isHashChecking, completed, size);
expect(status).toBe(expected);
});
});
});
describe('ARR Info Extraction', () => {
it('should extract series info from filename', () => {
const torrent = [
'hash123',
'Show Name - S01E02 - Episode Title',
1000000000,
500000000,
1048576,
0,
1,
1,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.arrType).toBe('series');
});
it('should extract movie info from filename', () => {
const torrent = [
'hash456',
'Movie Title (2023) 1080p',
2000000000,
1000000000,
1048576,
0,
1,
1,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.arrType).toBe('movie');
});
it('should not extract ARR info from generic filename', () => {
const torrent = [
'hash789',
'Generic File Name.mkv',
1000000000,
500000000,
1048576,
0,
1,
1,
0,
'/downloads',
''
];
const normalized = client.normalizeDownload(torrent);
expect(normalized.arrType).toBeUndefined();
});
});
describe('Client Status', () => {
it('should get client status', async () => {
mockMethodCall.mockImplementation((method, params, callback) => {
if (method === 'throttle.global_down.rate') {
callback(null, 1048576);
} else if (method === 'throttle.global_up.rate') {
callback(null, 512000);
}
});
const status = await client.getClientStatus();
expect(status).toEqual({
globalDownRate: 1048576,
globalUpRate: 512000
});
});
it('should handle status request errors', async () => {
mockMethodCall.mockImplementation((method, params, callback) => {
callback(new Error('Status error'));
});
const status = await client.getClientStatus();
expect(status).toBeNull();
});
});
});