test(history): add unit and integration tests for historyFetcher and /api/history/recent
This commit is contained in:
289
tests/integration/history.test.js
Normal file
289
tests/integration/history.test.js
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for GET /api/history/recent
|
||||||
|
*
|
||||||
|
* Uses supertest against createApp() with vi.mock to stub historyFetcher
|
||||||
|
* (avoids nock/ESM-CJS interop issues with axios) and nock for Emby auth.
|
||||||
|
* Covers:
|
||||||
|
* - 401 when unauthenticated
|
||||||
|
* - Empty history response when arr returns no records
|
||||||
|
* - Filters out records whose eventType is not imported/failed
|
||||||
|
* - Returns imported and failed records for tagged series/movies
|
||||||
|
* - ?days= param is respected (default 7, capped at 90)
|
||||||
|
* - failureMessage included for admins on failed records
|
||||||
|
*/
|
||||||
|
|
||||||
|
import request from 'supertest';
|
||||||
|
import nock from 'nock';
|
||||||
|
import { beforeEach, afterEach } from 'vitest';
|
||||||
|
import { createRequire } from 'module';
|
||||||
|
import { createApp } from '../../server/app.js';
|
||||||
|
|
||||||
|
// Use createRequire to get the same CJS singleton cache instance that
|
||||||
|
// server/utils/historyFetcher.js and server/routes/history.js use via
|
||||||
|
// require('./cache'). A plain ESM `import cache from '...'` resolves
|
||||||
|
// to a different module identity under vitest's ESM runtime.
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const cache = require('../../server/utils/cache.js');
|
||||||
|
|
||||||
|
const EMBY_BASE = 'https://emby.test';
|
||||||
|
|
||||||
|
process.env.EMBY_URL = EMBY_BASE;
|
||||||
|
process.env.SONARR_INSTANCES = JSON.stringify([
|
||||||
|
{ id: 'sonarr-1', name: 'Main Sonarr', url: 'https://sonarr.test', apiKey: 'sk' }
|
||||||
|
]);
|
||||||
|
process.env.RADARR_INSTANCES = JSON.stringify([
|
||||||
|
{ id: 'radarr-1', name: 'Main Radarr', url: 'https://radarr.test', apiKey: 'rk' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- Fixtures ---
|
||||||
|
const EMBY_AUTH = { AccessToken: 'tok', User: { Id: 'uid1', Name: 'alice' } };
|
||||||
|
const EMBY_USER = { Id: 'uid1', Name: 'alice', Policy: { IsAdministrator: false } };
|
||||||
|
const EMBY_ADMIN = { Id: 'uid2', Name: 'admin', Policy: { IsAdministrator: true } };
|
||||||
|
const EMBY_AUTH_ADMIN = { AccessToken: 'tok2', User: { Id: 'uid2', Name: 'admin' } };
|
||||||
|
|
||||||
|
// Sonarr tag: id 1 → 'alice'
|
||||||
|
const SONARR_TAGS = [{ id: 1, label: 'alice' }];
|
||||||
|
|
||||||
|
const SONARR_RECORD_IMPORTED = {
|
||||||
|
id: 100,
|
||||||
|
eventType: 'downloadFolderImported',
|
||||||
|
sourceTitle: 'Show.S01E01.720p',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
quality: { quality: { name: '720p' } },
|
||||||
|
series: { id: 10, title: 'My Show', titleSlug: 'my-show', tags: [1], images: [] },
|
||||||
|
seriesId: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
const SONARR_RECORD_FAILED = {
|
||||||
|
id: 101,
|
||||||
|
eventType: 'downloadFailed',
|
||||||
|
sourceTitle: 'Show.S01E02.720p',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
quality: { quality: { name: '720p' } },
|
||||||
|
data: { message: 'Not enough disk space' },
|
||||||
|
series: { id: 11, title: 'Admin Show', titleSlug: 'admin-show', tags: [2], images: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tag id 2 → 'admin' (used in the failed-import admin test)
|
||||||
|
const SONARR_TAGS_WITH_ADMIN = [{ id: 1, label: 'alice' }, { id: 2, label: 'admin' }];
|
||||||
|
|
||||||
|
const SONARR_RECORD_FAILED_ALICE = { // failed record tagged alice, for event-type filtering test
|
||||||
|
id: 103,
|
||||||
|
eventType: 'downloadFailed',
|
||||||
|
sourceTitle: 'Show.S01E02.720p',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
quality: { quality: { name: '720p' } },
|
||||||
|
data: { message: 'Disk full' },
|
||||||
|
series: { id: 10, title: 'My Show', titleSlug: 'my-show', tags: [1], images: [] },
|
||||||
|
seriesId: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
const SONARR_RECORD_GRABBED = {
|
||||||
|
id: 102,
|
||||||
|
eventType: 'grabbed',
|
||||||
|
sourceTitle: 'Show.S01E03.720p',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
series: { id: 10, title: 'My Show', titleSlug: 'my-show', tags: [1], images: [] },
|
||||||
|
seriesId: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
const RADARR_RECORD_IMPORTED = {
|
||||||
|
id: 200,
|
||||||
|
eventType: 'downloadFolderImported',
|
||||||
|
sourceTitle: 'My.Movie.2024.1080p',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
quality: { quality: { name: '1080p' } },
|
||||||
|
movie: { id: 20, title: 'My Movie', titleSlug: 'my-movie-2024', tags: [1], images: [] },
|
||||||
|
movieId: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
function interceptLogin(userBody = EMBY_USER, authBody = EMBY_AUTH) {
|
||||||
|
nock(EMBY_BASE).post('/Users/authenticatebyname').reply(200, authBody);
|
||||||
|
nock(EMBY_BASE).get(/\/Users\//).reply(200, userBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-seed the history cache keys that fetchSonarrHistory/fetchRadarrHistory check
|
||||||
|
// first, so they return without making any HTTP calls.
|
||||||
|
const CACHE_TTL = 60 * 60 * 1000; // 1 hour — won't expire during a test run
|
||||||
|
|
||||||
|
function setHistory(sonarrRecords = [], radarrRecords = []) {
|
||||||
|
cache.set('history:sonarr', sonarrRecords.map(r => ({
|
||||||
|
...r,
|
||||||
|
_instanceName: 'Main Sonarr',
|
||||||
|
series: r.series ? { ...r.series, _instanceUrl: 'https://sonarr.test', _instanceName: 'Main Sonarr' } : undefined
|
||||||
|
})), CACHE_TTL);
|
||||||
|
cache.set('history:radarr', radarrRecords.map(r => ({
|
||||||
|
...r,
|
||||||
|
_instanceName: 'Main Radarr',
|
||||||
|
movie: r.movie ? { ...r.movie, _instanceUrl: 'https://radarr.test', _instanceName: 'Main Radarr' } : undefined
|
||||||
|
})), CACHE_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginAs(app, userBody = EMBY_USER, authBody = EMBY_AUTH) {
|
||||||
|
interceptLogin(userBody, authBody);
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/login')
|
||||||
|
.send({ username: userBody.Name, password: 'pw' });
|
||||||
|
const cookies = res.headers['set-cookie'];
|
||||||
|
const csrf = res.body.csrfToken;
|
||||||
|
return { cookies, csrf };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Clear history caches so each test controls its own data
|
||||||
|
cache.invalidate('history:sonarr');
|
||||||
|
cache.invalidate('history:radarr');
|
||||||
|
// Default: empty history
|
||||||
|
setHistory([], []);
|
||||||
|
// Seed poll tag caches so the route can resolve tags
|
||||||
|
cache.set('poll:sonarr-tags', [{ data: SONARR_TAGS }], 60000);
|
||||||
|
cache.set('poll:radarr-tags', [], 60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
cache.invalidate('history:sonarr');
|
||||||
|
cache.invalidate('history:radarr');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/history/recent', () => {
|
||||||
|
describe('authentication', () => {
|
||||||
|
it('returns 401 when not logged in', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const res = await request(app).get('/api/history/recent');
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty history', () => {
|
||||||
|
it('returns empty array when arr returns no records', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.history).toEqual([]);
|
||||||
|
expect(res.body.days).toBe(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event type filtering', () => {
|
||||||
|
it('includes imported and failed records, excludes grabbed', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
setHistory(
|
||||||
|
[SONARR_RECORD_IMPORTED, SONARR_RECORD_FAILED_ALICE, SONARR_RECORD_GRABBED],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const outcomes = res.body.history.map(h => h.outcome);
|
||||||
|
expect(outcomes).toContain('imported');
|
||||||
|
expect(outcomes).toContain('failed');
|
||||||
|
expect(res.body.history).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tag filtering', () => {
|
||||||
|
it('only returns records tagged for the current user', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
setHistory([SONARR_RECORD_IMPORTED], []);
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.history).toHaveLength(1);
|
||||||
|
expect(res.body.history[0].seriesName).toBe('My Show');
|
||||||
|
expect(res.body.history[0].outcome).toBe('imported');
|
||||||
|
expect(res.body.history[0].quality).toBe('720p');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes records tagged for a different user', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const bobAuth = { AccessToken: 'tok3', User: { Id: 'uid3', Name: 'bob' } };
|
||||||
|
const bobUser = { Id: 'uid3', Name: 'bob', Policy: { IsAdministrator: false } };
|
||||||
|
setHistory([SONARR_RECORD_IMPORTED], []);
|
||||||
|
const { cookies } = await loginAs(app, bobUser, bobAuth);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.history).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('radarr records', () => {
|
||||||
|
it('returns movie history items', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
cache.set('poll:radarr-tags', [{ id: 1, label: 'alice' }], 60000);
|
||||||
|
setHistory([], [RADARR_RECORD_IMPORTED]);
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.history).toHaveLength(1);
|
||||||
|
expect(res.body.history[0].type).toBe('movie');
|
||||||
|
expect(res.body.history[0].movieName).toBe('My Movie');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('?days parameter', () => {
|
||||||
|
it('uses custom days value', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent?days=14')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.days).toBe(14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caps days at 90', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent?days=999')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.days).toBe(7); // falls back to default when > 90
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('failed import details', () => {
|
||||||
|
it('includes failureMessage for admin on failed records', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
cache.set('poll:sonarr-tags', [{ data: SONARR_TAGS_WITH_ADMIN }], 60000);
|
||||||
|
setHistory([SONARR_RECORD_FAILED], []);
|
||||||
|
const { cookies } = await loginAs(app, EMBY_ADMIN, EMBY_AUTH_ADMIN);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const failed = res.body.history.find(h => h.outcome === 'failed');
|
||||||
|
expect(failed).toBeDefined();
|
||||||
|
expect(failed.failureMessage).toBe('Not enough disk space');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('response shape', () => {
|
||||||
|
it('returns correct top-level fields', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies } = await loginAs(app);
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/history/recent')
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toHaveProperty('user');
|
||||||
|
expect(res.body).toHaveProperty('isAdmin');
|
||||||
|
expect(res.body).toHaveProperty('days');
|
||||||
|
expect(res.body).toHaveProperty('history');
|
||||||
|
expect(Array.isArray(res.body.history)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
177
tests/unit/historyFetcher.test.js
Normal file
177
tests/unit/historyFetcher.test.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user