// Copyright (c) 2026 Gordon Bolton. MIT License. /** * Tests for server/utils/config.js * * Verifies that instance config is parsed correctly from both the modern JSON * array format and the legacy single-instance env var format. This is critical * because misconfigured instances silently return no data rather than crashing. */ import { parseInstances, getSonarrInstances, getRadarrInstances } from '../../server/utils/config.js'; describe('parseInstances', () => { describe('JSON array format', () => { it('parses a valid single-instance JSON array', () => { const json = JSON.stringify([{ name: 'main', url: 'https://sonarr.local', apiKey: 'abc123' }]); const result = parseInstances(json, null, null); expect(result).toHaveLength(1); expect(result[0].url).toBe('https://sonarr.local'); expect(result[0].apiKey).toBe('abc123'); }); it('parses multiple instances', () => { const json = JSON.stringify([ { name: 'main', url: 'https://s1.local', apiKey: 'key1' }, { name: 'backup', url: 'https://s2.local', apiKey: 'key2' } ]); const result = parseInstances(json, null, null); expect(result).toHaveLength(2); expect(result[1].name).toBe('backup'); }); it('adds id from name when present', () => { const json = JSON.stringify([{ name: 'i3omb', url: 'https://s.local', apiKey: 'k' }]); const result = parseInstances(json, null, null); expect(result[0].id).toBe('i3omb'); }); it('generates fallback id when name is absent', () => { const json = JSON.stringify([{ url: 'https://s.local', apiKey: 'k' }]); const result = parseInstances(json, null, null); expect(result[0].id).toBe('instance-1'); }); it('handles multi-line JSON by stripping whitespace', () => { const json = `[ { "name": "main", "url": "https://sonarr.local", "apiKey": "abc" } ]`; const result = parseInstances(json, null, null); expect(result).toHaveLength(1); }); it('returns empty array for empty JSON array', () => { expect(parseInstances('[]', null, null)).toEqual([]); }); it('falls back to legacy format when JSON is malformed', () => { const result = parseInstances('not-json', 'https://legacy.local', 'legacyKey'); expect(result).toHaveLength(1); expect(result[0].url).toBe('https://legacy.local'); }); }); describe('legacy single-instance format', () => { it('returns single instance from legacy URL + key', () => { const result = parseInstances(null, 'https://sonarr.local', 'legacyapikey'); expect(result).toHaveLength(1); expect(result[0].id).toBe('default'); expect(result[0].name).toBe('Default'); expect(result[0].url).toBe('https://sonarr.local'); expect(result[0].apiKey).toBe('legacyapikey'); }); it('returns empty array for qBittorrent with no apiKey and no JSON (legacy requires key)', () => { // parseInstances requires legacyKey to be truthy for the legacy path; // qBittorrent uses JSON array format, not the legacy URL+key path. const result = parseInstances(null, 'https://qbt.local', null, 'admin', 'pass123'); expect(result).toEqual([]); }); it('returns empty array when both JSON and legacy URL are missing', () => { expect(parseInstances(null, null, null)).toEqual([]); }); it('returns empty array when URL is set but key is missing', () => { expect(parseInstances(null, 'https://sonarr.local', null)).toEqual([]); }); }); describe('env-based getters', () => { it('getSonarrInstances reads SONARR_INSTANCES from env', () => { process.env.SONARR_INSTANCES = JSON.stringify([{ name: 'test', url: 'https://s.local', apiKey: 'k' }]); const result = getSonarrInstances(); expect(result).toHaveLength(1); expect(result[0].name).toBe('test'); delete process.env.SONARR_INSTANCES; }); it('getRadarrInstances returns empty array when unconfigured', () => { delete process.env.RADARR_INSTANCES; delete process.env.RADARR_URL; const result = getRadarrInstances(); expect(result).toEqual([]); }); }); });