Files
sofarr/tests/unit/clients/OmbiRetriever.test.js
T
gronod 9621aec453 test: add comprehensive test suite for Ombi integration
- Add tests for Ombi configuration parsing (OMBI_INSTANCES JSON array, legacy fallback)
- Add tests for OmbiClient API methods (movie/TV requests, search by TMDB/IMDB/TVDB)
- Add tests for OmbiRetriever caching, queue, and search functionality
- Add tests for arrRetrieverRegistry initialization and retrieval methods
- Add tests for DownloadMatcher.addOmbiMatching integration
- Add tests for DownloadAssembler Ombi link generation utilities
- Export addOmbiMatching from DownloadMatcher module
2026-05-21 18:43:09 +01:00

679 lines
20 KiB
JavaScript

// 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);
});
});
});