diff --git a/e2e/tests/live-arr-fast/search-grab.api.spec.ts b/e2e/tests/live-arr-fast/search-grab.api.spec.ts index ff804208..295701c2 100644 --- a/e2e/tests/live-arr-fast/search-grab.api.spec.ts +++ b/e2e/tests/live-arr-fast/search-grab.api.spec.ts @@ -3,6 +3,7 @@ import { indexerMock } from '../helpers/live-arr'; import { RADARR, SONARR, + WHISPARR, TRANSITION_TIMEOUT, TRIGGERED_SEARCH_TIMEOUT, arrangeInstance, @@ -42,7 +43,7 @@ test.describe('Seeker grab on demand', () => { await resetLiveArrState(); }); - for (const target of [SONARR, RADARR]) { + for (const target of [SONARR, RADARR, WHISPARR]) { test(`records the real ${target.type} grab on the search event`, async ({ api }) => { test.setTimeout(TEST_TIMEOUT); diff --git a/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts b/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts index 833257dd..c4f2fb93 100644 --- a/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts +++ b/e2e/tests/live-arr-fast/seeker-filters.api.spec.ts @@ -4,6 +4,7 @@ import { indexerMock } from '../helpers/live-arr'; import { RADARR, SONARR, + WHISPARR, addDecoyDownloads, arrangeInstance, arrangeQueueCleaner, @@ -472,3 +473,56 @@ test.describe('Seeker monitored-only filters on Sonarr', () => { expect(event?.itemTitle).toBe(`${SONARR.seededTitle} S01`); }); }); + +test.describe('Seeker candidate filters on Whisparr v3', () => { + let library: Array> = []; + + async function mutateWhisparrMovie(mutate: (movie: Record) => Record): Promise { + const movie = await WHISPARR.arr.get>(`/api/v3/movie/${WHISPARR.itemId}`); + await WHISPARR.arr.put(`/api/v3/movie/${WHISPARR.itemId}`, mutate({ ...movie })); + } + + test.beforeEach(async () => { + await resetLiveArrState(); + library = await snapshotLibrary(WHISPARR); + await indexerMock.stubMany(grabbableRelease(WHISPARR.searchMode, WHISPARR.release, WHISPARR.category).mappings); + }); + + test.afterEach(async ({ api }) => { + await teardownInstances(api); + await restoreLibrary(WHISPARR, library); + await resetLiveArrState(); + }); + + test('searches the seeded movie when nothing filters it out', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR); + await triggerSeeker(api); + + await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); + + const event = await firstSearchEvent(api, instanceId); + expect(event?.itemTitle).toContain(WHISPARR.seededTitle); + }); + + test('skips a movie carrying a skipped tag', async ({ api }) => { + const tagId = await WHISPARR.arr.ensureTag(SKIP_TAG); + const instanceId = await arrangeInstance(api, WHISPARR, { instance: { skipTags: [SKIP_TAG] } }); + + await mutateWhisparrMovie((movie) => ({ ...movie, tags: [tagId] })); + await expectNoSearch(api, instanceId, SETTLE_MS); + }); + + test('skips an unmonitored movie when monitoredOnly is on', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR, { instance: { monitoredOnly: true } }); + + await mutateWhisparrMovie((movie) => ({ ...movie, monitored: false })); + await expectNoSearch(api, instanceId, SETTLE_MS); + }); + + test('searches a movie released long ago at the longest grace period', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR, { config: { postReleaseGraceHours: 72 } }); + await triggerSeeker(api); + + await expect.poll(async () => (await listSearchEvents(api, instanceId)).length, { timeout: SETTLE_MS }).toBe(1); + }); +}); diff --git a/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts b/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts index b5c27ace..d7f2711e 100644 --- a/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts +++ b/e2e/tests/live-arr-fast/seeker-preview.api.spec.ts @@ -5,6 +5,7 @@ import { RADARR, RUN_TAG, SONARR, + WHISPARR, TRIGGERED_SEARCH_TIMEOUT, arrangeInstance, listSearchEvents, @@ -172,3 +173,31 @@ test.describe('Seeker preview and funnel', () => { expect(res.status).toBe(404); }); }); + +test.describe('Seeker preview on Whisparr v3', () => { + let library: Array> = []; + + test.beforeEach(async () => { + await resetLiveArrState(); + library = await snapshotLibrary(WHISPARR); + await indexerMock.stub(torznabSearchStub(WHISPARR.searchMode, [])); + }); + + test.afterEach(async ({ api }) => { + await teardownInstances(api); + await restoreLibrary(WHISPARR, library); + await resetLiveArrState(); + }); + + test('lists the seeded movie next up as missing with funnel counts', async ({ api }) => { + const instanceId = await arrangeInstance(api, WHISPARR); + + const preview = await getPreview(api, instanceId); + + const next = preview.nextUp.find((u) => u.name === WHISPARR.seededTitle); + expect(next?.reason).toBe('Missing'); + expect(preview.funnel.libraryItems).toBe(library.length); + expect(preview.funnel.eligibleUnits).toBeGreaterThan(0); + expect(await listSearchEvents(api, instanceId)).toHaveLength(0); + }); +}); diff --git a/e2e/tests/seeker/seeker-config.api.spec.ts b/e2e/tests/seeker/seeker-config.api.spec.ts index 21bda238..7501a91f 100644 --- a/e2e/tests/seeker/seeker-config.api.spec.ts +++ b/e2e/tests/seeker/seeker-config.api.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '../fixtures/base'; +import { test, expect, TEST_CONFIG } from '../fixtures/base'; +import { ArrStubs } from '../helpers/mocks'; test.describe.serial('Seeker — config', () => { test('returns default seeker config', async ({ api }) => { @@ -20,6 +21,36 @@ test.describe.serial('Seeker — config', () => { expect(body).not.toHaveProperty('useCustomFormatScore'); }); + test('lists a Whisparr v3 instance but not a v2 one', async ({ api, mocks }) => { + await mocks.arr.stub(ArrStubs.arrHealthStub()); + + const created: string[] = []; + try { + for (const version of [2, 3]) { + const res = await api.arr.createInstance('whisparr', { + name: `whisparr-v${version}-e2e`, + url: TEST_CONFIG.mocks.arrUrl, + apiKey: 'e2e-test-key', + version, + enabled: true, + }); + expect(res.status).toBeLessThan(300); + created.push((await res.json()).id); + } + + const [v2Id, v3Id] = created; + const config = await (await api.seeker.getConfig()).json(); + const listed = config.instances.map((i: { arrInstanceId: string }) => i.arrInstanceId); + + expect(listed).toContain(v3Id); + expect(listed).not.toContain(v2Id); + } finally { + for (const id of created) { + await api.arr.deleteInstance('whisparr', id); + } + } + }); + test('updates seeker config', async ({ api }) => { const current = await (await api.seeker.getConfig()).json();