mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-02 15:39:37 +00:00
212 lines
8.9 KiB
TypeScript
212 lines
8.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { DownloadItem } from '../bindings/DownloadItem';
|
|
import {
|
|
downloadFileNamesMatch,
|
|
downloadFileNameWithSuffix,
|
|
downloadMediaKindsMatch,
|
|
MAX_DOWNLOAD_FILENAME_BYTES,
|
|
canonicalizeDownloadFileName,
|
|
isValidTorrentExcludeTrackerList,
|
|
isValidTorrentTrackerList,
|
|
normalizeTorrentEncryptionPolicy,
|
|
normalizeTorrentMaxOpenFiles,
|
|
normalizeTorrentPrioritizePiece,
|
|
normalizeTorrentTrackerInterval,
|
|
normalizeTorrentTrackerTimeout,
|
|
redactDownloadForPersistence,
|
|
resolveDownloadConnections
|
|
} from './downloads';
|
|
|
|
const item = (status: DownloadItem['status']): DownloadItem => ({
|
|
id: 'download-1',
|
|
url: 'https://example.com/file.bin',
|
|
fileName: 'file.bin',
|
|
status,
|
|
category: 'Other',
|
|
dateAdded: '2026-07-15T00:00:00.000Z',
|
|
downloadedBytes: 1024,
|
|
totalBytes: 4096,
|
|
totalIsEstimate: false
|
|
});
|
|
|
|
describe('download persistence progress snapshots', () => {
|
|
it('does not write active byte counters on every progress event', () => {
|
|
const persisted = redactDownloadForPersistence(item('downloading'));
|
|
|
|
expect(persisted.downloadedBytes).toBeUndefined();
|
|
expect(persisted.totalBytes).toBeUndefined();
|
|
expect(persisted.totalIsEstimate).toBeUndefined();
|
|
});
|
|
|
|
it('keeps byte counters for paused snapshots', () => {
|
|
const persisted = redactDownloadForPersistence(item('paused'));
|
|
|
|
expect(persisted.downloadedBytes).toBe(1024);
|
|
expect(persisted.totalBytes).toBe(4096);
|
|
expect(persisted.totalIsEstimate).toBe(false);
|
|
});
|
|
|
|
it.each(['queued', 'staged', 'retrying', 'processing'] as const)(
|
|
'keeps byte counters for %s snapshots',
|
|
(status) => {
|
|
const persisted = redactDownloadForPersistence(item(status));
|
|
|
|
expect(persisted.downloadedBytes).toBe(1024);
|
|
expect(persisted.totalBytes).toBe(4096);
|
|
expect(persisted.totalIsEstimate).toBe(false);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('Torrent tracker input validation', () => {
|
|
it('accepts supported trackers separated by lines or commas', () => {
|
|
expect(isValidTorrentTrackerList(
|
|
' https://tracker.example/announce\nudp://tracker.example:6969/announce '
|
|
)).toBe(true);
|
|
expect(isValidTorrentTrackerList('https://tracker.example/announce,https://tracker.example/announce')).toBe(true);
|
|
});
|
|
|
|
it('rejects unsupported, credential-bearing, empty, and oversized entries', () => {
|
|
expect(isValidTorrentTrackerList('ftp://tracker.example/announce')).toBe(false);
|
|
expect(isValidTorrentTrackerList('https://user:pass@tracker.example/announce')).toBe(false);
|
|
expect(isValidTorrentTrackerList('https://tracker.example/announce,')).toBe(false);
|
|
expect(isValidTorrentTrackerList(Array.from({ length: 65 }, (_, index) => `https://tracker${index}.example/announce`).join('\n'))).toBe(false);
|
|
});
|
|
|
|
it('accepts wildcard exclusions but rejects ambiguous mixtures', () => {
|
|
expect(isValidTorrentExcludeTrackerList('*')).toBe(true);
|
|
expect(isValidTorrentExcludeTrackerList('https://tracker.example/announce\nudp://tracker.example:6969/announce')).toBe(true);
|
|
expect(isValidTorrentExcludeTrackerList('*,https://tracker.example/announce')).toBe(false);
|
|
expect(isValidTorrentExcludeTrackerList('https://tracker.example/announce,*')).toBe(false);
|
|
expect(isValidTorrentExcludeTrackerList('https://user:pass@tracker.example/announce')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Torrent piece priority validation', () => {
|
|
it('normalizes head and tail preview policies', () => {
|
|
expect(normalizeTorrentPrioritizePiece(' tail = 64k, HEAD ')).toBe('head,tail=64K');
|
|
expect(normalizeTorrentPrioritizePiece('head=1m,tail=1024M')).toBe('head=1M,tail=1024M');
|
|
expect(normalizeTorrentPrioritizePiece('')).toBeNull();
|
|
});
|
|
|
|
it('rejects duplicate, unsupported, malformed, and oversized policies', () => {
|
|
for (const value of ['head,head', 'middle', 'head=0K', 'tail=1G', 'head=1K,', 'head=1025M']) {
|
|
expect(normalizeTorrentPrioritizePiece(value)).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Torrent encryption policy validation', () => {
|
|
it('accepts only the canonical policy states', () => {
|
|
expect(normalizeTorrentEncryptionPolicy('disabled')).toBe('disabled');
|
|
expect(normalizeTorrentEncryptionPolicy('require-crypto')).toBe('require-crypto');
|
|
expect(normalizeTorrentEncryptionPolicy('force-encryption')).toBe('force-encryption');
|
|
});
|
|
|
|
it('clears unknown or malformed persisted values', () => {
|
|
expect(normalizeTorrentEncryptionPolicy(undefined)).toBeUndefined();
|
|
expect(normalizeTorrentEncryptionPolicy('arc4')).toBeUndefined();
|
|
expect(normalizeTorrentEncryptionPolicy(true)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Torrent tracker timing validation', () => {
|
|
it('accepts bounded timeout values and an automatic interval', () => {
|
|
expect(normalizeTorrentTrackerTimeout('1')).toBe(1);
|
|
expect(normalizeTorrentTrackerTimeout(604800)).toBe(604800);
|
|
expect(normalizeTorrentTrackerInterval('0')).toBe(0);
|
|
expect(normalizeTorrentTrackerInterval(604800)).toBe(604800);
|
|
});
|
|
|
|
it('rejects zero timeouts and out-of-range timing values', () => {
|
|
expect(normalizeTorrentTrackerTimeout('0')).toBeUndefined();
|
|
expect(normalizeTorrentTrackerTimeout(604801)).toBeUndefined();
|
|
expect(normalizeTorrentTrackerInterval(-1)).toBeUndefined();
|
|
expect(normalizeTorrentTrackerInterval(604801)).toBeUndefined();
|
|
expect(normalizeTorrentTrackerTimeout('1.5')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Torrent open-file limit validation', () => {
|
|
it('accepts bounded integer limits', () => {
|
|
expect(normalizeTorrentMaxOpenFiles(1)).toBe(1);
|
|
expect(normalizeTorrentMaxOpenFiles(4096)).toBe(4096);
|
|
});
|
|
|
|
it('rejects zero, fractional, and oversized limits', () => {
|
|
expect(normalizeTorrentMaxOpenFiles(0)).toBeUndefined();
|
|
expect(normalizeTorrentMaxOpenFiles('1.5')).toBeUndefined();
|
|
expect(normalizeTorrentMaxOpenFiles(4097)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('download connection resolution', () => {
|
|
it('uses a clamped fallback for legacy rows without a saved value', () => {
|
|
expect(resolveDownloadConnections(undefined, 8)).toBe(8);
|
|
expect(resolveDownloadConnections(undefined, 0)).toBe(1);
|
|
expect(resolveDownloadConnections(undefined, Number.NaN)).toBe(16);
|
|
});
|
|
|
|
it('clamps malformed saved values before dispatch', () => {
|
|
expect(resolveDownloadConnections(0, 8)).toBe(1);
|
|
expect(resolveDownloadConnections(17, 8)).toBe(16);
|
|
expect(resolveDownloadConnections(Number.NaN, 8)).toBe(8);
|
|
});
|
|
});
|
|
|
|
describe('download filename matching', () => {
|
|
it('matches frontend filenames to the backend Windows device-name canonicalization', () => {
|
|
expect(canonicalizeDownloadFileName('CON.txt')).toBe('CON-.txt');
|
|
expect(canonicalizeDownloadFileName('CON .txt')).toBe('CON -.txt');
|
|
expect(canonicalizeDownloadFileName('com1.archive.zip')).toBe('com1.archive-.zip');
|
|
expect(downloadFileNamesMatch('CON-.txt', 'CON.txt')).toBe(true);
|
|
expect(downloadFileNamesMatch('console.txt', 'CON.txt')).toBe(false);
|
|
});
|
|
|
|
it('truncates long names by UTF-8 bytes while preserving the extension', () => {
|
|
const filename = canonicalizeDownloadFileName(`${'title '.repeat(100)}.mp4`);
|
|
|
|
expect(new TextEncoder().encode(filename).length).toBeLessThanOrEqual(MAX_DOWNLOAD_FILENAME_BYTES);
|
|
expect(filename.endsWith('.mp4')).toBe(true);
|
|
expect(filename).toContain('…');
|
|
});
|
|
|
|
it('does not split a multibyte character at the filesystem boundary', () => {
|
|
const filename = canonicalizeDownloadFileName(`${'😀'.repeat(100)}.mkv`);
|
|
|
|
expect(new TextEncoder().encode(filename).length).toBeLessThanOrEqual(MAX_DOWNLOAD_FILENAME_BYTES);
|
|
expect(filename.endsWith('.mkv')).toBe(true);
|
|
expect([...filename].every(character => character !== '\uFFFD')).toBe(true);
|
|
});
|
|
|
|
it('keeps alternate names unique and bounded after long-name truncation', () => {
|
|
const filename = downloadFileNameWithSuffix(`${'title '.repeat(100)}.mp4`, ' (1)');
|
|
|
|
expect(new TextEncoder().encode(filename).length).toBeLessThanOrEqual(MAX_DOWNLOAD_FILENAME_BYTES);
|
|
expect(filename.endsWith(' (1).mp4')).toBe(true);
|
|
});
|
|
|
|
it('matches case and path spelling while preserving the actual filename', () => {
|
|
expect(downloadFileNamesMatch(
|
|
'Media\\Example.Show.S01E01.MKV',
|
|
'example.show.s01e01.mkv'
|
|
)).toBe(true);
|
|
});
|
|
|
|
it('does not collapse distinct extensions or names', () => {
|
|
expect(downloadFileNamesMatch('example.zip', 'example.tar')).toBe(false);
|
|
expect(downloadFileNamesMatch('example-1.zip', 'example.zip')).toBe(false);
|
|
});
|
|
|
|
it('does not auto-match weak metadata fallback names', () => {
|
|
expect(downloadFileNamesMatch('download', 'download')).toBe(false);
|
|
expect(downloadFileNamesMatch('identifier', 'IDENTIFIER')).toBe(false);
|
|
expect(downloadFileNamesMatch('real-file.bin', 'real-file.bin')).toBe(true);
|
|
});
|
|
|
|
it('treats omitted media flags as ordinary downloads', () => {
|
|
expect(downloadMediaKindsMatch(undefined, false)).toBe(true);
|
|
expect(downloadMediaKindsMatch(undefined, true)).toBe(false);
|
|
});
|
|
});
|