// 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'); }); }); }); describe('OmbiRetriever._hydrateRequest', () => { let retriever; beforeEach(() => { retriever = new OmbiRetriever({ id: 'ombi-test', name: 'Test Ombi', url: 'http://localhost:5000', apiKey: 'test-key' }); // Seed the userMap cache retriever.cache.userMap.set('user-1', { id: 'user-1', userName: 'testuser', alias: 'TestUser', userAlias: 'TestUser', normalizedUserName: 'testuser' }); retriever.cache.userMap.set('user-2', { id: 'user-2', userName: 'adminuser', alias: 'AdminUser', userAlias: 'AdminUser', normalizedUserName: 'adminuser' }); }); it('hydrates top-level requestedUserId', () => { const req = { id: 1, requestedUserId: 'user-1', requestedUser: {} }; const result = retriever._hydrateRequest(req); expect(result.requestedUser.userName).toBe('testuser'); expect(result.requestedUser.alias).toBe('TestUser'); }); it('hydrates childRequests requestedUserId (TV requests)', () => { const req = { id: 3, title: 'Test Show', requestedUserId: 'user-1', requestedUser: {}, childRequests: [ { id: 10, requestedUserId: 'user-2', requestedUser: {} } ] }; const result = retriever._hydrateRequest(req); expect(result.requestedUser.userName).toBe('testuser'); expect(result.childRequests[0].requestedUser.userName).toBe('adminuser'); expect(result.childRequests[0].requestedUser.alias).toBe('AdminUser'); }); it('promotes requestedDate from childRequests to top level', () => { const req = { id: 3, title: 'Test Show', childRequests: [ { id: 10, requestedDate: '2026-05-15T10:00:00.000Z' } ] }; const result = retriever._hydrateRequest(req); expect(result.requestedDate).toBe('2026-05-15T10:00:00.000Z'); expect(result.childRequests[0].requestedDate).toBe('2026-05-15T10:00:00.000Z'); }); it('does not overwrite existing top-level requestedDate', () => { const req = { id: 3, requestedDate: '2026-01-01T00:00:00.000Z', childRequests: [ { id: 10, requestedDate: '2026-05-15T10:00:00.000Z' } ] }; const result = retriever._hydrateRequest(req); expect(result.requestedDate).toBe('2026-01-01T00:00:00.000Z'); }); it('handles PascalCase RequestedDate from childRequests', () => { const req = { id: 3, childRequests: [ { id: 10, RequestedDate: '2026-06-01T12:00:00.000Z' } ] }; const result = retriever._hydrateRequest(req); expect(result.requestedDate).toBe('2026-06-01T12:00:00.000Z'); }); it('returns unmodified request when no hydration needed', () => { const req = { id: 1, title: 'Test Movie', requestedUser: { userName: 'existing', alias: 'Existing' } }; const result = retriever._hydrateRequest(req); expect(result).toEqual(req); }); it('handles null childRequests gracefully', () => { const req = { id: 3, childRequests: null }; const result = retriever._hydrateRequest(req); expect(result).toEqual(req); }); it('handles empty childRequests gracefully', () => { const req = { id: 3, childRequests: [] }; const result = retriever._hydrateRequest(req); expect(result).toEqual(req); }); it('skips child hydration when child already has valid requestedUser', () => { const req = { id: 3, childRequests: [ { id: 10, requestedUserId: 'user-1', requestedUser: { userName: 'already_set', alias: 'AlreadySet' } } ] }; const result = retriever._hydrateRequest(req); expect(result.childRequests[0].requestedUser.userName).toBe('already_set'); expect(result.childRequests[0].requestedUser.alias).toBe('AlreadySet'); }); });