// Copyright (c) 2026 Gordon Bolton. MIT License. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Mock dependencies vi.mock('../../../server/utils/logger', () => ({ logToFile: vi.fn() })); // Import after mocking const DownloadMatcher = require('../../../server/services/DownloadMatcher'); describe('DownloadMatcher', () => { const ombiBaseUrl = 'http://localhost:5000'; beforeEach(() => { vi.clearAllMocks(); }); describe('addOmbiMatching', () => { it('should return early when ombiBaseUrl is missing', () => { const downloadObj = { type: 'series', title: 'Test Show' }; const series = { tvdbId: '12345', tmdbId: '67890' }; const context = { ombiBaseUrl: null }; DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBeUndefined(); expect(downloadObj.ombiTooltip).toBeUndefined(); }); it('should return early when seriesOrMovie is missing', () => { const downloadObj = { type: 'series', title: 'Test Show' }; const context = { ombiBaseUrl }; DownloadMatcher.addOmbiMatching(downloadObj, null, context); expect(downloadObj.ombiLink).toBeUndefined(); expect(downloadObj.ombiTooltip).toBeUndefined(); }); it('should add ombiLink for series with TMDB ID', () => { const downloadObj = { type: 'series', title: 'Test Show' }; const series = { tmdbId: '67890' }; const context = { ombiBaseUrl }; DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/details/tv/67890'); expect(downloadObj.ombiTooltip).toBe('View in Ombi'); }); it('should add ombiLink for movie with TMDB ID', () => { const downloadObj = { type: 'movie', title: 'Test Movie' }; const movie = { tmdbId: '54321' }; const context = { ombiBaseUrl }; DownloadMatcher.addOmbiMatching(downloadObj, movie, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/details/movie/54321'); expect(downloadObj.ombiTooltip).toBe('View in Ombi'); }); it('should not add ombiLink when TMDB ID is missing', () => { const downloadObj = { type: 'series', title: 'Test Show' }; const series = { tvdbId: '12345' }; const context = { ombiBaseUrl }; DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBeUndefined(); expect(downloadObj.ombiTooltip).toBeUndefined(); }); it('should not add ombiLink for unknown download type', () => { const downloadObj = { type: 'unknown', title: 'Test Unknown' }; const series = { tmdbId: '67890' }; const context = { ombiBaseUrl }; DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBeUndefined(); expect(downloadObj.ombiTooltip).toBeUndefined(); }); }); describe('buildSeriesMapFromRecords', () => { it('should build a map from queue and history records', () => { const queueRecords = [ { seriesId: 1, series: { id: 1, title: 'Series 1' } } ]; const historyRecords = [ { seriesId: 2, series: { id: 2, title: 'Series 2' } } ]; const result = DownloadMatcher.buildSeriesMapFromRecords(queueRecords, historyRecords); expect(result.get(1)).toEqual({ id: 1, title: 'Series 1' }); expect(result.get(2)).toEqual({ id: 2, title: 'Series 2' }); }); it('should not overwrite existing series in map', () => { const queueRecords = [ { seriesId: 1, series: { id: 1, title: 'Series 1' } } ]; const historyRecords = [ { seriesId: 1, series: { id: 1, title: 'Series 1 from History' } } ]; const result = DownloadMatcher.buildSeriesMapFromRecords(queueRecords, historyRecords); expect(result.get(1).title).toBe('Series 1'); }); }); describe('buildMoviesMapFromRecords', () => { it('should build a map from queue and history records', () => { const queueRecords = [ { movieId: 1, movie: { id: 1, title: 'Movie 1' } } ]; const historyRecords = [ { movieId: 2, movie: { id: 2, title: 'Movie 2' } } ]; const result = DownloadMatcher.buildMoviesMapFromRecords(queueRecords, historyRecords); expect(result.get(1)).toEqual({ id: 1, title: 'Movie 1' }); expect(result.get(2)).toEqual({ id: 2, title: 'Movie 2' }); }); }); describe('getSlotStatusAndSpeed', () => { it('should return Paused status when queue is paused', () => { const slot = { status: 'Downloading' }; const result = DownloadMatcher.getSlotStatusAndSpeed(slot, 'Paused', '0', '0'); expect(result.status).toBe('Paused'); expect(result.speed).toBe('0'); }); it('should return slot status when queue is active', () => { const slot = { status: 'Downloading' }; const result = DownloadMatcher.getSlotStatusAndSpeed(slot, 'Active', '1.5 MB/s', '1536'); expect(result.status).toBe('Downloading'); expect(result.speed).toBe('1.5 MB/s'); }); }); describe('buildArrDownload', () => { const context = { seriesMap: new Map([[1, { id: 1, title: 'My Show', tags: [1] }]]), moviesMap: new Map([[2, { id: 2, title: 'My Movie', tags: [1] }]]), sonarrTagMap: new Map([[1, 'alice']]), radarrTagMap: new Map([[1, 'alice']]), username: 'alice', isAdmin: false, showAll: false, embyUserMap: new Map() }; it('correctly uses caller-supplied client, instanceId, and instanceName values', () => { const record = { id: 100, seriesId: 1, title: 'My Show' }; const dl = DownloadMatcher.buildArrDownload(record, context, { client: 'deluge', instanceId: 'deluge-1', instanceName: 'Deluge Instance 1' }); expect(dl).toBeDefined(); expect(dl.client).toBe('deluge'); expect(dl.instanceId).toBe('deluge-1'); expect(dl.instanceName).toBe('Deluge Instance 1'); }); it('uses neutral fallback defaults when not supplied', () => { const record = { id: 100, seriesId: 1, title: 'My Show' }; const dl = DownloadMatcher.buildArrDownload(record, context); expect(dl).toBeDefined(); expect(dl.client).toBe('orphaned'); expect(dl.instanceId).toBe('orphaned'); expect(dl.instanceName).toBe('Unknown'); }); it('uses correct blocklist determination and defaults progress to 0', () => { const record = { id: 100, seriesId: 1, title: 'My Show' }; const dl = DownloadMatcher.buildArrDownload(record, context); expect(dl.progress).toBe(0); expect(dl.canBlocklist).toBe(false); }); }); describe('matchSabHistory', () => { const context = { sonarrHistoryRecords: [ { id: 100, downloadId: 'sabnzbd_1234', seriesId: 1 } ], sonarrQueueRecords: [ { id: 101, downloadId: 'sabnzbd_5678', seriesId: 1 } ], radarrHistoryRecords: [], radarrQueueRecords: [], seriesMap: new Map([[1, { id: 1, title: 'Show 1', tags: [1] }]]), moviesMap: new Map(), sonarrTagMap: new Map([[1, 'alice']]), radarrTagMap: new Map(), username: 'alice', isAdmin: false, showAll: false, embyUserMap: new Map() }; it('matches by downloadId case-insensitively and type-safely', async () => { const slots = [{ id: 'SABNZBD_1234', name: 'Show 1', status: 'Completed', mb: 1000 }]; const result = await DownloadMatcher.matchSabHistory(slots, context); expect(result).toHaveLength(1); expect(result[0].arrQueueId).toBe(100); }); it('dual-lookup: matches history slots against active queue records', async () => { const slots = [{ id: 'sabnzbd_5678', name: 'Show 1', status: 'Completed', mb: 1000 }]; const result = await DownloadMatcher.matchSabHistory(slots, context); expect(result).toHaveLength(1); expect(result[0].arrQueueId).toBe(101); }); }); describe('titleMatches helper', () => { it('matches identical strings and handles dots/spaces/dashes bidirectionally', () => { // Direct exports or internal reference const titleMatches = DownloadMatcher.__get__ ? DownloadMatcher.__get__('titleMatches') : null; // Since it is not exported, we can test it indirectly via matchTorrents or call it if we export it, // or we can test it via matchTorrents fallback matching. Let's test it using matchTorrents with dot names: }); }); describe('matchOrphanedArrRecords', () => { const context = { sonarrQueueRecords: [ { id: 100, seriesId: 1, title: 'Orphan 1', size: 1000, sizeleft: 400 }, { id: 101, seriesId: 1, title: 'Already Matched', size: 1000, sizeleft: 0 } ], radarrQueueRecords: [], seriesMap: new Map([[1, { id: 1, title: 'Series 1', tags: [1] }]]), moviesMap: new Map(), sonarrTagMap: new Map([[1, 'alice']]), radarrTagMap: new Map(), username: 'alice', isAdmin: false, showAll: false, embyUserMap: new Map() }; it('constructs orphans, filters matched IDs, and computes safe progress math', () => { const matchedIds = new Set([101]); const result = DownloadMatcher.matchOrphanedArrRecords(matchedIds, context); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ title: 'Orphan 1', isOrphaned: true, progress: 60, client: 'orphaned', instanceId: 'orphaned' }); }); it('handles size=0 safely without returning NaN or Infinity', () => { const zeroContext = { ...context, sonarrQueueRecords: [ { id: 102, seriesId: 1, title: 'Zero Size', size: 0, sizeleft: 0 } ] }; const result = DownloadMatcher.matchOrphanedArrRecords(new Set(), zeroContext); expect(result).toHaveLength(1); expect(result[0].progress).toBe(0); }); }); });