Files
sofarr/tests/unit/clients/TransmissionClient.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

437 lines
12 KiB
JavaScript

// Copyright (c) 2026 Gordon Bolton. MIT License.
import TransmissionClient from '../../../server/clients/TransmissionClient.js';
import nock from 'nock';
import { vi } from 'vitest';
vi.mock('../../../server/utils/logger', () => ({
logToFile: vi.fn()
}));
describe('TransmissionClient', () => {
let client;
let mockConfig;
beforeEach(() => {
mockConfig = {
id: 'test-transmission',
name: 'Test Transmission',
url: 'http://localhost:9091',
username: 'transmission',
password: 'transmission'
};
client = new TransmissionClient(mockConfig);
// Clear all mocks
vi.clearAllMocks();
});
describe('Constructor', () => {
it('should initialize with correct properties', () => {
expect(client.getClientType()).toBe('transmission');
expect(client.getInstanceId()).toBe('test-transmission');
expect(client.name).toBe('Test Transmission');
expect(client.url).toBe('http://localhost:9091');
expect(client.sessionId).toBeNull();
expect(client.rpcUrl).toBe('http://localhost:9091/transmission/rpc');
});
});
describe('Connection Test', () => {
it('should test connection successfully', async () => {
const mockResponse = {
data: { result: 'success', arguments: {} }
};
client.makeRequest = vi.fn().mockResolvedValue(mockResponse);
const result = await client.testConnection();
expect(result).toBe(true);
expect(client.makeRequest).toHaveBeenCalledWith('session-get');
});
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('RPC Requests', () => {
it('should make RPC request with session ID', async () => {
client.sessionId = 'test-session-id';
nock('http://localhost:9091')
.post('/transmission/rpc', {
method: 'torrent-get',
arguments: { fields: ['id', 'name'] }
})
.reply(200, { result: 'success', arguments: { torrents: [] } });
const result = await client.makeRequest('torrent-get', { fields: ['id', 'name'] });
expect(result.data).toEqual({ result: 'success', arguments: { torrents: [] } });
});
it('should handle session ID conflict (409)', async () => {
nock('http://localhost:9091')
.post('/transmission/rpc', { method: 'session-get', arguments: {} })
.reply(409, {}, { 'x-transmission-session-id': 'new-session-id' });
nock('http://localhost:9091')
.post('/transmission/rpc', { method: 'session-get', arguments: {} })
.reply(200, { result: 'success', arguments: {} });
const result = await client.makeRequest('session-get');
expect(client.sessionId).toBe('new-session-id');
expect(result.data).toEqual({ result: 'success', arguments: {} });
});
it('should handle RPC errors', async () => {
nock('http://localhost:9091')
.post('/transmission/rpc', { method: 'invalid-method', arguments: {} })
.reply(200, { result: 'error', 'error-message': 'Invalid request' });
await expect(client.makeRequest('invalid-method')).rejects.toThrow('Transmission RPC error: error');
});
});
describe('Download Normalization', () => {
it('should normalize torrent data correctly', () => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Test Torrent',
status: 4, // downloading
totalSize: 1000000000,
sizeWhenDone: 1000000000,
leftUntilDone: 250000000,
rateDownload: 1048576,
rateUpload: 0,
eta: 3600,
downloadedEver: 750000000,
uploadedEver: 0,
percentDone: 0.75,
addedDate: 1640995200,
doneDate: 0,
labels: ['movies', 'hd'],
downloadDir: '/downloads/test'
};
const normalized = client.normalizeDownload(torrent);
expect(normalized).toEqual({
id: 'abc123',
title: 'Test Torrent',
type: 'torrent',
client: 'transmission',
instanceId: 'test-transmission',
instanceName: 'Test Transmission',
status: 'Downloading',
progress: 75,
size: 1000000000,
downloaded: 750000000,
speed: 1048576,
eta: 3600,
category: 'movies',
tags: ['movies', 'hd'],
savePath: '/downloads/test',
addedOn: '2022-01-01T00:00:00.000Z',
raw: torrent
});
});
it('should handle different torrent statuses', () => {
const statusMap = {
0: 'Stopped',
1: 'Queued',
2: 'Checking',
3: 'Queued',
4: 'Downloading',
5: 'Queued',
6: 'Seeding',
7: 'Unknown'
};
Object.entries(statusMap).forEach(([status, expectedStatus]) => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Test',
status: parseInt(status),
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: -1,
percentDone: 0.5,
labels: []
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.status).toBe(expectedStatus);
});
});
it('should handle unknown ETA values', () => {
const testCases = [
{ eta: -1, expected: null },
{ eta: -2, expected: null },
{ eta: 3600, expected: 3600 }
];
testCases.forEach(({ eta, expected }) => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Test',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: eta,
percentDone: 0.5,
labels: []
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.eta).toBe(expected);
});
});
it('should extract category from first label', () => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Test',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: -1,
percentDone: 0.5,
labels: ['movies', 'hd', '4k']
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.category).toBe('movies');
expect(normalized.tags).toEqual(['movies', 'hd', '4k']);
});
it('should handle empty labels', () => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Test',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: -1,
percentDone: 0.5,
labels: []
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.category).toBeUndefined();
expect(normalized.tags).toEqual([]);
});
});
describe('Active Downloads', () => {
it('should fetch and normalize torrents', async () => {
const mockResponse = {
data: {
result: 'success',
arguments: {
torrents: [
{
id: 1,
hashString: 'abc123',
name: 'Test Torrent 1',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 1048576,
eta: 3600,
percentDone: 0.5,
labels: ['test']
},
{
id: 2,
hashString: 'def456',
name: 'Test Torrent 2',
status: 6,
totalSize: 2000000,
sizeWhenDone: 2000000,
leftUntilDone: 0,
rateDownload: 0,
eta: -1,
percentDone: 1.0,
labels: []
}
]
}
}
};
client.makeRequest = vi.fn().mockResolvedValue(mockResponse);
const downloads = await client.getActiveDownloads();
expect(client.makeRequest).toHaveBeenCalledWith('torrent-get', {
fields: [
'id', 'name', 'hashString', 'status', 'totalSize', 'sizeWhenDone',
'leftUntilDone', 'rateDownload', 'rateUpload', 'eta', 'downloadedEver',
'uploadedEver', 'percentDone', 'addedDate', 'doneDate', 'trackerStats',
'labels', 'downloadDir', 'error', 'errorString', 'peersConnected',
'peersGettingFromUs', 'peersSendingToUs', 'queuePosition'
]
});
expect(downloads).toHaveLength(2);
expect(downloads[0].title).toBe('Test Torrent 1');
expect(downloads[0].status).toBe('Downloading');
expect(downloads[1].title).toBe('Test Torrent 2');
expect(downloads[1].status).toBe('Seeding');
});
it('should handle API errors gracefully', async () => {
client.makeRequest = vi.fn().mockRejectedValue(new Error('API Error'));
const downloads = await client.getActiveDownloads();
expect(downloads).toEqual([]);
});
it('should handle empty torrent list', async () => {
const mockResponse = {
data: {
result: 'success',
arguments: { torrents: [] }
}
};
client.makeRequest = vi.fn().mockResolvedValue(mockResponse);
const downloads = await client.getActiveDownloads();
expect(downloads).toEqual([]);
});
});
describe('Client Status', () => {
it('should get client status and session stats', async () => {
const mockSessionResponse = {
data: {
result: 'success',
arguments: {
'download-dir': '/downloads',
'peer-port': 51413,
'rpc-version': 15
}
}
};
const mockStatsResponse = {
data: {
result: 'success',
arguments: {
'downloaded-bytes': 1000000000,
'uploaded-bytes': 500000000,
'torrent-count': 5
}
}
};
client.makeRequest = vi.fn()
.mockResolvedValueOnce(mockSessionResponse)
.mockResolvedValueOnce(mockStatsResponse);
const status = await client.getClientStatus();
expect(client.makeRequest).toHaveBeenCalledWith('session-get');
expect(client.makeRequest).toHaveBeenCalledWith('session-stats');
expect(status).toEqual({
session: mockSessionResponse.data.arguments,
stats: mockStatsResponse.data.arguments
});
});
it('should handle status request errors', async () => {
client.makeRequest = vi.fn().mockRejectedValue(new Error('Status error'));
const status = await client.getClientStatus();
expect(status).toBeNull();
});
});
describe('ARR Info Extraction', () => {
it('should extract series info from filename', () => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Show Name - S01E02 - Episode Title',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: -1,
percentDone: 0.5,
labels: []
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.arrType).toBe('series');
});
it('should extract movie info from filename', () => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Movie Title (2023) 1080p',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: -1,
percentDone: 0.5,
labels: []
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.arrType).toBe('movie');
});
it('should not extract ARR info from generic filename', () => {
const torrent = {
id: 1,
hashString: 'abc123',
name: 'Generic File Name.mkv',
status: 4,
totalSize: 1000000,
sizeWhenDone: 1000000,
leftUntilDone: 500000,
rateDownload: 0,
eta: -1,
percentDone: 0.5,
labels: []
};
const normalized = client.normalizeDownload(torrent);
expect(normalized.arrType).toBeUndefined();
});
});
});