Add guard test for DownloadBuilder service
This commit is contained in:
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user