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
This commit is contained in:
2026-05-21 18:43:09 +01:00
parent ed4237debb
commit 9621aec453
7 changed files with 1729 additions and 1 deletions
+1
View File
@@ -629,6 +629,7 @@ module.exports = {
buildSeriesMapFromRecords, buildSeriesMapFromRecords,
buildMoviesMapFromRecords, buildMoviesMapFromRecords,
getSlotStatusAndSpeed, getSlotStatusAndSpeed,
addOmbiMatching,
matchSabSlots, matchSabSlots,
matchSabHistory, matchSabHistory,
matchTorrents matchTorrents
+185
View File
@@ -0,0 +1,185 @@
// Copyright (c) 2026 Gordon Bolton. MIT License.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import nock from 'nock';
// Mock the logger and config before importing the registry
vi.mock('../../server/utils/logger', () => ({
logToFile: vi.fn()
}));
// Mock config to return test data
const mockOmbiInstances = [
{ id: 'ombi-test', name: 'Test Ombi', url: 'http://localhost:5000', apiKey: 'test-key' }
];
const mockSonarrInstances = [
{ id: 'sonarr-test', name: 'Test Sonarr', url: 'http://localhost:8989', apiKey: 'sonarr-key' }
];
const mockRadarrInstances = [
{ id: 'radarr-test', name: 'Test Radarr', url: 'http://localhost:7878', apiKey: 'radarr-key' }
];
vi.mock('../../server/utils/config', () => ({
getSonarrInstances: vi.fn(() => mockSonarrInstances),
getRadarrInstances: vi.fn(() => mockRadarrInstances),
getOmbiInstances: vi.fn(() => mockOmbiInstances)
}));
// Import the registry after mocking
const arrRetrieverRegistry = require('../../server/utils/arrRetrievers');
const OmbiRetriever = require('../../server/clients/OmbiRetriever');
const ArrRetriever = require('../../server/clients/ArrRetriever');
describe('arrRetrieverRegistry', () => {
beforeEach(() => {
// Reset the registry state before each test
arrRetrieverRegistry.retrievers.clear();
arrRetrieverRegistry.initialized = false;
nock.cleanAll();
});
afterEach(() => {
nock.cleanAll();
});
describe('initialize', () => {
it('should initialize without errors', async () => {
await expect(arrRetrieverRegistry.initialize()).resolves.not.toThrow();
});
it('should not reinitialize if already initialized', async () => {
await arrRetrieverRegistry.initialize();
const firstRetrieverCount = arrRetrieverRegistry.getOmbiRetrievers().length;
await arrRetrieverRegistry.initialize();
const secondRetrieverCount = arrRetrieverRegistry.getOmbiRetrievers().length;
expect(secondRetrieverCount).toBe(firstRetrieverCount);
});
});
describe('getOmbiRetrievers', () => {
it('should return Ombi retrievers only', async () => {
await arrRetrieverRegistry.initialize();
const ombiRetrievers = arrRetrieverRegistry.getOmbiRetrievers();
expect(ombiRetrievers.length).toBeGreaterThanOrEqual(0);
ombiRetrievers.forEach(retriever => {
expect(retriever.getRetrieverType()).toBe('ombi');
});
});
});
describe('getOmbiRequests', () => {
it('should return movie and TV request arrays', async () => {
await arrRetrieverRegistry.initialize();
const result = await arrRetrieverRegistry.getOmbiRequests();
expect(result).toHaveProperty('movie');
expect(result).toHaveProperty('tv');
expect(Array.isArray(result.movie)).toBe(true);
expect(Array.isArray(result.tv)).toBe(true);
});
it('should handle errors gracefully', async () => {
nock('http://localhost:5000')
.get('/api/v1/Request/movie')
.reply(500, { error: 'Server Error' });
nock('http://localhost:5000')
.get('/api/v1/Request/tv')
.reply(500, { error: 'Server Error' });
await arrRetrieverRegistry.initialize();
const result = await arrRetrieverRegistry.getOmbiRequests();
expect(result).toEqual({ movie: [], tv: [] });
});
});
describe('getOmbiRequestsByType', () => {
it('should return grouped requests by type', async () => {
await arrRetrieverRegistry.initialize();
const result = await arrRetrieverRegistry.getOmbiRequestsByType();
expect(result).toHaveProperty('movie');
expect(result).toHaveProperty('tv');
expect(Array.isArray(result.movie)).toBe(true);
expect(Array.isArray(result.tv)).toBe(true);
});
});
describe('findOmbiRequest', () => {
it('should return null for unknown type', async () => {
await arrRetrieverRegistry.initialize();
const result = await arrRetrieverRegistry.findOmbiRequest('unknown', { tmdbId: '12345' });
expect(result).toBeNull();
});
});
describe('getAllRetrievers', () => {
it('should return all retrievers', async () => {
await arrRetrieverRegistry.initialize();
const allRetrievers = arrRetrieverRegistry.getAllRetrievers();
expect(Array.isArray(allRetrievers)).toBe(true);
});
});
describe('getRetriever', () => {
it('should return null for non-existent instance', async () => {
await arrRetrieverRegistry.initialize();
const retriever = arrRetrieverRegistry.getRetriever('non-existent');
expect(retriever).toBeNull();
});
});
describe('getRetrieversByType', () => {
it('should filter retrievers by type', async () => {
await arrRetrieverRegistry.initialize();
const sonarrRetrievers = arrRetrieverRegistry.getRetrieversByType('sonarr');
const radarrRetrievers = arrRetrieverRegistry.getRetrieversByType('radarr');
const ombiRetrievers = arrRetrieverRegistry.getRetrieversByType('ombi');
sonarrRetrievers.forEach(r => expect(r.getRetrieverType()).toBe('sonarr'));
radarrRetrievers.forEach(r => expect(r.getRetrieverType()).toBe('radarr'));
ombiRetrievers.forEach(r => expect(r.getRetrieverType()).toBe('ombi'));
});
});
describe('matching helper functions', () => {
it('should expose matchDownload function', () => {
expect(arrRetrieverRegistry.matchDownload).toBeDefined();
expect(typeof arrRetrieverRegistry.matchDownload).toBe('function');
});
it('should expose matchDownloadToArr alias', () => {
expect(arrRetrieverRegistry.matchDownloadToArr).toBeDefined();
expect(typeof arrRetrieverRegistry.matchDownloadToArr).toBe('function');
});
it('should expose aggregateMatch alias', () => {
expect(arrRetrieverRegistry.aggregateMatch).toBeDefined();
expect(typeof arrRetrieverRegistry.aggregateMatch).toBe('function');
});
it('should expose matchingHelper alias', () => {
expect(arrRetrieverRegistry.matchingHelper).toBeDefined();
expect(typeof arrRetrieverRegistry.matchingHelper).toBe('function');
});
it('should expose compareDownloadAndArr alias', () => {
expect(arrRetrieverRegistry.compareDownloadAndArr).toBeDefined();
expect(typeof arrRetrieverRegistry.compareDownloadAndArr).toBe('function');
});
});
});
+339
View File
@@ -0,0 +1,339 @@
// 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 client
vi.mock('../../../server/utils/logger', () => ({
logToFile: vi.fn()
}));
// Import OmbiClient after mocking
const OmbiClient = require('../../../server/clients/OmbiClient');
describe('OmbiClient', () => {
const baseUrl = 'http://localhost:5000';
const apiKey = 'test-api-key-12345';
beforeEach(() => {
// Clean up nock after each test
nock.cleanAll();
});
afterEach(() => {
nock.cleanAll();
});
describe('constructor', () => {
it('should initialize with URL and API key', () => {
const client = new OmbiClient(baseUrl, apiKey);
expect(client.url).toBe(baseUrl);
expect(client.apiKey).toBe(apiKey);
});
it('should remove trailing slash from URL', () => {
const client = new OmbiClient('http://localhost:5000/', apiKey);
expect(client.url).toBe('http://localhost:5000');
});
it('should set up axios with API key header', () => {
const client = new OmbiClient(baseUrl, apiKey);
expect(client.axios.defaults.headers['ApiKey']).toBe(apiKey);
});
it('should set up axios with 10 second timeout', () => {
const client = new OmbiClient(baseUrl, apiKey);
expect(client.axios.defaults.timeout).toBe(10000);
});
});
describe('getMovieRequests', () => {
it('should return movie requests on successful API call', async () => {
const mockMovies = [
{ id: 1, title: 'Test Movie 1', theMovieDbId: '12345' },
{ id: 2, title: 'Test Movie 2', theMovieDbId: '67890' }
];
nock(baseUrl)
.get('/api/v1/Request/movie')
.matchHeader('ApiKey', apiKey)
.reply(200, mockMovies);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.getMovieRequests();
expect(result).toEqual(mockMovies);
});
it('should return empty array on API error', async () => {
nock(baseUrl)
.get('/api/v1/Request/movie')
.matchHeader('ApiKey', apiKey)
.reply(500, { error: 'Internal Server Error' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.getMovieRequests();
expect(result).toEqual([]);
});
it('should return empty array when response data is null', async () => {
nock(baseUrl)
.get('/api/v1/Request/movie')
.matchHeader('ApiKey', apiKey)
.reply(200, null);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.getMovieRequests();
expect(result).toEqual([]);
});
});
describe('getTvRequests', () => {
it('should return TV requests on successful API call', async () => {
const mockTvShows = [
{ id: 1, title: 'Test Show 1', theTvDbId: '12345' },
{ id: 2, title: 'Test Show 2', theTvDbId: '67890' }
];
nock(baseUrl)
.get('/api/v1/Request/tv')
.matchHeader('ApiKey', apiKey)
.reply(200, mockTvShows);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.getTvRequests();
expect(result).toEqual(mockTvShows);
});
it('should return empty array on API error', async () => {
nock(baseUrl)
.get('/api/v1/Request/tv')
.matchHeader('ApiKey', apiKey)
.reply(500, { error: 'Internal Server Error' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.getTvRequests();
expect(result).toEqual([]);
});
it('should return empty array when response data is null', async () => {
nock(baseUrl)
.get('/api/v1/Request/tv')
.matchHeader('ApiKey', apiKey)
.reply(200, null);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.getTvRequests();
expect(result).toEqual([]);
});
});
describe('searchMovieByTmdbId', () => {
it('should return movie data for valid TMDB ID', async () => {
const mockMovie = {
id: 12345,
title: 'Test Movie',
theMovieDbId: '12345'
};
nock(baseUrl)
.get('/api/v1/Search/movie/12345')
.matchHeader('ApiKey', apiKey)
.reply(200, mockMovie);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByTmdbId('12345');
expect(result).toEqual(mockMovie);
});
it('should return null for null TMDB ID', async () => {
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByTmdbId(null);
expect(result).toBeNull();
});
it('should return null for undefined TMDB ID', async () => {
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByTmdbId(undefined);
expect(result).toBeNull();
});
it('should return null on API error', async () => {
nock(baseUrl)
.get('/api/v1/Search/movie/12345')
.matchHeader('ApiKey', apiKey)
.reply(404, { error: 'Not Found' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByTmdbId('12345');
expect(result).toBeNull();
});
});
describe('searchMovieByImdbId', () => {
it('should return movie data for valid IMDB ID', async () => {
const mockMovie = {
id: 12345,
title: 'Test Movie',
imdbId: 'tt1234567'
};
nock(baseUrl)
.get('/api/v1/Search/movie/imdb/tt1234567')
.matchHeader('ApiKey', apiKey)
.reply(200, mockMovie);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByImdbId('tt1234567');
expect(result).toEqual(mockMovie);
});
it('should return null for null IMDB ID', async () => {
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByImdbId(null);
expect(result).toBeNull();
});
it('should return null on API error', async () => {
nock(baseUrl)
.get('/api/v1/Search/movie/imdb/tt1234567')
.matchHeader('ApiKey', apiKey)
.reply(404, { error: 'Not Found' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchMovieByImdbId('tt1234567');
expect(result).toBeNull();
});
});
describe('searchTvByTvdbId', () => {
it('should return TV show data for valid TVDB ID', async () => {
const mockShow = {
id: 12345,
title: 'Test Show',
theTvDbId: '12345'
};
nock(baseUrl)
.get('/api/v1/Search/tv/12345')
.matchHeader('ApiKey', apiKey)
.reply(200, mockShow);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchTvByTvdbId('12345');
expect(result).toEqual(mockShow);
});
it('should return null for null TVDB ID', async () => {
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchTvByTvdbId(null);
expect(result).toBeNull();
});
it('should return null on API error', async () => {
nock(baseUrl)
.get('/api/v1/Search/tv/12345')
.matchHeader('ApiKey', apiKey)
.reply(404, { error: 'Not Found' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchTvByTvdbId('12345');
expect(result).toBeNull();
});
});
describe('searchTvByTmdbId', () => {
it('should return TV show data for valid TMDB ID', async () => {
const mockShow = {
id: 12345,
title: 'Test Show',
theMovieDbId: '67890'
};
nock(baseUrl)
.get('/api/v1/Search/tv/tmdb/67890')
.matchHeader('ApiKey', apiKey)
.reply(200, mockShow);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchTvByTmdbId('67890');
expect(result).toEqual(mockShow);
});
it('should return null for null TMDB ID', async () => {
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchTvByTmdbId(null);
expect(result).toBeNull();
});
it('should return null on API error', async () => {
nock(baseUrl)
.get('/api/v1/Search/tv/tmdb/67890')
.matchHeader('ApiKey', apiKey)
.reply(404, { error: 'Not Found' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.searchTvByTmdbId('67890');
expect(result).toBeNull();
});
});
describe('testConnection', () => {
it('should return true for successful connection', async () => {
nock(baseUrl)
.get('/api/v1/Request/movie')
.matchHeader('ApiKey', apiKey)
.reply(200, []);
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.testConnection();
expect(result).toBe(true);
});
it('should return false for failed connection', async () => {
nock(baseUrl)
.get('/api/v1/Request/movie')
.matchHeader('ApiKey', apiKey)
.reply(401, { error: 'Unauthorized' });
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.testConnection();
expect(result).toBe(false);
});
it('should return false on network error', async () => {
nock(baseUrl)
.get('/api/v1/Request/movie')
.matchHeader('ApiKey', apiKey)
.replyWithError('Network error');
const client = new OmbiClient(baseUrl, apiKey);
const result = await client.testConnection();
expect(result).toBe(false);
});
});
});
+678
View File
@@ -0,0 +1,678 @@
// 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);
});
});
});
+84 -1
View File
@@ -7,7 +7,7 @@
* because misconfigured instances silently return no data rather than crashing. * because misconfigured instances silently return no data rather than crashing.
*/ */
import { parseInstances, getSonarrInstances, getRadarrInstances } from '../../server/utils/config.js'; import { parseInstances, getSonarrInstances, getRadarrInstances, getOmbiInstances } from '../../server/utils/config.js';
describe('parseInstances', () => { describe('parseInstances', () => {
describe('JSON array format', () => { describe('JSON array format', () => {
@@ -106,4 +106,87 @@ describe('parseInstances', () => {
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
}); });
describe('Ombi configuration', () => {
it('getOmbiInstances parses OMBI_INSTANCES JSON array', () => {
process.env.OMBI_INSTANCES = JSON.stringify([{ name: 'ombi-main', url: 'https://ombi.local', apiKey: 'ombi-key-123' }]);
const result = getOmbiInstances();
expect(result).toHaveLength(1);
expect(result[0].name).toBe('ombi-main');
expect(result[0].url).toBe('https://ombi.local');
expect(result[0].apiKey).toBe('ombi-key-123');
expect(result[0].id).toBe('ombi-main');
delete process.env.OMBI_INSTANCES;
});
it('getOmbiInstances parses multiple Ombi instances', () => {
process.env.OMBI_INSTANCES = JSON.stringify([
{ name: 'ombi-primary', url: 'https://ombi1.local', apiKey: 'key1' },
{ name: 'ombi-backup', url: 'https://ombi2.local', apiKey: 'key2' }
]);
const result = getOmbiInstances();
expect(result).toHaveLength(2);
expect(result[0].name).toBe('ombi-primary');
expect(result[1].name).toBe('ombi-backup');
delete process.env.OMBI_INSTANCES;
});
it('getOmbiInstances falls back to legacy OMBI_URL and OMBI_API_KEY', () => {
delete process.env.OMBI_INSTANCES;
process.env.OMBI_URL = 'https://legacy-ombi.local';
process.env.OMBI_API_KEY = 'legacy-ombi-key';
const result = getOmbiInstances();
expect(result).toHaveLength(1);
expect(result[0].id).toBe('default');
expect(result[0].name).toBe('Default');
expect(result[0].url).toBe('https://legacy-ombi.local');
expect(result[0].apiKey).toBe('legacy-ombi-key');
delete process.env.OMBI_URL;
delete process.env.OMBI_API_KEY;
});
it('getOmbiInstances returns empty array when not configured', () => {
delete process.env.OMBI_INSTANCES;
delete process.env.OMBI_URL;
delete process.env.OMBI_API_KEY;
const result = getOmbiInstances();
expect(result).toEqual([]);
});
it('getOmbiInstances handles multi-line JSON', () => {
const json = `[
{
"name": "ombi-test",
"url": "https://ombi.test",
"apiKey": "test-key"
}
]`;
process.env.OMBI_INSTANCES = json;
const result = getOmbiInstances();
expect(result).toHaveLength(1);
expect(result[0].name).toBe('ombi-test');
delete process.env.OMBI_INSTANCES;
});
it('getOmbiInstances handles invalid JSON by falling back to legacy', () => {
process.env.OMBI_INSTANCES = 'not-valid-json';
process.env.OMBI_URL = 'https://fallback-ombi.local';
process.env.OMBI_API_KEY = 'fallback-key';
const result = getOmbiInstances();
expect(result).toHaveLength(1);
expect(result[0].url).toBe('https://fallback-ombi.local');
delete process.env.OMBI_INSTANCES;
delete process.env.OMBI_URL;
delete process.env.OMBI_API_KEY;
});
it('parseInstances validates Ombi instance URLs', () => {
process.env.OMBI_INSTANCES = JSON.stringify([{ name: 'bad-url', url: 'not-a-valid-url', apiKey: 'key' }]);
const result = getOmbiInstances();
// Should still parse but with validation warning
expect(result).toHaveLength(1);
expect(result[0].url).toBe('not-a-valid-url');
delete process.env.OMBI_INSTANCES;
});
});
}); });
@@ -752,4 +752,78 @@ describe('DownloadAssembler', () => {
]); ]);
}); });
}); });
describe('getOmbiLink', () => {
it('returns correct URL for valid requestId, type, and baseUrl', () => {
const result = DownloadAssembler.getOmbiLink(123, 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/request/movie/123');
});
it('returns correct URL for TV type', () => {
const result = DownloadAssembler.getOmbiLink(456, 'tv', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/request/tv/456');
});
it('returns null when requestId is missing', () => {
const result = DownloadAssembler.getOmbiLink(null, 'movie', 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when type is missing', () => {
const result = DownloadAssembler.getOmbiLink(123, null, 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when baseUrl is missing', () => {
const result = DownloadAssembler.getOmbiLink(123, 'movie', null);
expect(result).toBeNull();
});
it('returns null when all parameters are missing', () => {
const result = DownloadAssembler.getOmbiLink(null, null, null);
expect(result).toBeNull();
});
it('handles string requestId', () => {
const result = DownloadAssembler.getOmbiLink('abc-123', 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/request/movie/abc-123');
});
});
describe('getOmbiSearchLink', () => {
it('returns correct URL for series type', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, 'series', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/tv/search/789');
});
it('returns correct URL for movie type', () => {
const result = DownloadAssembler.getOmbiSearchLink(101, 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/movie/search/101');
});
it('returns null when searchId is missing', () => {
const result = DownloadAssembler.getOmbiSearchLink(null, 'series', 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when type is missing', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, null, 'http://localhost:5000');
expect(result).toBeNull();
});
it('returns null when baseUrl is missing', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, 'series', null);
expect(result).toBeNull();
});
it('returns null for invalid type', () => {
const result = DownloadAssembler.getOmbiSearchLink(789, 'invalid', 'http://localhost:5000');
expect(result).toBeNull();
});
it('handles string searchId', () => {
const result = DownloadAssembler.getOmbiSearchLink('search-123', 'movie', 'http://localhost:5000');
expect(result).toBe('http://localhost:5000/#/movie/search/search-123');
});
});
}); });
+368
View File
@@ -0,0 +1,368 @@
// 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');
});
});
});