// Copyright (c) 2026 Gordon Bolton. MIT License. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import nock from 'nock'; // Mock the logger before importing the retriever vi.mock('../../../server/utils/logger', () => ({ logToFile: vi.fn() })); // Import OmbiRetriever after mocking const OmbiRetriever = require('../../../server/clients/OmbiRetriever'); const ArrRetriever = require('../../../server/clients/ArrRetriever'); describe('OmbiRetriever', () => { const baseUrl = 'http://localhost:5000'; const apiKey = 'test-api-key-12345'; const instanceConfig = { id: 'test-ombi-1', name: 'Test Ombi Instance', url: baseUrl, apiKey: apiKey }; beforeEach(() => { nock.cleanAll(); vi.useFakeTimers(); }); afterEach(() => { nock.cleanAll(); vi.useRealTimers(); }); describe('constructor', () => { it('should extend ArrRetriever base class', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever).toBeInstanceOf(ArrRetriever); }); it('should initialize with correct properties', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever.id).toBe('test-ombi-1'); expect(retriever.name).toBe('Test Ombi Instance'); expect(retriever.url).toBe(baseUrl); expect(retriever.apiKey).toBe(apiKey); expect(retriever.baseUrl).toBe(baseUrl); }); it('should initialize cache with empty arrays and maps', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever.cache.movieRequests).toEqual([]); expect(retriever.cache.tvRequests).toEqual([]); expect(retriever.cache.movieMap).toBeInstanceOf(Map); expect(retriever.cache.tvMap).toBeInstanceOf(Map); }); it('should set cache TTL to 5 minutes', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever.cache.ttl).toBe(5 * 60 * 1000); // 5 minutes in ms }); }); describe('getRetrieverType', () => { it('should return "ombi"', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever.getRetrieverType()).toBe('ombi'); }); }); describe('getInstanceId', () => { it('should return configured instance ID', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever.getInstanceId()).toBe('test-ombi-1'); }); }); describe('getTags', () => { it('should return empty array', async () => { const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.getTags(); expect(result).toEqual([]); }); }); describe('getQueue', () => { it('should return combined movie and TV requests', async () => { const mockMovies = [ { id: 1, title: 'Movie 1', theMovieDbId: '12345' }, { id: 2, title: 'Movie 2', theMovieDbId: '67890' } ]; const mockTvShows = [ { id: 3, title: 'Show 1', theTvDbId: '11111' } ]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.getQueue(); expect(result.records).toHaveLength(3); expect(result.records[0].title).toBe('Movie 1'); expect(result.records[2].title).toBe('Show 1'); }); }); describe('getHistory', () => { it('should return empty records array', async () => { const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.getHistory(); expect(result).toEqual({ records: [] }); }); it('should return empty records even with options', async () => { const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.getHistory({ pageSize: 10, sortKey: 'date' }); expect(result).toEqual({ records: [] }); }); }); describe('testConnection', () => { it('should return true for successful connection', async () => { nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, []); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.testConnection(); expect(result).toBe(true); }); it('should return false for failed connection', async () => { nock(baseUrl) .get('/api/v1/Request/movie') .reply(401, { error: 'Unauthorized' }); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.testConnection(); expect(result).toBe(false); }); }); describe('isCacheExpired', () => { it('should return true when cache is fresh (never fetched)', () => { const retriever = new OmbiRetriever(instanceConfig); expect(retriever.isCacheExpired()).toBe(true); }); it('should return false when cache is within TTL', async () => { const mockMovies = [{ id: 1, title: 'Movie 1' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); expect(retriever.isCacheExpired()).toBe(false); }); it('should return true when cache is beyond TTL', async () => { const mockMovies = [{ id: 1, title: 'Movie 1' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); // Advance time by 6 minutes (beyond 5-minute TTL) vi.advanceTimersByTime(6 * 60 * 1000); expect(retriever.isCacheExpired()).toBe(true); }); }); describe('refreshCache', () => { it('should not refresh if cache is not expired', async () => { const mockMovies = [{ id: 1, title: 'Movie 1', theMovieDbId: '12345' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); // First refresh await retriever.refreshCache(); expect(retriever.cache.movieRequests).toHaveLength(1); // Reset nock to verify no new calls are made nock.cleanAll(); // Second refresh should not make API calls await retriever.refreshCache(); expect(retriever.cache.movieRequests).toHaveLength(1); }); it('should refresh when cache is expired', async () => { const mockMovies1 = [{ id: 1, title: 'Movie 1', theMovieDbId: '12345' }]; const mockMovies2 = [{ id: 1, title: 'Movie 1' }, { id: 2, title: 'Movie 2', theMovieDbId: '67890' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies1); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); // First refresh await retriever.refreshCache(); expect(retriever.cache.movieRequests).toHaveLength(1); // Advance time beyond TTL vi.advanceTimersByTime(6 * 60 * 1000); // Set up new mocks for second refresh nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies2); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); // Second refresh should make API calls await retriever.refreshCache(); expect(retriever.cache.movieRequests).toHaveLength(2); }); it('should build movie map with TMDB and IMDB IDs', async () => { const mockMovies = [ { id: 1, title: 'Movie 1', theMovieDbId: '12345', imdbId: 'tt12345' }, { id: 2, title: 'Movie 2', theMovieDbId: '67890' } ]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); expect(retriever.cache.movieMap.get('12345')).toEqual(mockMovies[0]); expect(retriever.cache.movieMap.get('tt12345')).toEqual(mockMovies[0]); expect(retriever.cache.movieMap.get('67890')).toEqual(mockMovies[1]); }); it('should build TV map with TVDB and TMDB IDs', async () => { const mockMovies = []; const mockTvShows = [ { id: 1, title: 'Show 1', theTvDbId: '11111', theMovieDbId: '22222' }, { id: 2, title: 'Show 2', theTvDbId: '33333' } ]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); expect(retriever.cache.tvMap.get('11111')).toEqual(mockTvShows[0]); expect(retriever.cache.tvMap.get('22222')).toEqual(mockTvShows[0]); expect(retriever.cache.tvMap.get('33333')).toEqual(mockTvShows[1]); }); it('should handle API errors gracefully', async () => { nock(baseUrl) .get('/api/v1/Request/movie') .reply(500, { error: 'Server Error' }); nock(baseUrl) .get('/api/v1/Request/tv') .reply(500, { error: 'Server Error' }); const retriever = new OmbiRetriever(instanceConfig); // Should not throw error await expect(retriever.refreshCache()).resolves.not.toThrow(); // Cache should remain empty but not crash expect(retriever.cache.movieRequests).toEqual([]); expect(retriever.cache.tvRequests).toEqual([]); }); }); describe('getMovieRequests', () => { it('should return cached movie requests on cache hit', async () => { const mockMovies = [{ id: 1, title: 'Movie 1', theMovieDbId: '12345' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); // Reset nock to ensure no new API calls nock.cleanAll(); const result = await retriever.getMovieRequests(); expect(result).toEqual(mockMovies); }); it('should fetch and return movie requests on cache miss', async () => { const mockMovies = [{ id: 1, title: 'Movie 1', theMovieDbId: '12345' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.getMovieRequests(); expect(result).toEqual(mockMovies); }); }); describe('getTvRequests', () => { it('should return cached TV requests on cache hit', async () => { const mockMovies = []; const mockTvShows = [{ id: 1, title: 'Show 1', theTvDbId: '11111' }]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); // Reset nock to ensure no new API calls nock.cleanAll(); const result = await retriever.getTvRequests(); expect(result).toEqual(mockTvShows); }); it('should fetch and return TV requests on cache miss', async () => { const mockMovies = []; const mockTvShows = [{ id: 1, title: 'Show 1', theTvDbId: '11111' }]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.getTvRequests(); expect(result).toEqual(mockTvShows); }); }); describe('findMovieRequest', () => { it('should find movie by TMDB ID from cache', async () => { const mockMovies = [ { id: 1, title: 'Movie 1', theMovieDbId: '12345', imdbId: 'tt12345' } ]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.findMovieRequest('12345'); expect(result).toEqual(mockMovies[0]); }); it('should find movie by IMDB ID when TMDB ID not found', async () => { const mockMovies = [ { id: 1, title: 'Movie 1', theMovieDbId: '12345', imdbId: 'tt12345' } ]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.findMovieRequest('99999', 'tt12345'); expect(result).toEqual(mockMovies[0]); }); it('should return null when movie not found', async () => { const mockMovies = [{ id: 1, title: 'Movie 1', theMovieDbId: '12345' }]; const mockTvShows = []; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.findMovieRequest('99999'); expect(result).toBeNull(); }); }); describe('findTvRequest', () => { it('should find TV show by TVDB ID from cache', async () => { const mockMovies = []; const mockTvShows = [ { id: 1, title: 'Show 1', theTvDbId: '11111', theMovieDbId: '22222' } ]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.findTvRequest('11111'); expect(result).toEqual(mockTvShows[0]); }); it('should find TV show by TMDB ID when TVDB ID not found', async () => { const mockMovies = []; const mockTvShows = [ { id: 1, title: 'Show 1', theTvDbId: '11111', theMovieDbId: '22222' } ]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.findTvRequest('99999', '22222'); expect(result).toEqual(mockTvShows[0]); }); it('should return null when TV show not found', async () => { const mockMovies = []; const mockTvShows = [{ id: 1, title: 'Show 1', theTvDbId: '11111' }]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.findTvRequest('99999'); expect(result).toBeNull(); }); }); describe('searchMovie', () => { it('should search by TMDB ID first', async () => { const mockSearchResult = { id: 12345, title: 'Searched Movie', theMovieDbId: '12345' }; nock(baseUrl) .get('/api/v1/Search/movie/12345') .reply(200, mockSearchResult); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.searchMovie('12345'); expect(result).toEqual(mockSearchResult); }); it('should fall back to IMDB ID when TMDB search fails', async () => { const mockSearchResult = { id: 12345, title: 'Searched Movie', imdbId: 'tt12345' }; nock(baseUrl) .get('/api/v1/Search/movie/12345') .reply(404, { error: 'Not Found' }); nock(baseUrl) .get('/api/v1/Search/movie/imdb/tt12345') .reply(200, mockSearchResult); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.searchMovie('12345', 'tt12345'); expect(result).toEqual(mockSearchResult); }); it('should return null when both searches fail', async () => { nock(baseUrl) .get('/api/v1/Search/movie/12345') .reply(404, { error: 'Not Found' }); nock(baseUrl) .get('/api/v1/Search/movie/imdb/tt12345') .reply(404, { error: 'Not Found' }); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.searchMovie('12345', 'tt12345'); expect(result).toBeNull(); }); }); describe('searchTv', () => { it('should search by TVDB ID first', async () => { const mockSearchResult = { id: 11111, title: 'Searched Show', theTvDbId: '11111' }; nock(baseUrl) .get('/api/v1/Search/tv/11111') .reply(200, mockSearchResult); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.searchTv('11111'); expect(result).toEqual(mockSearchResult); }); it('should fall back to TMDB ID when TVDB search fails', async () => { const mockSearchResult = { id: 11111, title: 'Searched Show', theMovieDbId: '22222' }; nock(baseUrl) .get('/api/v1/Search/tv/11111') .reply(404, { error: 'Not Found' }); nock(baseUrl) .get('/api/v1/Search/tv/tmdb/22222') .reply(200, mockSearchResult); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.searchTv('11111', '22222'); expect(result).toEqual(mockSearchResult); }); it('should return null when both searches fail', async () => { nock(baseUrl) .get('/api/v1/Search/tv/11111') .reply(404, { error: 'Not Found' }); nock(baseUrl) .get('/api/v1/Search/tv/tmdb/22222') .reply(404, { error: 'Not Found' }); const retriever = new OmbiRetriever(instanceConfig); const result = await retriever.searchTv('11111', '22222'); expect(result).toBeNull(); }); }); describe('getCacheStats', () => { it('should return cache statistics', async () => { const mockMovies = [ { id: 1, title: 'Movie 1', theMovieDbId: '12345' }, { id: 2, title: 'Movie 2', theMovieDbId: '67890' } ]; const mockTvShows = [{ id: 3, title: 'Show 1', theTvDbId: '11111' }]; nock(baseUrl) .get('/api/v1/Request/movie') .reply(200, mockMovies); nock(baseUrl) .get('/api/v1/Request/tv') .reply(200, mockTvShows); const retriever = new OmbiRetriever(instanceConfig); await retriever.refreshCache(); const stats = retriever.getCacheStats(); expect(stats.movieRequests).toBe(2); expect(stats.tvRequests).toBe(1); expect(stats.movieMapSize).toBe(2); expect(stats.tvMapSize).toBe(1); expect(stats.lastFetch).toBeGreaterThan(0); expect(stats.age).toBeGreaterThanOrEqual(0); }); }); });