// Copyright (c) 2026 Gordon Bolton. MIT License. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import nock from 'nock'; // Mock dependencies vi.mock('../../../server/utils/logger', () => ({ logToFile: vi.fn() })); // Import after mocking const DownloadMatcher = require('../../../server/services/DownloadMatcher'); const OmbiRetriever = require('../../../server/clients/OmbiRetriever'); describe('DownloadMatcher', () => { const ombiBaseUrl = 'http://localhost:5000'; beforeEach(() => { nock.cleanAll(); vi.clearAllMocks(); }); afterEach(() => { nock.cleanAll(); }); describe('addOmbiMatching', () => { it('should return early when ombiRetriever is missing', async () => { const downloadObj = { type: 'series', title: 'Test Show' }; const series = { tvdbId: '12345', tmdbId: '67890' }; const context = { ombiRetriever: null, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBeUndefined(); expect(downloadObj.ombiRequestId).toBeUndefined(); }); it('should return early when ombiBaseUrl is missing', async () => { const downloadObj = { type: 'series', title: 'Test Show' }; const series = { tvdbId: '12345', tmdbId: '67890' }; const mockRetriever = { findTvRequest: vi.fn(), searchTv: vi.fn() }; const context = { ombiRetriever: mockRetriever, ombiBaseUrl: null }; await DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(mockRetriever.findTvRequest).not.toHaveBeenCalled(); expect(downloadObj.ombiLink).toBeUndefined(); }); it('should return early when seriesOrMovie is missing', async () => { const downloadObj = { type: 'series', title: 'Test Show' }; const mockRetriever = { findTvRequest: vi.fn(), searchTv: vi.fn() }; const context = { ombiRetriever: mockRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, null, context); expect(mockRetriever.findTvRequest).not.toHaveBeenCalled(); expect(downloadObj.ombiLink).toBeUndefined(); }); it('should add ombiLink and ombiRequestId for TV request found by TVDB ID', async () => { const mockMovieRequests = []; const mockTvRequests = [ { id: 101, title: 'Test Show', type: 'tv', theTvDbId: '12345' } ]; nock(ombiBaseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovieRequests); nock(ombiBaseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvRequests); const ombiRetriever = new OmbiRetriever({ id: 'test-ombi', name: 'Test Ombi', url: ombiBaseUrl, apiKey: 'test-key' }); const downloadObj = { type: 'series', title: 'Test Show' }; const series = { tvdbId: '12345', tmdbId: '67890' }; const context = { ombiRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/tv/101'); expect(downloadObj.ombiRequestId).toBe(101); expect(downloadObj.ombiTooltip).toBe('Request'); }); it('should add ombiLink and ombiRequestId for TV request found by TMDB ID fallback', async () => { const mockMovieRequests = []; const mockTvRequests = [ { id: 102, title: 'Test Show TMDB', type: 'tv', theMovieDbId: '67890' } ]; nock(ombiBaseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovieRequests); nock(ombiBaseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvRequests); const ombiRetriever = new OmbiRetriever({ id: 'test-ombi', name: 'Test Ombi', url: ombiBaseUrl, apiKey: 'test-key' }); const downloadObj = { type: 'series', title: 'Test Show TMDB' }; const series = { tvdbId: '99999', tmdbId: '67890' }; const context = { ombiRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/tv/102'); expect(downloadObj.ombiRequestId).toBe(102); expect(downloadObj.ombiTooltip).toBe('Request'); }); it('should add ombiLink and ombiRequestId for movie request found by TMDB ID', async () => { const mockMovieRequests = [ { id: 201, title: 'Test Movie', type: 'movie', theMovieDbId: '54321' } ]; const mockTvRequests = []; nock(ombiBaseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovieRequests); nock(ombiBaseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvRequests); const ombiRetriever = new OmbiRetriever({ id: 'test-ombi', name: 'Test Ombi', url: ombiBaseUrl, apiKey: 'test-key' }); const downloadObj = { type: 'movie', title: 'Test Movie' }; const movie = { tmdbId: '54321', imdbId: 'tt54321' }; const context = { ombiRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, movie, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/movie/201'); expect(downloadObj.ombiRequestId).toBe(201); expect(downloadObj.ombiTooltip).toBe('Request'); }); it('should add ombiLink and ombiRequestId for movie request found by IMDB ID fallback', async () => { const mockMovieRequests = [ { id: 202, title: 'Test Movie IMDB', type: 'movie', imdbId: 'tt98765' } ]; const mockTvRequests = []; nock(ombiBaseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovieRequests); nock(ombiBaseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvRequests); const ombiRetriever = new OmbiRetriever({ id: 'test-ombi', name: 'Test Ombi', url: ombiBaseUrl, apiKey: 'test-key' }); const downloadObj = { type: 'movie', title: 'Test Movie IMDB' }; const movie = { tmdbId: '99999', imdbId: 'tt98765' }; const context = { ombiRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, movie, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/request/movie/202'); expect(downloadObj.ombiRequestId).toBe(202); expect(downloadObj.ombiTooltip).toBe('Request'); }); it('should add search link and tooltip when no request found but search succeeds', async () => { const mockMovieRequests = []; const mockTvRequests = []; const mockSearchResult = { id: 12345, title: 'Test Show Search', theTvDbId: '11111' }; nock(ombiBaseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovieRequests); nock(ombiBaseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvRequests); nock(ombiBaseUrl) .get('/api/v1/Search/tv/11111') .reply(200, mockSearchResult); const ombiRetriever = new OmbiRetriever({ id: 'test-ombi', name: 'Test Ombi', url: ombiBaseUrl, apiKey: 'test-key' }); const downloadObj = { type: 'series', title: 'Test Show Search' }; const series = { tvdbId: '11111', tmdbId: '22222' }; const context = { ombiRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, series, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/tv/search/12345'); expect(downloadObj.ombiTooltip).toBe('Search'); expect(downloadObj.ombiRequestId).toBeUndefined(); }); it('should add movie search link for movie type', async () => { const mockMovieRequests = []; const mockTvRequests = []; const mockSearchResult = { id: 54321, title: 'Test Movie Search', theMovieDbId: '33333' }; nock(ombiBaseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovieRequests); nock(ombiBaseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvRequests); nock(ombiBaseUrl) .get('/api/v1/Search/movie/33333') .reply(200, mockSearchResult); const ombiRetriever = new OmbiRetriever({ id: 'test-ombi', name: 'Test Ombi', url: ombiBaseUrl, apiKey: 'test-key' }); const downloadObj = { type: 'movie', title: 'Test Movie Search' }; const movie = { tmdbId: '33333', imdbId: 'tt33333' }; const context = { ombiRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, movie, context); expect(downloadObj.ombiLink).toBe('http://localhost:5000/#/movie/search/54321'); expect(downloadObj.ombiTooltip).toBe('Search'); expect(downloadObj.ombiRequestId).toBeUndefined(); }); it('should handle errors gracefully without breaking download object', async () => { const mockRetriever = { findTvRequest: vi.fn().mockRejectedValue(new Error('Ombi API error')), searchTv: vi.fn().mockRejectedValue(new Error('Search error')) }; const downloadObj = { type: 'series', title: 'Test Show Error' }; const series = { tvdbId: '66666', tmdbId: '77777' }; const context = { ombiRetriever: mockRetriever, ombiBaseUrl }; // Should not throw error await expect(DownloadMatcher.addOmbiMatching(downloadObj, series, context)).resolves.not.toThrow(); // Download object should still have original data expect(downloadObj.title).toBe('Test Show Error'); expect(downloadObj.ombiLink).toBeUndefined(); expect(downloadObj.ombiRequestId).toBeUndefined(); }); it('should do nothing for unknown download type', async () => { const mockRetriever = { findTvRequest: vi.fn(), findMovieRequest: vi.fn() }; const downloadObj = { type: 'unknown', title: 'Unknown Type' }; const media = { id: 123 }; const context = { ombiRetriever: mockRetriever, ombiBaseUrl }; await DownloadMatcher.addOmbiMatching(downloadObj, media, context); expect(mockRetriever.findTvRequest).not.toHaveBeenCalled(); expect(mockRetriever.findMovieRequest).not.toHaveBeenCalled(); expect(downloadObj.ombiLink).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'); }); }); });