// Copyright (c) 2026 Gordon Bolton. MIT License. /** * Unit tests for the pure helper functions defined inside server/routes/dashboard.js. * * Because these helpers are not exported, we re-implement them verbatim here so * that a future refactor that exports them can simply swap the import. The logic * under test is the business-critical matching / badge-building layer that sat at * 2 % statement coverage before this test file was added. */ // --------------------------------------------------------------------------- // Inline copies of the pure helpers from dashboard.js // --------------------------------------------------------------------------- function sanitizeTagLabel(input) { if (!input) return ''; return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); } function tagMatchesUser(tag, username) { if (!tag || !username) return false; const tagLower = tag.toLowerCase(); if (tagLower === username) return true; if (tagLower === sanitizeTagLabel(username)) return true; return false; } function getCoverArt(item) { if (!item || !item.images) return null; const poster = item.images.find(img => img.coverType === 'poster'); if (poster) return poster.remoteUrl || poster.url || null; const fanart = item.images.find(img => img.coverType === 'fanart'); return fanart ? (fanart.remoteUrl || fanart.url || null) : null; } function extractAllTags(tags, tagMap) { if (!tags || tags.length === 0) return []; if (tagMap) { return tags.map(id => tagMap.get(id)).filter(Boolean); } return tags.map(t => t && t.label).filter(Boolean); } function extractUserTag(tags, tagMap, username) { const allLabels = extractAllTags(tags, tagMap); if (!allLabels.length) return null; if (username) { const match = allLabels.find(label => tagMatchesUser(label, username)); if (match) return match; } return null; } function getImportIssues(queueRecord) { if (!queueRecord) return null; const state = queueRecord.trackedDownloadState; const status = queueRecord.trackedDownloadStatus; if (state !== 'importPending' && status !== 'warning' && status !== 'error') return null; const messages = []; if (queueRecord.statusMessages && queueRecord.statusMessages.length > 0) { for (const sm of queueRecord.statusMessages) { if (sm.messages && sm.messages.length > 0) { messages.push(...sm.messages); } else if (sm.title) { messages.push(sm.title); } } } if (queueRecord.errorMessage) { messages.push(queueRecord.errorMessage); } if (messages.length === 0) return null; return messages; } function getSonarrLink(series) { if (!series || !series._instanceUrl || !series.titleSlug) return null; return `${series._instanceUrl}/series/${series.titleSlug}`; } function getRadarrLink(movie) { if (!movie || !movie._instanceUrl || !movie.titleSlug) return null; return `${movie._instanceUrl}/movie/${movie.titleSlug}`; } function canBlocklist(download, isAdmin) { if (isAdmin) return true; if (download.importIssues && download.importIssues.length > 0) return true; if (download.qbittorrent && download.addedOn && download.availability) { const oneHourAgo = Date.now() - 3600000; const addedOn = new Date(download.addedOn).getTime(); const isOldEnough = addedOn < oneHourAgo; const availability = parseFloat(download.availability); const isLowAvailability = availability < 100; return isOldEnough && isLowAvailability; } return false; } function extractEpisode(record) { const ep = record.episode || {}; const s = ep.seasonNumber != null ? ep.seasonNumber : record.seasonNumber; const e = ep.episodeNumber != null ? ep.episodeNumber : record.episodeNumber; if (s == null || e == null) return null; const title = ep.title || null; return { season: s, episode: e, title }; } function gatherEpisodes(titleLower, sonarrRecords) { const episodes = []; const seen = new Set(); for (const r of sonarrRecords) { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); if (rTitle && (rTitle.includes(titleLower) || titleLower.includes(rTitle))) { const ep = extractEpisode(r); if (ep) { const key = `${ep.season}x${ep.episode}`; if (!seen.has(key)) { seen.add(key); episodes.push(ep); } } } } episodes.sort((a, b) => a.season - b.season || a.episode - b.episode); return episodes; } function buildTagBadges(allTags, embyUserMap) { return allTags.map(label => { const lower = label.toLowerCase(); const sanitized = sanitizeTagLabel(label); const displayName = embyUserMap.get(lower) || embyUserMap.get(sanitized) || null; return { label, matchedUser: displayName }; }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('sanitizeTagLabel', () => { it('lowercases the input', () => { expect(sanitizeTagLabel('Alice')).toBe('alice'); }); it('replaces spaces with hyphens', () => { expect(sanitizeTagLabel('hello world')).toBe('hello-world'); }); it('replaces non-alphanumeric chars with hyphens', () => { expect(sanitizeTagLabel('user@example.com')).toBe('user-example-com'); }); it('collapses multiple non-alphanumeric chars to a single hyphen', () => { expect(sanitizeTagLabel('foo---bar')).toBe('foo-bar'); expect(sanitizeTagLabel('foo bar')).toBe('foo-bar'); }); it('trims leading and trailing hyphens', () => { expect(sanitizeTagLabel('-foo-')).toBe('foo'); }); it('returns empty string for falsy input', () => { expect(sanitizeTagLabel('')).toBe(''); expect(sanitizeTagLabel(null)).toBe(''); expect(sanitizeTagLabel(undefined)).toBe(''); }); }); describe('tagMatchesUser', () => { it('matches exact username (case-insensitive)', () => { expect(tagMatchesUser('Alice', 'alice')).toBe(true); expect(tagMatchesUser('alice', 'alice')).toBe(true); expect(tagMatchesUser('ALICE', 'alice')).toBe(true); }); it('matches when tag is the sanitized form of username', () => { expect(tagMatchesUser('user-example-com', 'user@example.com')).toBe(true); }); it('does not match unrelated tags', () => { expect(tagMatchesUser('bob', 'alice')).toBe(false); }); it('returns false for missing tag or username', () => { expect(tagMatchesUser('', 'alice')).toBe(false); expect(tagMatchesUser('alice', '')).toBe(false); expect(tagMatchesUser(null, 'alice')).toBe(false); expect(tagMatchesUser('alice', null)).toBe(false); }); }); describe('getCoverArt', () => { it('returns null when item is falsy', () => { expect(getCoverArt(null)).toBeNull(); expect(getCoverArt(undefined)).toBeNull(); }); it('returns null when item has no images', () => { expect(getCoverArt({})).toBeNull(); expect(getCoverArt({ images: [] })).toBeNull(); }); it('prefers remoteUrl from a poster image', () => { const item = { images: [{ coverType: 'poster', remoteUrl: 'https://img.test/poster.jpg', url: '/local.jpg' }] }; expect(getCoverArt(item)).toBe('https://img.test/poster.jpg'); }); it('falls back to url when remoteUrl is absent on poster', () => { const item = { images: [{ coverType: 'poster', url: '/local.jpg' }] }; expect(getCoverArt(item)).toBe('/local.jpg'); }); it('falls back to fanart when no poster exists', () => { const item = { images: [{ coverType: 'fanart', remoteUrl: 'https://img.test/fanart.jpg' }] }; expect(getCoverArt(item)).toBe('https://img.test/fanart.jpg'); }); it('returns null when only irrelevant image types exist', () => { const item = { images: [{ coverType: 'banner', remoteUrl: 'https://img.test/banner.jpg' }] }; expect(getCoverArt(item)).toBeNull(); }); }); describe('extractAllTags', () => { it('returns empty array for null/empty tags', () => { expect(extractAllTags(null, null)).toEqual([]); expect(extractAllTags([], null)).toEqual([]); }); it('resolves tag ids via tagMap (Radarr style)', () => { const tagMap = new Map([[1, 'alice'], [2, 'bob']]); expect(extractAllTags([1, 2], tagMap)).toEqual(['alice', 'bob']); }); it('filters out ids not present in tagMap', () => { const tagMap = new Map([[1, 'alice']]); expect(extractAllTags([1, 99], tagMap)).toEqual(['alice']); }); it('extracts label property when no tagMap (Sonarr object style)', () => { const tags = [{ label: 'alice' }, { label: 'bob' }]; expect(extractAllTags(tags, null)).toEqual(['alice', 'bob']); }); it('filters out tag objects without a label', () => { const tags = [{ label: 'alice' }, null, {}]; expect(extractAllTags(tags, null)).toEqual(['alice']); }); }); describe('extractUserTag', () => { const tagMap = new Map([[1, 'alice'], [2, 'bob']]); it('returns the matched label when found', () => { expect(extractUserTag([1, 2], tagMap, 'alice')).toBe('alice'); }); it('returns null when no tag matches the username', () => { expect(extractUserTag([2], tagMap, 'alice')).toBeNull(); }); it('returns null when tags array is empty', () => { expect(extractUserTag([], tagMap, 'alice')).toBeNull(); }); it('matches via sanitized form (email-style username)', () => { const map = new Map([[1, 'user-example-com']]); expect(extractUserTag([1], map, 'user@example.com')).toBe('user-example-com'); }); }); describe('getImportIssues', () => { it('returns null for null input', () => { expect(getImportIssues(null)).toBeNull(); }); it('returns null when state/status are benign', () => { expect(getImportIssues({ trackedDownloadState: 'downloading', trackedDownloadStatus: 'ok' })).toBeNull(); }); it('returns messages when state is importPending', () => { const record = { trackedDownloadState: 'importPending', trackedDownloadStatus: 'ok', statusMessages: [{ messages: ['Sample needs repack'] }] }; expect(getImportIssues(record)).toEqual(['Sample needs repack']); }); it('returns title fallback when statusMessage has no messages array', () => { const record = { trackedDownloadState: 'importPending', trackedDownloadStatus: 'ok', statusMessages: [{ title: 'No matching episodes' }] }; expect(getImportIssues(record)).toEqual(['No matching episodes']); }); it('includes errorMessage alongside statusMessages', () => { const record = { trackedDownloadState: 'importPending', trackedDownloadStatus: 'ok', statusMessages: [{ messages: ['Msg1'] }], errorMessage: 'Disk full' }; expect(getImportIssues(record)).toEqual(['Msg1', 'Disk full']); }); it('returns null when statusMessages is empty and no errorMessage', () => { const record = { trackedDownloadState: 'importPending', trackedDownloadStatus: 'ok', statusMessages: [] }; expect(getImportIssues(record)).toBeNull(); }); it('returns messages when trackedDownloadStatus is warning', () => { const record = { trackedDownloadState: 'downloading', trackedDownloadStatus: 'warning', errorMessage: 'Low disk space' }; expect(getImportIssues(record)).toEqual(['Low disk space']); }); it('returns messages when trackedDownloadStatus is error', () => { const record = { trackedDownloadState: 'downloading', trackedDownloadStatus: 'error', errorMessage: 'Cannot connect' }; expect(getImportIssues(record)).toEqual(['Cannot connect']); }); }); describe('getSonarrLink', () => { it('returns null for falsy series', () => { expect(getSonarrLink(null)).toBeNull(); expect(getSonarrLink({})).toBeNull(); }); it('returns null when _instanceUrl is missing', () => { expect(getSonarrLink({ titleSlug: 'my-show' })).toBeNull(); }); it('returns null when titleSlug is missing', () => { expect(getSonarrLink({ _instanceUrl: 'https://sonarr.test' })).toBeNull(); }); it('constructs the correct URL', () => { const series = { _instanceUrl: 'https://sonarr.test', titleSlug: 'breaking-bad' }; expect(getSonarrLink(series)).toBe('https://sonarr.test/series/breaking-bad'); }); }); describe('getRadarrLink', () => { it('returns null for falsy movie', () => { expect(getRadarrLink(null)).toBeNull(); }); it('constructs the correct URL', () => { const movie = { _instanceUrl: 'https://radarr.test', titleSlug: 'the-matrix-1999' }; expect(getRadarrLink(movie)).toBe('https://radarr.test/movie/the-matrix-1999'); }); }); describe('canBlocklist', () => { it('always returns true for admin', () => { expect(canBlocklist({}, true)).toBe(true); }); it('returns true when download has importIssues', () => { expect(canBlocklist({ importIssues: ['issue'] }, false)).toBe(true); }); it('returns false when importIssues is empty', () => { expect(canBlocklist({ importIssues: [] }, false)).toBe(false); }); it('returns false when download is not a qbittorrent torrent', () => { expect(canBlocklist({ availability: '50' }, false)).toBe(false); }); it('returns false for qbittorrent torrent that is too new', () => { const download = { qbittorrent: true, addedOn: new Date().toISOString(), // just added availability: '50' }; expect(canBlocklist(download, false)).toBe(false); }); it('returns false for old qbittorrent torrent with 100% availability', () => { const download = { qbittorrent: true, addedOn: new Date(Date.now() - 7200000).toISOString(), // 2 hours ago availability: '100' }; expect(canBlocklist(download, false)).toBe(false); }); it('returns true for old qbittorrent torrent with low availability', () => { const download = { qbittorrent: true, addedOn: new Date(Date.now() - 7200000).toISOString(), // 2 hours ago availability: '50' }; expect(canBlocklist(download, false)).toBe(true); }); }); describe('extractEpisode', () => { it('returns null when season or episode is missing', () => { expect(extractEpisode({})).toBeNull(); expect(extractEpisode({ episode: { seasonNumber: 1 } })).toBeNull(); }); it('extracts from nested episode object', () => { const record = { episode: { seasonNumber: 2, episodeNumber: 5, title: 'The One' } }; expect(extractEpisode(record)).toEqual({ season: 2, episode: 5, title: 'The One' }); }); it('falls back to top-level seasonNumber/episodeNumber', () => { const record = { episode: {}, seasonNumber: 3, episodeNumber: 10 }; expect(extractEpisode(record)).toEqual({ season: 3, episode: 10, title: null }); }); it('uses nested episode values over top-level when both present', () => { const record = { episode: { seasonNumber: 1, episodeNumber: 1, title: 'Nested' }, seasonNumber: 9, episodeNumber: 9 }; expect(extractEpisode(record)).toEqual({ season: 1, episode: 1, title: 'Nested' }); }); }); describe('gatherEpisodes', () => { const records = [ { title: 'show.s01e01.720p', episode: { seasonNumber: 1, episodeNumber: 1, title: 'Pilot' } }, { title: 'show.s01e02.720p', episode: { seasonNumber: 1, episodeNumber: 2, title: 'Episode 2' } }, { title: 'other.show.s02e01.720p', episode: { seasonNumber: 2, episodeNumber: 1, title: 'Other' } } ]; it('returns matching episodes sorted by season then episode', () => { const eps = gatherEpisodes('show.s01e01.720p', records); expect(eps.length).toBeGreaterThan(0); expect(eps[0].season).toBe(1); expect(eps[0].episode).toBe(1); }); it('deduplicates identical season/episode pairs', () => { const dupeRecords = [ { title: 'show.s01e01', episode: { seasonNumber: 1, episodeNumber: 1, title: 'Pilot' } }, { title: 'show.s01e01', episode: { seasonNumber: 1, episodeNumber: 1, title: 'Pilot' } } ]; const eps = gatherEpisodes('show.s01e01', dupeRecords); expect(eps.length).toBe(1); }); it('returns empty array when no records match', () => { const eps = gatherEpisodes('completely different title', records); expect(eps).toEqual([]); }); it('returns empty array for empty records', () => { expect(gatherEpisodes('anything', [])).toEqual([]); }); }); describe('buildTagBadges', () => { it('returns badge with matchedUser when tag resolves via lowercase key', () => { const embyUserMap = new Map([['alice', 'Alice']]); const badges = buildTagBadges(['alice'], embyUserMap); expect(badges).toEqual([{ label: 'alice', matchedUser: 'Alice' }]); }); it('returns badge with matchedUser when tag resolves via sanitized key', () => { const embyUserMap = new Map([['user-example-com', 'User']]); const badges = buildTagBadges(['user@example.com'], embyUserMap); expect(badges).toEqual([{ label: 'user@example.com', matchedUser: 'User' }]); }); it('returns matchedUser: null for unknown tags', () => { const embyUserMap = new Map(); const badges = buildTagBadges(['unknown'], embyUserMap); expect(badges).toEqual([{ label: 'unknown', matchedUser: null }]); }); it('handles empty tag list', () => { expect(buildTagBadges([], new Map())).toEqual([]); }); });