Files
sofarr/tests/unit/historyFetcher.test.js

178 lines
5.5 KiB
JavaScript

/**
* Unit tests for server/utils/historyFetcher.js
*
* Covers:
* - classifySonarrEvent / classifyRadarrEvent event classification
* - fetchSonarrHistory / fetchRadarrHistory: successful fetch, cache hit, per-instance errors
* - invalidateHistoryCache
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import nock from 'nock';
// Env must be set before importing modules that read it at load time
process.env.SONARR_INSTANCES = JSON.stringify([
{ id: 'sonarr-1', name: 'Main Sonarr', url: 'https://sonarr.test', apiKey: 'sonarr-key' }
]);
process.env.RADARR_INSTANCES = JSON.stringify([
{ id: 'radarr-1', name: 'Main Radarr', url: 'https://radarr.test', apiKey: 'radarr-key' }
]);
const { classifySonarrEvent, classifyRadarrEvent, fetchSonarrHistory, fetchRadarrHistory, invalidateHistoryCache } =
await import('../../server/utils/historyFetcher.js');
const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
afterEach(() => {
nock.cleanAll();
invalidateHistoryCache();
});
describe('classifySonarrEvent', () => {
it('returns imported for downloadFolderImported', () => {
expect(classifySonarrEvent('downloadFolderImported')).toBe('imported');
});
it('returns imported for downloadImported', () => {
expect(classifySonarrEvent('downloadImported')).toBe('imported');
});
it('returns failed for downloadFailed', () => {
expect(classifySonarrEvent('downloadFailed')).toBe('failed');
});
it('returns failed for importFailed', () => {
expect(classifySonarrEvent('importFailed')).toBe('failed');
});
it('returns other for grabbed', () => {
expect(classifySonarrEvent('grabbed')).toBe('other');
});
it('returns other for unknown event', () => {
expect(classifySonarrEvent('someFutureEvent')).toBe('other');
});
});
describe('classifyRadarrEvent', () => {
it('returns imported for downloadFolderImported', () => {
expect(classifyRadarrEvent('downloadFolderImported')).toBe('imported');
});
it('returns failed for downloadFailed', () => {
expect(classifyRadarrEvent('downloadFailed')).toBe('failed');
});
it('returns other for grabbed', () => {
expect(classifyRadarrEvent('grabbed')).toBe('other');
});
});
describe('fetchSonarrHistory', () => {
const mockRecords = [
{
id: 1,
eventType: 'downloadFolderImported',
sourceTitle: 'Show.S01E01',
date: new Date().toISOString(),
series: { id: 10, title: 'My Show', titleSlug: 'my-show', tags: [] },
seriesId: 10
}
];
it('fetches records and tags them with _instanceUrl and _instanceName', async () => {
nock('https://sonarr.test')
.get('/api/v3/history')
.query(true)
.reply(200, { records: mockRecords });
const result = await fetchSonarrHistory(since);
expect(result).toHaveLength(1);
expect(result[0].series._instanceUrl).toBe('https://sonarr.test');
expect(result[0].series._instanceName).toBe('Main Sonarr');
expect(result[0]._instanceName).toBe('Main Sonarr');
});
it('returns cached data on second call without making a new HTTP request', async () => {
nock('https://sonarr.test')
.get('/api/v3/history')
.query(true)
.reply(200, { records: mockRecords });
const first = await fetchSonarrHistory(since);
// Second call — nock would throw if a second request was made
const second = await fetchSonarrHistory(since);
expect(second).toEqual(first);
});
it('returns empty array and does not throw when instance errors', async () => {
nock('https://sonarr.test')
.get('/api/v3/history')
.query(true)
.replyWithError('ECONNREFUSED');
const result = await fetchSonarrHistory(since);
expect(result).toEqual([]);
});
it('handles missing records key gracefully', async () => {
nock('https://sonarr.test')
.get('/api/v3/history')
.query(true)
.reply(200, {});
const result = await fetchSonarrHistory(since);
expect(result).toEqual([]);
});
});
describe('fetchRadarrHistory', () => {
const mockRecords = [
{
id: 2,
eventType: 'downloadFolderImported',
sourceTitle: 'My.Movie.2024',
date: new Date().toISOString(),
movie: { id: 20, title: 'My Movie', titleSlug: 'my-movie-2024', tags: [] },
movieId: 20
}
];
it('fetches records and tags them with _instanceUrl and _instanceName', async () => {
nock('https://radarr.test')
.get('/api/v3/history')
.query(true)
.reply(200, { records: mockRecords });
const result = await fetchRadarrHistory(since);
expect(result).toHaveLength(1);
expect(result[0].movie._instanceUrl).toBe('https://radarr.test');
expect(result[0].movie._instanceName).toBe('Main Radarr');
});
it('returns empty array on network error', async () => {
nock('https://radarr.test')
.get('/api/v3/history')
.query(true)
.replyWithError('timeout');
const result = await fetchRadarrHistory(since);
expect(result).toEqual([]);
});
});
describe('invalidateHistoryCache', () => {
it('forces a fresh fetch after invalidation', async () => {
nock('https://sonarr.test')
.get('/api/v3/history')
.query(true)
.reply(200, { records: [] });
await fetchSonarrHistory(since);
invalidateHistoryCache();
// Should make a second HTTP request — nock will satisfy it
nock('https://sonarr.test')
.get('/api/v3/history')
.query(true)
.reply(200, { records: [] });
const result = await fetchSonarrHistory(since);
expect(result).toEqual([]);
expect(nock.isDone()).toBe(true);
});
});