From 7a76142ae337ce27779109b2b47bdc14883e93b9 Mon Sep 17 00:00:00 2001 From: Michael Thomas Date: Sat, 12 Sep 2026 19:32:51 -0400 Subject: [PATCH] test: add scanner update rate override for testing (#3241) --- server/lib/scanners/jellyfin/jellyfin.test.ts | 7 ++-- server/lib/scanners/radarr/radarr.test.ts | 37 ++++++++++--------- server/lib/scanners/sonarr/sonarr.test.ts | 31 ++++++++-------- server/test/runWithMockTimers.ts | 34 +++++++++++++++++ 4 files changed, 73 insertions(+), 36 deletions(-) create mode 100644 server/test/runWithMockTimers.ts diff --git a/server/lib/scanners/jellyfin/jellyfin.test.ts b/server/lib/scanners/jellyfin/jellyfin.test.ts index d56e139d4..4c93dc29b 100644 --- a/server/lib/scanners/jellyfin/jellyfin.test.ts +++ b/server/lib/scanners/jellyfin/jellyfin.test.ts @@ -18,6 +18,7 @@ import { User } from '@server/entity/User'; import type { Library } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { setupTestDb } from '@server/test/db'; +import { runWithMockTimers } from '@server/test/runWithMockTimers'; import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; @@ -342,7 +343,7 @@ describe('Jellyfin Scanner', () => { return []; }; - await jellyfinFullScanner.run(); + await runWithMockTimers(() => jellyfinFullScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 5000 }, @@ -420,7 +421,7 @@ describe('Jellyfin Scanner', () => { return []; }; - await jellyfinFullScanner.run(); + await runWithMockTimers(() => jellyfinFullScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 5001 }, @@ -497,7 +498,7 @@ describe('Jellyfin Scanner', () => { return []; }; - await jellyfinFullScanner.run(); + await runWithMockTimers(() => jellyfinFullScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 5002 }, diff --git a/server/lib/scanners/radarr/radarr.test.ts b/server/lib/scanners/radarr/radarr.test.ts index 7faeed97f..25a0e346f 100644 --- a/server/lib/scanners/radarr/radarr.test.ts +++ b/server/lib/scanners/radarr/radarr.test.ts @@ -13,6 +13,7 @@ import { radarrScanner } from '@server/lib/scanners/radarr'; import type { RadarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { setupTestDb } from '@server/test/db'; +import { runWithMockTimers } from '@server/test/runWithMockTimers'; import assert from 'node:assert/strict'; import { beforeEach, describe, it, mock } from 'node:test'; @@ -93,7 +94,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ monitored: false, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 550 }, @@ -109,7 +110,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 777, monitored: false, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const media = await mediaRepository.findOne({ where: { tmdbId: 777 }, @@ -131,7 +132,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 551, monitored: true, hasFile: true }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 551 }, @@ -153,7 +154,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 552, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 552 }, @@ -175,7 +176,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 553, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 553 }, @@ -197,7 +198,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 554, monitored: false, hasFile: true }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 554 }, @@ -223,7 +224,7 @@ describe('Radarr Scanner', () => { getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 950 }, @@ -243,7 +244,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 1, id: 99 })]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 999 }, @@ -263,7 +264,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 888 }, @@ -285,7 +286,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 700, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 700 }, @@ -306,7 +307,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 800 }, @@ -344,7 +345,7 @@ describe('Radarr Scanner', () => { return [fakeRadarrMovie({ tmdbId: 903, id: 11 })]; }; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updatedOrphan = await mediaRepository.findOneOrFail({ where: { tmdbId: 901 }, @@ -372,7 +373,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true, is4k: true }]); getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 1, id: 99 })]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 960 }, @@ -393,7 +394,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true, is4k: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 961 }, @@ -436,7 +437,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => [fakeRadarrMovie({ tmdbId: 1, id: 99 })]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updatedMedia = await mediaRepository.findOneOrFail({ where: { tmdbId: 1003596 }, @@ -484,7 +485,7 @@ describe('Radarr Scanner', () => { fakeRadarrMovie({ tmdbId: 700, monitored: true, hasFile: false }), ]; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updatedRequest = await requestRepository.findOneOrFail({ where: { id: request.id }, @@ -525,7 +526,7 @@ describe('Radarr Scanner', () => { configureRadarr([{ syncEnabled: true }]); getMoviesImpl = async () => []; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updatedMedia = await mediaRepository.findOneOrFail({ where: { tmdbId: 1234 }, @@ -621,7 +622,7 @@ describe('Radarr Scanner', () => { return [fakeRadarrMovie({ tmdbId: 2, id: 88 })]; }; - await radarrScanner.run(); + await runWithMockTimers(() => radarrScanner.run()); const updatedMedia = await mediaRepository.findOneOrFail({ where: { tmdbId: 1003598 }, diff --git a/server/lib/scanners/sonarr/sonarr.test.ts b/server/lib/scanners/sonarr/sonarr.test.ts index eb061e063..6f5790d2d 100644 --- a/server/lib/scanners/sonarr/sonarr.test.ts +++ b/server/lib/scanners/sonarr/sonarr.test.ts @@ -15,10 +15,10 @@ import Media from '@server/entity/Media'; import MediaRequest from '@server/entity/MediaRequest'; import Season from '@server/entity/Season'; import { User } from '@server/entity/User'; -import { sonarrScanner } from '@server/lib/scanners/sonarr'; import type { SonarrSettings } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import { setupTestDb } from '@server/test/db'; +import { runWithMockTimers } from '@server/test/runWithMockTimers'; import assert from 'node:assert/strict'; import { beforeEach, describe, it, mock } from 'node:test'; @@ -116,6 +116,7 @@ Object.defineProperty(TheMovieDb.prototype, 'getTvShowForScan', { configurable: true, }); +import { sonarrScanner } from '@server/lib/scanners/sonarr'; mock.method(MediaRequest, 'sendNotification', async () => undefined); setupTestDb(); @@ -206,7 +207,7 @@ describe('Sonarr Scanner', () => { getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1050 }, @@ -236,7 +237,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1000 }, @@ -266,7 +267,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1001 }, @@ -316,7 +317,7 @@ describe('Sonarr Scanner', () => { getShowByTvdbIdImpl = async () => fakeTmdbShow(1); getTvShowImpl = async () => fakeTmdbShow(1); - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1 }, @@ -350,7 +351,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1003 }, @@ -376,7 +377,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1004 }, @@ -425,7 +426,7 @@ describe('Sonarr Scanner', () => { getShowByTvdbIdImpl = async () => fakeTmdbShow(2); getTvShowImpl = async () => fakeTmdbShow(2); - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updatedOrphan = await mediaRepository.findOneOrFail({ where: { tmdbId: 1010 }, @@ -453,7 +454,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1020 }, @@ -484,7 +485,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true, is4k: true }]); getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1030 }, @@ -520,7 +521,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true, is4k: true }]); getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updated = await mediaRepository.findOneOrFail({ where: { tmdbId: 1031 }, @@ -575,7 +576,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => [fakeSonarrSeries({ tvdbId: 999 })]; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updatedMedia = await mediaRepository.findOneOrFail({ where: { tmdbId: 2000 }, @@ -649,7 +650,7 @@ describe('Sonarr Scanner', () => { getShowByTvdbIdImpl = async () => fakeTmdbShow(2001); getTvShowImpl = async () => fakeTmdbShow(2001); - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updatedRequest = await requestRepository.findOneOrFail({ where: { id: request.id }, @@ -698,7 +699,7 @@ describe('Sonarr Scanner', () => { configureSonarr([{ syncEnabled: true }]); getSeriesImpl = async () => []; - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updatedMedia = await mediaRepository.findOneOrFail({ where: { tmdbId: 2005 }, @@ -817,7 +818,7 @@ describe('Sonarr Scanner', () => { tvdbId === 666 ? fakeTmdbShow(2002) : fakeTmdbShow(997); getTvShowImpl = async ({ tvId }) => fakeTmdbShow(tvId); - await sonarrScanner.run(); + await runWithMockTimers(() => sonarrScanner.run()); const updatedMedia = await mediaRepository.findOneOrFail({ where: { tmdbId: 2002 }, diff --git a/server/test/runWithMockTimers.ts b/server/test/runWithMockTimers.ts new file mode 100644 index 000000000..597f44dd2 --- /dev/null +++ b/server/test/runWithMockTimers.ts @@ -0,0 +1,34 @@ +import { mock } from 'node:test'; + +export async function runWithMockTimers( + runFn: () => Promise, + tickMs = 4000 +): Promise { + mock.timers.enable({ apis: ['setTimeout'] }); + try { + let settled = false; + const runPromise = runFn(); + runPromise + .catch(() => undefined) + .finally(() => { + settled = true; + }); + const maxTicks = 100000; + for (let i = 0; i < maxTicks && !settled; i++) { + await new Promise((resolve) => setImmediate(resolve)); + mock.timers.tick(tickMs); + } + // The final tick may have settled runPromise, but `settled` only updates + // in a microtask; yield once more before the guard check to avoid false + // negatives when the resolving tick lands on the last iteration. + await new Promise((resolve) => setImmediate(resolve)); + if (!settled) { + throw new Error( + `runWithMockTimers: promise did not settle after ${maxTicks} ticks` + ); + } + return await runPromise; + } finally { + mock.timers.reset(); + } +}