diff --git a/.gitattributes b/.gitattributes index 8eb53ea..5e71a70 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -/fixtures/** linguist-generated +**/__fixtures__/** linguist-generated diff --git a/jest.config.ts b/jest.config.ts index 4467868..acd06e2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -20,7 +20,6 @@ const config: Config = { }, resetModules: true, restoreMocks: true, - setupFilesAfterEnv: ['/jest.setup.ts'], testEnvironment: 'node', transform: { '^.+.tsx?$': ['ts-jest', {}], diff --git a/jest.setup.ts b/jest.setup.ts deleted file mode 100644 index 584888c..0000000 --- a/jest.setup.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { cachedFetchText } from './src/utils/fetch'; -import fs from 'node:fs'; -import slugify from 'slugify'; - -// Mocks cachedFetchText and either returns existing fixtures or creates new ones -jest.mock('./src/utils/fetch', () => ({ - cachedFetchText: jest.fn(), -})); -(cachedFetchText as jest.Mock).mockImplementation( - async (url: string) => { - const path = `${__dirname}/fixtures/cachedFetchText/${slugify(url)}`; - if (fs.existsSync(path)) { - return fs.readFileSync(path).toString(); - } else { - const realFetchModule = jest.requireActual('./src/utils/fetch'); - const text = await realFetchModule.cachedFetchText(url); - fs.writeFileSync(path, text); - - return text; - } - }, -); diff --git a/src/embed-extractor/Dropload.ts b/src/embed-extractor/Dropload.ts index ca9a240..be432b3 100644 --- a/src/embed-extractor/Dropload.ts +++ b/src/embed-extractor/Dropload.ts @@ -1,14 +1,22 @@ import { EmbedExtractor } from './types'; -import { cachedFetchText, extractUrlFromPacked, iso2ToFlag, scanFromResolution } from '../utils'; +import { extractUrlFromPacked, Fetcher, iso2ToFlag, scanFromResolution } from '../utils'; export class Dropload implements EmbedExtractor { readonly id = 'dropload'; readonly label = 'Dropload'; + private readonly fetcher: Fetcher; + + constructor(fetcher: Fetcher) { + this.fetcher = fetcher; + } + + readonly supports = (url: string): boolean => null !== url.match(/dropload/); + readonly extract = async (url: string, language: string) => { const normalizedUrl = url.replace('/e/', '').replace('/embed-', '/'); - const html = await cachedFetchText(normalizedUrl); + const html = await this.fetcher.text(normalizedUrl); const resolution = scanFromResolution((html.match(/(\d{3,}x\d{3,}),/) as string[])[1] as string); diff --git a/src/embed-extractor/EmbedExtractorRegistry.ts b/src/embed-extractor/EmbedExtractorRegistry.ts deleted file mode 100644 index afacee8..0000000 --- a/src/embed-extractor/EmbedExtractorRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EmbedExtractor } from './types'; -import { Dropload } from './Dropload'; -import { SuperVideo } from './SuperVideo'; - -type EmbedExtractorRegistryType = Record; - -export const EmbedExtractorRegistry: EmbedExtractorRegistryType = { - dropload: new Dropload(), - supervideo: new SuperVideo(), -}; diff --git a/src/embed-extractor/EmbedExtractors.test.ts b/src/embed-extractor/EmbedExtractors.test.ts new file mode 100644 index 0000000..32c5fe2 --- /dev/null +++ b/src/embed-extractor/EmbedExtractors.test.ts @@ -0,0 +1,9 @@ +import { EmbedExtractors } from './EmbedExtractors'; + +describe('EmbedExtractors', () => { + test('throws when no embed extractor can be found', () => { + const embedExtractors = new EmbedExtractors([]); + + expect(embedExtractors.handle('https://some-url.test', 'en')).rejects.toThrow('No embed extractor found that supports url https://some-url.test'); + }); +}); diff --git a/src/embed-extractor/EmbedExtractors.ts b/src/embed-extractor/EmbedExtractors.ts new file mode 100644 index 0000000..352535b --- /dev/null +++ b/src/embed-extractor/EmbedExtractors.ts @@ -0,0 +1,20 @@ +import { EmbedExtractor } from './types'; +import { StreamWithMeta } from '../types'; + +export class EmbedExtractors { + private readonly embedExtractors: EmbedExtractor[]; + + constructor(embedExtractors: EmbedExtractor[]) { + this.embedExtractors = embedExtractors; + } + + readonly handle = async (url: string, language: string): Promise => { + const embedExtractor = this.embedExtractors.find(embedExtractor => embedExtractor.supports(url)); + + if (undefined === embedExtractor) { + throw new Error(`No embed extractor found that supports url ${url}`); + } + + return embedExtractor.extract(url, language); + }; +} diff --git a/src/embed-extractor/SuperVideo.ts b/src/embed-extractor/SuperVideo.ts index 6437dc6..627fc64 100644 --- a/src/embed-extractor/SuperVideo.ts +++ b/src/embed-extractor/SuperVideo.ts @@ -1,14 +1,22 @@ import { EmbedExtractor } from './types'; -import { cachedFetchText, extractUrlFromPacked, iso2ToFlag, scanFromResolution } from '../utils'; +import { extractUrlFromPacked, Fetcher, iso2ToFlag, scanFromResolution } from '../utils'; export class SuperVideo implements EmbedExtractor { readonly id = 'supervideo'; readonly label = 'SuperVideo'; + private readonly fetcher: Fetcher; + + constructor(fetcher: Fetcher) { + this.fetcher = fetcher; + } + + readonly supports = (url: string): boolean => null !== url.match(/supervideo/); + readonly extract = async (url: string, language: string) => { const normalizedUrl = url.replace('/e/', '/').replace('/embed-', '/'); - const html = await cachedFetchText(normalizedUrl); + const html = await this.fetcher.text(normalizedUrl); const resolutionAndSizeMatch = html.match(/(\d{3,}x\d{3,}), ([\d.]+) ?([GM]B)/) as string[]; const resolution = scanFromResolution(resolutionAndSizeMatch[1] as string); diff --git a/src/embed-extractor/index.ts b/src/embed-extractor/index.ts index 76c0bb1..2630e13 100644 --- a/src/embed-extractor/index.ts +++ b/src/embed-extractor/index.ts @@ -1,2 +1,4 @@ -export * from './EmbedExtractorRegistry'; +export * from './EmbedExtractors'; +export * from './Dropload'; +export * from './SuperVideo'; export * from './types'; diff --git a/src/embed-extractor/types.ts b/src/embed-extractor/types.ts index e517e18..cc24b34 100644 --- a/src/embed-extractor/types.ts +++ b/src/embed-extractor/types.ts @@ -5,5 +5,7 @@ export interface EmbedExtractor { readonly label: string; - readonly extract: (embedUrl: string, language: string) => Promise; + readonly supports: (url: string) => boolean; + + readonly extract: (url: string, language: string) => Promise; } diff --git a/src/handler/KinoKiste.test.ts b/src/handler/KinoKiste.test.ts index 565874e..037b586 100644 --- a/src/handler/KinoKiste.test.ts +++ b/src/handler/KinoKiste.test.ts @@ -1,6 +1,11 @@ import { KinoKiste } from './KinoKiste'; +import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +import { Fetcher } from '../utils'; +jest.mock('../utils/Fetcher'); -const kinokiste = new KinoKiste(); +// @ts-expect-error No constructor args needed +const fetcher = new Fetcher(); +const kinokiste = new KinoKiste(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); describe('KinoKiste', () => { test('does not handle non imdb series', async () => { diff --git a/src/handler/KinoKiste.ts b/src/handler/KinoKiste.ts index a58c87a..37cc28c 100644 --- a/src/handler/KinoKiste.ts +++ b/src/handler/KinoKiste.ts @@ -1,8 +1,7 @@ import * as cheerio from 'cheerio'; -import slugify from 'slugify'; import { Handler } from './types'; -import { ImdbId, cachedFetchText, fulfillAllPromises, parseImdbId } from '../utils'; -import { EmbedExtractor, EmbedExtractorRegistry } from '../embed-extractor'; +import { ImdbId, fulfillAllPromises, parseImdbId, Fetcher } from '../utils'; +import { EmbedExtractors } from '../embed-extractor'; export class KinoKiste implements Handler { readonly id = 'kinokiste'; @@ -13,6 +12,14 @@ export class KinoKiste implements Handler { readonly languages = ['de']; + private readonly fetcher: Fetcher; + private readonly embedExtractors: EmbedExtractors; + + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + this.fetcher = fetcher; + this.embedExtractors = embedExtractors; + } + readonly handle = async (id: string) => { if (!id.startsWith('tt')) { return Promise.resolve([]); @@ -25,7 +32,7 @@ export class KinoKiste implements Handler { return Promise.resolve([]); } - const html = await cachedFetchText(seriesPageUrl); + const html = await this.fetcher.text(seriesPageUrl); const $ = cheerio.load(html); @@ -33,19 +40,15 @@ export class KinoKiste implements Handler { $(`[data-num="${imdbId.series}x${imdbId.episode}"]`) .siblings('.mirrors') .children('[data-link]') - .map((_i, urlElement) => ({ - embedId: slugify($(urlElement).attr('data-m') as string), - embedUrl: ($(urlElement).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'), - })) + .map((_i, el) => ($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')) .toArray() - .filter(({ embedId }) => embedId.match(/^(dropload|supervideo)$/)) - .map(({ embedId, embedUrl }) => (EmbedExtractorRegistry[embedId] as EmbedExtractor).extract(embedUrl, 'de')) - .filter(stream => stream !== undefined), + .filter(embedUrl => embedUrl.match(/(dropload|supervideo)/)) + .map(embedUrl => this.embedExtractors.handle(embedUrl, 'de')), ); }; private fetchSeriesPageUrl = async (imdbId: ImdbId): Promise => { - const html = await cachedFetchText(`https://kinokiste.live/serien/?do=search&subaction=search&story=${imdbId.id}`); + const html = await this.fetcher.text(`https://kinokiste.live/serien/?do=search&subaction=search&story=${imdbId.id}`); const $ = cheerio.load(html); diff --git a/src/handler/MeineCloud.test.ts b/src/handler/MeineCloud.test.ts index bc65c9e..a9fd75e 100644 --- a/src/handler/MeineCloud.test.ts +++ b/src/handler/MeineCloud.test.ts @@ -1,6 +1,11 @@ import { MeineCloud } from './MeineCloud'; +import { Fetcher } from '../utils'; +import { Dropload, EmbedExtractors, SuperVideo } from '../embed-extractor'; +jest.mock('../utils/Fetcher'); -const meinecloud = new MeineCloud(); +// @ts-expect-error No constructor args needed +const fetcher = new Fetcher(); +const meinecloud = new MeineCloud(fetcher, new EmbedExtractors([new Dropload(fetcher), new SuperVideo(fetcher)])); describe('MeineCloud', () => { test('does not handle non imdb movies', async () => { diff --git a/src/handler/MeineCloud.ts b/src/handler/MeineCloud.ts index a65eff8..acb06a6 100644 --- a/src/handler/MeineCloud.ts +++ b/src/handler/MeineCloud.ts @@ -1,8 +1,7 @@ import * as cheerio from 'cheerio'; -import slugify from 'slugify'; import { Handler } from './types'; -import { cachedFetchText, fulfillAllPromises, parseImdbId } from '../utils'; -import { EmbedExtractor, EmbedExtractorRegistry } from '../embed-extractor'; +import { Fetcher, fulfillAllPromises, parseImdbId } from '../utils'; +import { EmbedExtractors } from '../embed-extractor'; export class MeineCloud implements Handler { readonly id = 'meinecloud'; @@ -13,25 +12,29 @@ export class MeineCloud implements Handler { readonly languages = ['de']; + private readonly fetcher: Fetcher; + private readonly embedExtractors: EmbedExtractors; + + constructor(fetcher: Fetcher, embedExtractors: EmbedExtractors) { + this.fetcher = fetcher; + this.embedExtractors = embedExtractors; + } + readonly handle = async (id: string) => { if (!id.startsWith('tt')) { return Promise.resolve([]); } - const html = await cachedFetchText(`https://meinecloud.click/movie/${parseImdbId(id).id}`); + const html = await this.fetcher.text(`https://meinecloud.click/movie/${parseImdbId(id).id}`); const $ = cheerio.load(html); return fulfillAllPromises( $('[data-link!=""]') - .map((_i, el) => ({ - embedId: slugify($(el).text()), - embedUrl: ($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'), - })) + .map((_i, el) => ($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://')) .toArray() - .filter(({ embedId }) => embedId.match(/^(dropload|supervideo)$/)) - .map(({ embedId, embedUrl }) => (EmbedExtractorRegistry[embedId] as EmbedExtractor).extract(embedUrl, 'de')) - .filter(stream => stream !== undefined), + .filter(embedUrl => embedUrl.match(/(dropload|supervideo)/)) + .map(embedUrl => this.embedExtractors.handle(embedUrl, 'de')), ); }; } diff --git a/src/index.ts b/src/index.ts index 4ac29b5..805f457 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,27 @@ import express, { NextFunction, Request, Response } from 'express'; +import makeFetchHappen from 'make-fetch-happen'; import { landingTemplate } from './landingTemplate'; import { Handler, KinoKiste, MeineCloud } from './handler'; -import { buildManifest, fulfillAllPromises, logInfo } from './utils'; +import { Dropload, EmbedExtractors, SuperVideo } from './embed-extractor'; +import { buildManifest, Fetcher, fulfillAllPromises, logInfo } from './utils'; import { Config, StreamWithMeta } from './types'; +import fs from 'node:fs'; +import * as os from 'node:os'; const addon = express(); +const fetcher = new Fetcher(makeFetchHappen.defaults({ + cachePath: `${fs.realpathSync(os.tmpdir())}/webstreamr`, +})); + +const embedExtractors = new EmbedExtractors([ + new Dropload(fetcher), + new SuperVideo(fetcher), +]); + const handlers: Handler[] = [ - new KinoKiste(), - new MeineCloud(), + new KinoKiste(fetcher, embedExtractors), + new MeineCloud(fetcher, embedExtractors), ]; addon.use((_req: Request, res: Response, next: NextFunction) => { diff --git a/src/utils/fetch.test.ts b/src/utils/Fetcher.test.ts similarity index 51% rename from src/utils/fetch.test.ts rename to src/utils/Fetcher.test.ts index 92b77f3..806c191 100644 --- a/src/utils/fetch.test.ts +++ b/src/utils/Fetcher.test.ts @@ -1,5 +1,5 @@ -const realFetchModule = jest.requireActual('./fetch'); -const cachedFetchText = realFetchModule.cachedFetchText; +import { Fetcher } from './Fetcher'; +import makeFetchHappen from 'make-fetch-happen'; global.console = { ...console, @@ -15,17 +15,19 @@ jest.mock('make-fetch-happen', () => ({ }, })); +const fetcher = new Fetcher(makeFetchHappen.defaults()); + describe('fetch', () => { - test('cachedFetchText throws if the response is not OK', async () => { + test('text throws if the response is not OK', async () => { mockedFetch.mockResolvedValue({ ok: false, status: 500, statusText: 'some error happened' }); - await expect(cachedFetchText('https://some-url.test')).rejects.toThrow(new Error('HTTP error: 500 - some error happened')); + await expect(fetcher.text('https://some-url.test')).rejects.toThrow(new Error('HTTP error: 500 - some error happened')); }); - test('cachedFetchText passes response through if it is OK', async () => { + test('text passes response through if it is OK', async () => { mockedFetch.mockResolvedValue({ ok: true, text: () => Promise.resolve('some text') }); - const responseText = await cachedFetchText('https://some-url.test'); + const responseText = await fetcher.text('https://some-url.test'); expect(responseText).toBe('some text'); }); diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts new file mode 100644 index 0000000..08aed17 --- /dev/null +++ b/src/utils/Fetcher.ts @@ -0,0 +1,21 @@ +import { FetchInterface, FetchOptions } from 'make-fetch-happen'; +import { logInfo } from './log'; + +export class Fetcher { + private readonly fetch: FetchInterface; + + constructor(fetch: FetchInterface) { + this.fetch = fetch; + } + + readonly text = async (uriOrRequest: string | Request, opts?: FetchOptions): Promise => { + logInfo(`Fetch ${uriOrRequest}`); + + const response = await this.fetch(uriOrRequest, opts); + if (!response.ok) { + throw new Error(`HTTP error: ${response.status} - ${response.statusText}`); + } + + return await response.text(); + }; +} diff --git a/fixtures/cachedFetchText/https:dropload.iojvjwrkpijr0f.html b/src/utils/__fixtures__/Fetcher/https:dropload.iojvjwrkpijr0f.html similarity index 100% rename from fixtures/cachedFetchText/https:dropload.iojvjwrkpijr0f.html rename to src/utils/__fixtures__/Fetcher/https:dropload.iojvjwrkpijr0f.html diff --git a/fixtures/cachedFetchText/https:dropload.iolyo2h1snpe5c.html b/src/utils/__fixtures__/Fetcher/https:dropload.iolyo2h1snpe5c.html similarity index 100% rename from fixtures/cachedFetchText/https:dropload.iolyo2h1snpe5c.html rename to src/utils/__fixtures__/Fetcher/https:dropload.iolyo2h1snpe5c.html diff --git a/fixtures/cachedFetchText/https:kinokiste.livedeutsch29992-black-mirror-anschauen.html b/src/utils/__fixtures__/Fetcher/https:kinokiste.livedeutsch29992-black-mirror-anschauen.html similarity index 100% rename from fixtures/cachedFetchText/https:kinokiste.livedeutsch29992-black-mirror-anschauen.html rename to src/utils/__fixtures__/Fetcher/https:kinokiste.livedeutsch29992-black-mirror-anschauen.html diff --git a/fixtures/cachedFetchText/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt12345678 b/src/utils/__fixtures__/Fetcher/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt12345678 similarity index 100% rename from fixtures/cachedFetchText/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt12345678 rename to src/utils/__fixtures__/Fetcher/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt12345678 diff --git a/fixtures/cachedFetchText/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt2085059 b/src/utils/__fixtures__/Fetcher/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt2085059 similarity index 100% rename from fixtures/cachedFetchText/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt2085059 rename to src/utils/__fixtures__/Fetcher/https:kinokiste.liveseriendosearchandsubactionsearchandstorytt2085059 diff --git a/fixtures/cachedFetchText/https:meinecloud.clickmoviett12345678 b/src/utils/__fixtures__/Fetcher/https:meinecloud.clickmoviett12345678 similarity index 100% rename from fixtures/cachedFetchText/https:meinecloud.clickmoviett12345678 rename to src/utils/__fixtures__/Fetcher/https:meinecloud.clickmoviett12345678 diff --git a/fixtures/cachedFetchText/https:meinecloud.clickmoviett29141112 b/src/utils/__fixtures__/Fetcher/https:meinecloud.clickmoviett29141112 similarity index 100% rename from fixtures/cachedFetchText/https:meinecloud.clickmoviett29141112 rename to src/utils/__fixtures__/Fetcher/https:meinecloud.clickmoviett29141112 diff --git a/fixtures/cachedFetchText/https:supervideo.cc22go50mon0ke.html b/src/utils/__fixtures__/Fetcher/https:supervideo.cc22go50mon0ke.html similarity index 100% rename from fixtures/cachedFetchText/https:supervideo.cc22go50mon0ke.html rename to src/utils/__fixtures__/Fetcher/https:supervideo.cc22go50mon0ke.html diff --git a/fixtures/cachedFetchText/https:supervideo.ccq7i0sw1oytw3 b/src/utils/__fixtures__/Fetcher/https:supervideo.ccq7i0sw1oytw3 similarity index 100% rename from fixtures/cachedFetchText/https:supervideo.ccq7i0sw1oytw3 rename to src/utils/__fixtures__/Fetcher/https:supervideo.ccq7i0sw1oytw3 diff --git a/src/utils/__mocks__/Fetcher.ts b/src/utils/__mocks__/Fetcher.ts new file mode 100644 index 0000000..d39e0fd --- /dev/null +++ b/src/utils/__mocks__/Fetcher.ts @@ -0,0 +1,20 @@ +import { FetchOptions } from 'make-fetch-happen'; +import makeFetchHappen from 'make-fetch-happen'; +import fs from 'node:fs'; +import slugify from 'slugify'; + +export class Fetcher { + readonly text = async (uriOrRequest: string, opts?: FetchOptions): Promise => { + const path = `${__dirname}/../__fixtures__/Fetcher/${slugify(uriOrRequest)}`; + + if (fs.existsSync(path)) { + return fs.readFileSync(path).toString(); + } else { + const text = await (await makeFetchHappen.defaults()(uriOrRequest, opts)).text(); + + fs.writeFileSync(path, text); + + return text; + } + }; +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts deleted file mode 100644 index d28af14..0000000 --- a/src/utils/fetch.ts +++ /dev/null @@ -1,37 +0,0 @@ -import makeFetchHappen from 'make-fetch-happen'; -import { FetchInterface, FetchOptions } from 'make-fetch-happen'; -import fs from 'node:fs'; -import * as os from 'node:os'; -import { Response } from 'node-fetch'; -import { logInfo } from './log'; - -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -class MakeFetchHappenSingleton { - private static instance: FetchInterface; - - public static getInstance(): FetchInterface { - if (!MakeFetchHappenSingleton.instance) { - MakeFetchHappenSingleton.instance = makeFetchHappen.defaults({ - cachePath: `${fs.realpathSync(os.tmpdir())}/webstreamr`, - }); - } - return MakeFetchHappenSingleton.instance; - } -} - -const cachedFetch = async (uriOrRequest: string | Request, opts?: FetchOptions): Promise => { - logInfo(`Fetch ${uriOrRequest}`); - - const fetch = MakeFetchHappenSingleton.getInstance(); - - return await fetch(uriOrRequest, opts); -}; - -export const cachedFetchText = async (uriOrRequest: string | Request, opts?: FetchOptions): Promise => { - const response = await cachedFetch(uriOrRequest, opts); - if (!response.ok) { - throw new Error(`HTTP error: ${response.status} - ${response.statusText}`); - } - - return await response.text(); -}; diff --git a/src/utils/index.ts b/src/utils/index.ts index ef9fc4f..f263404 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ +export * from './Fetcher'; export * from './embed'; -export * from './fetch'; export * from './imdb'; export * from './language'; export * from './log'; diff --git a/src/utils/manifest.test.ts b/src/utils/manifest.test.ts index b998aa6..6fa37b2 100644 --- a/src/utils/manifest.test.ts +++ b/src/utils/manifest.test.ts @@ -1,5 +1,10 @@ import { buildManifest } from './manifest'; import { KinoKiste } from '../handler'; +import { Fetcher } from './Fetcher'; +import { EmbedExtractors } from '../embed-extractor'; + +// @ts-expect-error No constructor args needed +const fetcher = new Fetcher(); describe('buildManifest', () => { test('has an empty config without handlers', () => { @@ -9,14 +14,14 @@ describe('buildManifest', () => { }); test('has unchecked handler without a config', () => { - const manifest = buildManifest([new KinoKiste()], {}); + const manifest = buildManifest([new KinoKiste(fetcher, new EmbedExtractors([]))], {}); expect(manifest.config).toHaveLength(1); expect(manifest.config[0]?.default).toBeUndefined(); }); test('has checked handler with appropriate config', () => { - const kinokiste = new KinoKiste(); + const kinokiste = new KinoKiste(fetcher, new EmbedExtractors([])); const manifest = buildManifest([kinokiste], { [kinokiste.id]: 'on' }); expect(manifest.config).toHaveLength(1);