Add guard test for DownloadBuilder service
Build and Push Docker Image / build (push) Successful in 40s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 53s
CI / Tests & coverage (push) Failing after 1m18s
CI / Security audit (push) Successful in 1m27s

This commit is contained in:
2026-05-20 22:34:24 +01:00
parent 9cffb96f29
commit d74b46d5b0
+928
View File
@@ -0,0 +1,928 @@
// 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('sets canBlocklist correctly for non-admin users', () => {
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(), // 2 hours ago
availability: '50' // Low availability
}]
};
const result = buildUserDownloads(cacheSnapshot, {
username,
usernameSanitized,
isAdmin: false,
showAll,
seriesMap,
moviesMap,
sonarrTagMap,
radarrTagMap,
embyUserMap
});
expect(result).toHaveLength(1);
// Non-admin with old torrent and low availability should be able to blocklist
expect(result[0].canBlocklist).toBe(true);
});
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');
});
});