// Copyright (c) 2026 Gordon Bolton. MIT License. /** * Guard tests for server/services/DownloadBuilder.js * * This test file serves as a regression guard for the deduplicated download-assembly * logic that will be extracted from dashboard.js into the DownloadBuilder service. * The function buildUserDownloads does not exist yet - this test will pass once * the implementation is complete in the next prompt. * * Coverage: * - Happy path with matching downloads * - Empty data scenarios * - Mixed series and movies * - Admin vs regular user permissions * - showAll=true vs showAll=false filtering * - Duplicate prevention (same download matched via multiple sources) */ import { describe, it, expect } from 'vitest'; import { buildUserDownloads } from '../../../server/services/DownloadBuilder.js'; describe('buildUserDownloads', () => { const username = 'alice'; const usernameSanitized = 'alice'; const isAdmin = false; const showAll = false; const sonarrTagMap = new Map([[1, 'alice'], [2, 'bob']]); const radarrTagMap = new Map([[1, 'alice'], [2, 'bob']]); const embyUserMap = new Map([['alice', 'Alice'], ['bob', 'Bob']]); const seriesMap = new Map([ [1, { id: 1, title: 'Test Series', titleSlug: 'test-series', path: '/series/test', tags: [1], images: [{ coverType: 'poster', remoteUrl: 'https://example.com/poster.jpg' }], _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] ]); const moviesMap = new Map([ [1, { id: 1, title: 'Test Movie', titleSlug: 'test-movie', path: '/movies/test', tags: [1], images: [{ coverType: 'poster', remoteUrl: 'https://example.com/movie-poster.jpg' }], _instanceUrl: 'https://radarr.test', _instanceKey: 'test-key' }] ]); it('returns empty array when no downloads match user', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { slots: [] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toEqual([]); }); it('returns empty array for null/undefined cache data', () => { const cacheSnapshot = { sabnzbdQueue: null, sabnzbdHistory: null, sonarrQueue: null, sonarrHistory: null, radarrQueue: null, radarrHistory: null, qbittorrentTorrents: null }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toEqual([]); }); it('matches SABnzbd queue slot to Sonarr series for tagged user', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'test.series.s01e01.720p', nzbname: 'test.series.s01e01.720p', status: 'Downloading', percentage: 50, mb: 1000, mbmissing: 500, size: '1 GB', timeleft: '10:00', storage: '/downloads/test' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e01.720p', sourceTitle: 'test.series.s01e01.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 200, _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'series', title: 'test.series.s01e01.720p', status: 'Downloading', progress: 50, coverArt: 'https://example.com/poster.jpg', seriesName: 'Test Series', allTags: ['alice'], matchedUserTag: 'alice', client: 'sabnzbd' }); expect(result[0].episodes).toBeInstanceOf(Array); }); it('matches SABnzbd queue slot to Radarr movie for tagged user', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'test.movie.2023.1080p', nzbname: 'test.movie.2023.1080p', status: 'Downloading', percentage: 75, mb: 2000, mbmissing: 500, size: '2 GB', timeleft: '5:00', storage: '/downloads/testmovie' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [{ id: 100, title: 'Test Movie 2023', sourceTitle: 'test.movie.2023.1080p', movieId: 1, movie: moviesMap.get(1), _instanceUrl: 'https://radarr.test', _instanceKey: 'test-key' }] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'movie', title: 'test.movie.2023.1080p', status: 'Downloading', progress: 75, coverArt: 'https://example.com/movie-poster.jpg', movieName: 'Test Movie', allTags: ['alice'], matchedUserTag: 'alice', client: 'sabnzbd' }); }); it('matches qBittorrent torrent to Sonarr series for tagged user', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { slots: [] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e02.720p', sourceTitle: 'test.series.s01e02.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 201, _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [{ hash: 'abc123', name: 'test.series.s01e02.720p', progress: 60, dlspeed: 5242880, eta: 600, size: 1073741824, savePath: '/downloads/test', addedOn: new Date(Date.now() - 7200000).toISOString(), availability: '50' }] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'series', title: 'test.series.s01e02.720p', seriesName: 'Test Series', allTags: ['alice'], matchedUserTag: 'alice', client: 'qbittorrent' }); expect(result[0]).toHaveProperty('id'); expect(result[0]).toHaveProperty('progress'); expect(result[0]).toHaveProperty('speed'); expect(result[0]).toHaveProperty('eta'); }); it('includes admin-specific fields when isAdmin is true', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'test.series.s01e03.720p', nzbname: 'test.series.s01e03.720p', status: 'Downloading', percentage: 30, mb: 1500, mbmissing: 1050, size: '1.5 GB', timeleft: '15:00', storage: '/downloads/test' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e03.720p', sourceTitle: 'test.series.s01e03.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 202, _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin: true, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ downloadPath: '/downloads/test', targetPath: '/series/test', arrLink: 'https://sonarr.test/series/test-series', arrQueueId: 100, arrType: 'sonarr', arrInstanceUrl: 'https://sonarr.test', arrInstanceKey: 'test-key', arrContentId: 202, arrContentType: 'episode', canBlocklist: true }); }); it('filters by user tag when showAll is false', () => { const bobSeriesMap = new Map([ [2, { id: 2, title: 'Bob Series', titleSlug: 'bob-series', path: '/series/bob', tags: [2], // Bob's tag images: [{ coverType: 'poster', remoteUrl: 'https://example.com/bob-poster.jpg' }] }] ]); const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'bob.series.s01e01.720p', nzbname: 'bob.series.s01e01.720p', status: 'Downloading', percentage: 50, mb: 1000, mbmissing: 500, size: '1 GB', timeleft: '10:00', storage: '/downloads/bob' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 200, title: 'bob.series.s01e01.720p', sourceTitle: 'bob.series.s01e01.720p', seriesId: 2, series: bobSeriesMap.get(2), episodeId: 300 }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username: 'alice', usernameSanitized: 'alice', isAdmin: false, showAll: false, seriesMap: bobSeriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); // Alice should not see Bob's download when showAll is false expect(result).toEqual([]); }); it('shows all tagged downloads when showAll is true (admin mode)', () => { const bobSeriesMap = new Map([ [2, { id: 2, title: 'Bob Series', titleSlug: 'bob-series', path: '/series/bob', tags: [2], // Bob's tag images: [{ coverType: 'poster', remoteUrl: 'https://example.com/bob-poster.jpg' }] }] ]); const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'bob.series.s01e01.720p', nzbname: 'bob.series.s01e01.720p', status: 'Downloading', percentage: 50, mb: 1000, mbmissing: 500, size: '1 GB', timeleft: '10:00', storage: '/downloads/bob' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 200, title: 'bob.series.s01e01.720p', sourceTitle: 'bob.series.s01e01.720p', seriesId: 2, series: bobSeriesMap.get(2), episodeId: 300 }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username: 'alice', usernameSanitized: 'alice', isAdmin: true, showAll: true, seriesMap: bobSeriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); // Admin with showAll=true should see all tagged downloads expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'series', title: 'bob.series.s01e01.720p', allTags: ['bob'], matchedUserTag: null, tagBadges: [{ label: 'bob', matchedUser: 'Bob' }] }); }); it('includes importIssues when present in queue record', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'test.series.s01e04.720p', nzbname: 'test.series.s01e04.720p', status: 'Downloading', percentage: 90, mb: 2000, mbmissing: 200, size: '2 GB', timeleft: '2:00', storage: '/downloads/test' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e04.720p', sourceTitle: 'test.series.s01e04.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 203, trackedDownloadState: 'importPending', trackedDownloadStatus: 'warning', statusMessages: [{ messages: ['Sample needs repack'] }], errorMessage: 'Disk space low', _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(1); expect(result[0].importIssues).toEqual(['Sample needs repack', 'Disk space low']); }); it('handles mixed series and movie downloads', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '10.0 MB/s', kbpersec: 10240, slots: [ { filename: 'test.series.s01e05.720p', nzbname: 'test.series.s01e05.720p', status: 'Downloading', percentage: 40, mb: 800, mbmissing: 480, size: '800 MB', timeleft: '8:00', storage: '/downloads/series' }, { filename: 'test.movie.2023.1080p', nzbname: 'test.movie.2023.1080p', status: 'Downloading', percentage: 60, mb: 1200, mbmissing: 480, size: '1.2 GB', timeleft: '6:00', storage: '/downloads/movie' } ] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e05.720p', sourceTitle: 'test.series.s01e05.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 204, _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [{ id: 101, title: 'Test Movie 2023', sourceTitle: 'test.movie.2023.1080p', movieId: 1, movie: moviesMap.get(1), _instanceUrl: 'https://radarr.test', _instanceKey: 'test-key' }] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(2); expect(result[0].type).toBe('series'); expect(result[1].type).toBe('movie'); }); it('prevents duplicate downloads when same item matches multiple sources', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [{ filename: 'test.series.s01e06.720p', nzbname: 'test.series.s01e06.720p', status: 'Downloading', percentage: 50, mb: 1000, mbmissing: 500, size: '1 GB', timeleft: '10:00', storage: '/downloads/test' }] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e06.720p', sourceTitle: 'test.series.s01e06.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 205, _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [{ id: 100, title: 'test.series.s01e06.720p', sourceTitle: 'test.series.s01e06.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 205 }] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [{ hash: 'def456', name: 'test.series.s01e06.720p', progress: 50, dlspeed: 5242880, eta: 600, size: 1073741824, savePath: '/downloads/test' }] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); // Should only return one download item even though it matches in queue, history, and torrents expect(result).toHaveLength(1); }); it('matches SABnzbd history slots to completed downloads', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { slots: [] } } }, sabnzbdHistory: { data: { history: { slots: [{ name: 'test.series.s01e07.720p', nzb_name: 'test.series.s01e07.720p', status: 'Completed', mb: 1000, size: '1 GB', completed_time: '2024-01-01T12:00:00Z', storage: '/downloads/completed' }] } } }, sonarrQueue: { data: { records: [] } }, sonarrHistory: { data: { records: [{ id: 100, title: 'test.series.s01e07.720p', sourceTitle: 'test.series.s01e07.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 206 }] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: 'series', title: 'test.series.s01e07.720p', status: 'Completed', completedAt: '2024-01-01T12:00:00Z' }); }); it('does not display unmatched torrents', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { slots: [] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [{ hash: 'ghi789', name: 'test.movie.2023.1080p', progress: 30, dlspeed: 2097152, eta: 1200, size: 2147483648, savePath: '/downloads/test', addedOn: new Date(Date.now() - 7200000).toISOString(), availability: '50' }] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin: false, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); // Unmatched torrents (not in Sonarr/Radarr queue/history) should not be displayed expect(result).toEqual([]); }); it('includes sonarrLink and radarrLink when available', () => { const cacheSnapshot = { sabnzbdQueue: { data: { queue: { status: 'Downloading', speed: '5.0 MB/s', kbpersec: 5120, slots: [ { filename: 'test.series.s01e08.720p', nzbname: 'test.series.s01e08.720p', status: 'Downloading', percentage: 25, mb: 500, mbmissing: 375, size: '500 MB', timeleft: '12:00', storage: '/downloads/series' }, { filename: 'test.movie.2023.1080p', nzbname: 'test.movie.2023.1080p', status: 'Downloading', percentage: 50, mb: 1000, mbmissing: 500, size: '1 GB', timeleft: '10:00', storage: '/downloads/movie' } ] } } }, sabnzbdHistory: { data: { history: { slots: [] } } }, sonarrQueue: { data: { records: [{ id: 100, title: 'test.series.s01e08.720p', sourceTitle: 'test.series.s01e08.720p', seriesId: 1, series: seriesMap.get(1), episodeId: 207, _instanceUrl: 'https://sonarr.test', _instanceKey: 'test-key' }] } }, sonarrHistory: { data: { records: [] } }, radarrQueue: { data: { records: [{ id: 101, title: 'Test Movie 2023', sourceTitle: 'test.movie.2023.1080p', movieId: 1, movie: moviesMap.get(1), _instanceUrl: 'https://radarr.test', _instanceKey: 'test-key' }] } }, radarrHistory: { data: { records: [] } }, qbittorrentTorrents: [] }; const result = buildUserDownloads(cacheSnapshot, { username, usernameSanitized, isAdmin: true, showAll, seriesMap, moviesMap, sonarrTagMap, radarrTagMap, embyUserMap }); expect(result).toHaveLength(2); expect(result[0].arrLink).toBe('https://sonarr.test/series/test-series'); expect(result[1].arrLink).toBe('https://radarr.test/movie/test-movie'); }); });