import test from 'node:test'; import assert from 'node:assert/strict'; import { createHandleAnilistSetupWindowClosedHandler, createMaybeFocusExistingAnilistSetupWindowHandler, createHandleAnilistSetupWindowOpenedHandler, createAnilistSetupDidFailLoadHandler, createAnilistSetupDidFinishLoadHandler, createAnilistSetupDidNavigateHandler, createAnilistSetupFallbackHandler, createAnilistSetupWillNavigateHandler, createAnilistSetupWillRedirectHandler, createAnilistSetupWindowOpenHandler, createHandleManualAnilistSetupSubmissionHandler, createOpenAnilistSetupWindowHandler, } from './anilist-setup-window'; test('manual anilist setup submission forwards access token to callback consumer', () => { const consumed: string[] = []; const handleSubmission = createHandleManualAnilistSetupSubmissionHandler({ consumeCallbackUrl: (rawUrl) => { consumed.push(rawUrl); return true; }, redirectUri: 'https://anilist.subminer.moe/', logWarn: () => {}, }); const handled = handleSubmission('subminer://anilist-setup?access_token=abc123'); assert.equal(handled, true); assert.equal(consumed.length, 1); assert.ok(consumed[0]!.includes('https://anilist.subminer.moe/#access_token=abc123')); }); test('maybe focus anilist setup window focuses existing window', () => { let focused = false; const handler = createMaybeFocusExistingAnilistSetupWindowHandler({ getSetupWindow: () => ({ focus: () => { focused = true; }, }), }); const handled = handler(); assert.equal(handled, true); assert.equal(focused, true); }); test('manual anilist setup submission warns on missing token', () => { const warnings: string[] = []; const handleSubmission = createHandleManualAnilistSetupSubmissionHandler({ consumeCallbackUrl: () => false, redirectUri: 'https://anilist.subminer.moe/', logWarn: (message) => warnings.push(message), }); const handled = handleSubmission('subminer://anilist-setup'); assert.equal(handled, true); assert.deepEqual(warnings, ['AniList setup submission missing access token']); }); test('anilist setup fallback handler triggers browser + manual entry on load fail', () => { const calls: string[] = []; const fallback = createAnilistSetupFallbackHandler({ authorizeUrl: 'https://anilist.co', developerSettingsUrl: 'https://anilist.co/settings/developer', setupWindow: { isDestroyed: () => false, }, openSetupInBrowser: () => calls.push('open-browser'), loadManualTokenEntry: () => calls.push('load-manual'), logError: () => calls.push('error'), logWarn: () => calls.push('warn'), }); fallback.onLoadFailure({ errorCode: -1, errorDescription: 'failed', validatedURL: 'about:blank', }); assert.deepEqual(calls, ['error', 'open-browser', 'load-manual']); }); test('anilist setup window open handler denies unsafe url', () => { const calls: string[] = []; const handler = createAnilistSetupWindowOpenHandler({ isAllowedExternalUrl: () => false, openExternal: () => calls.push('open'), logWarn: () => calls.push('warn'), }); const result = handler({ url: 'https://malicious.example' }); assert.deepEqual(result, { action: 'deny' }); assert.deepEqual(calls, ['warn']); }); test('anilist setup will-navigate handler blocks callback redirect uri', () => { let prevented = false; const handler = createAnilistSetupWillNavigateHandler({ handleManualSubmission: () => false, consumeCallbackUrl: () => false, redirectUri: 'https://anilist.subminer.moe/', isAllowedNavigationUrl: () => true, logWarn: () => {}, }); handler({ url: 'https://anilist.subminer.moe/#access_token=abc', preventDefault: () => { prevented = true; }, }); assert.equal(prevented, true); }); test('anilist setup will-navigate handler blocks unsafe urls', () => { const calls: string[] = []; let prevented = false; const handler = createAnilistSetupWillNavigateHandler({ handleManualSubmission: () => false, consumeCallbackUrl: () => false, redirectUri: 'https://anilist.subminer.moe/', isAllowedNavigationUrl: () => false, logWarn: () => calls.push('warn'), }); handler({ url: 'https://unsafe.example', preventDefault: () => { prevented = true; }, }); assert.equal(prevented, true); assert.deepEqual(calls, ['warn']); }); test('anilist setup will-redirect handler prevents callback redirects', () => { let prevented = false; const handler = createAnilistSetupWillRedirectHandler({ consumeCallbackUrl: () => true, }); handler({ url: 'https://anilist.subminer.moe/#access_token=abc', preventDefault: () => { prevented = true; }, }); assert.equal(prevented, true); }); test('anilist setup did-navigate handler consumes callback url', () => { const seen: string[] = []; const handler = createAnilistSetupDidNavigateHandler({ consumeCallbackUrl: (url) => { seen.push(url); return true; }, }); handler('https://anilist.subminer.moe/#access_token=abc'); assert.deepEqual(seen, ['https://anilist.subminer.moe/#access_token=abc']); }); test('anilist setup did-fail-load handler forwards details', () => { const seen: Array<{ errorCode: number; errorDescription: string; validatedURL: string }> = []; const handler = createAnilistSetupDidFailLoadHandler({ onLoadFailure: (details) => seen.push(details), }); handler({ errorCode: -3, errorDescription: 'timeout', validatedURL: 'https://anilist.co/api/v2/oauth/authorize', }); assert.equal(seen.length, 1); assert.equal(seen[0]!.errorCode, -3); }); test('anilist setup did-finish-load handler triggers fallback on blank page', () => { const calls: string[] = []; const handler = createAnilistSetupDidFinishLoadHandler({ getLoadedUrl: () => 'about:blank', onBlankPageLoaded: () => calls.push('fallback'), }); handler(); assert.deepEqual(calls, ['fallback']); }); test('anilist setup did-finish-load handler no-ops on non-blank page', () => { const calls: string[] = []; const handler = createAnilistSetupDidFinishLoadHandler({ getLoadedUrl: () => 'https://anilist.co/api/v2/oauth/authorize', onBlankPageLoaded: () => calls.push('fallback'), }); handler(); assert.equal(calls.length, 0); }); test('anilist setup window closed handler clears references', () => { const calls: string[] = []; const handler = createHandleAnilistSetupWindowClosedHandler({ clearSetupWindow: () => calls.push('clear-window'), setSetupPageOpened: (opened) => calls.push(`opened:${opened ? 'yes' : 'no'}`), }); handler(); assert.deepEqual(calls, ['clear-window', 'opened:no']); }); test('anilist setup window opened handler sets references', () => { const calls: string[] = []; const handler = createHandleAnilistSetupWindowOpenedHandler({ setSetupWindow: () => calls.push('set-window'), setSetupPageOpened: (opened) => calls.push(`opened:${opened ? 'yes' : 'no'}`), }); handler(); assert.deepEqual(calls, ['set-window', 'opened:yes']); }); test('open anilist setup handler no-ops when existing setup window focused', () => { const calls: string[] = []; const handler = createOpenAnilistSetupWindowHandler({ maybeFocusExistingSetupWindow: () => { calls.push('focus-existing'); return true; }, createSetupWindow: () => { calls.push('create-window'); throw new Error('should not create'); }, buildAuthorizeUrl: () => 'https://anilist.co/api/v2/oauth/authorize?client_id=36084', consumeCallbackUrl: () => false, openSetupInBrowser: () => {}, loadManualTokenEntry: () => {}, redirectUri: 'https://anilist.subminer.moe/', developerSettingsUrl: 'https://anilist.co/settings/developer', isAllowedExternalUrl: () => true, isAllowedNavigationUrl: () => true, logWarn: () => {}, logError: () => {}, clearSetupWindow: () => {}, setSetupPageOpened: () => {}, setSetupWindow: () => {}, openExternal: () => {}, }); handler(); assert.deepEqual(calls, ['focus-existing']); }); test('open anilist setup handler wires navigation, fallback, and lifecycle', () => { let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null; let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null; let didNavigateHandler: ((event: unknown, url: string) => void) | null = null; let didFinishLoadHandler: (() => void) | null = null; let didFailLoadHandler: | ((event: unknown, errorCode: number, errorDescription: string, validatedURL: string) => void) | null = null; let closedHandler: (() => void) | null = null; let prevented = false; const calls: string[] = []; const fakeWindow = { focus: () => {}, webContents: { setWindowOpenHandler: (handler: (params: { url: string }) => { action: 'deny' }) => { openHandler = handler; }, on: ( event: | 'will-navigate' | 'will-redirect' | 'did-navigate' | 'did-fail-load' | 'did-finish-load', handler: (...args: any[]) => void, ) => { if (event === 'will-navigate') willNavigateHandler = handler as never; if (event === 'did-navigate') didNavigateHandler = handler as never; if (event === 'did-finish-load') didFinishLoadHandler = handler as never; if (event === 'did-fail-load') didFailLoadHandler = handler as never; }, getURL: () => 'about:blank', }, on: (event: 'closed', handler: () => void) => { if (event === 'closed') closedHandler = handler; }, isDestroyed: () => false, }; const handler = createOpenAnilistSetupWindowHandler({ maybeFocusExistingSetupWindow: () => false, createSetupWindow: () => fakeWindow, buildAuthorizeUrl: () => 'https://anilist.co/api/v2/oauth/authorize?client_id=36084', consumeCallbackUrl: (rawUrl) => { calls.push(`consume:${rawUrl}`); return rawUrl.includes('access_token='); }, openSetupInBrowser: () => calls.push('open-browser'), loadManualTokenEntry: () => calls.push('load-manual'), redirectUri: 'https://anilist.subminer.moe/', developerSettingsUrl: 'https://anilist.co/settings/developer', isAllowedExternalUrl: () => true, isAllowedNavigationUrl: () => true, logWarn: (message) => calls.push(`warn:${message}`), logError: (message) => calls.push(`error:${message}`), clearSetupWindow: () => calls.push('clear-window'), setSetupPageOpened: (opened) => calls.push(`opened:${opened ? 'yes' : 'no'}`), setSetupWindow: () => calls.push('set-window'), openExternal: (url) => calls.push(`open:${url}`), }); handler(); assert.ok(openHandler); assert.ok(willNavigateHandler); assert.ok(didNavigateHandler); assert.ok(didFinishLoadHandler); assert.ok(didFailLoadHandler); assert.ok(closedHandler); assert.deepEqual(calls.slice(0, 3), ['load-manual', 'set-window', 'opened:yes']); const onOpen = openHandler as ((params: { url: string }) => { action: 'deny' }) | null; if (!onOpen) throw new Error('missing window open handler'); assert.deepEqual(onOpen({ url: 'https://anilist.co/settings/developer' }), { action: 'deny' }); assert.ok(calls.includes('open:https://anilist.co/settings/developer')); const onWillNavigate = willNavigateHandler as | ((event: { preventDefault: () => void }, url: string) => void) | null; if (!onWillNavigate) throw new Error('missing will navigate handler'); onWillNavigate( { preventDefault: () => { prevented = true; }, }, 'https://anilist.subminer.moe/#access_token=abc', ); assert.equal(prevented, true); const onDidNavigate = didNavigateHandler as ((event: unknown, url: string) => void) | null; if (!onDidNavigate) throw new Error('missing did navigate handler'); onDidNavigate({}, 'https://anilist.subminer.moe/#access_token=abc'); const onDidFinishLoad = didFinishLoadHandler as (() => void) | null; if (!onDidFinishLoad) throw new Error('missing did finish load handler'); onDidFinishLoad(); assert.ok(calls.includes('warn:AniList setup loaded a blank page; using fallback')); assert.ok(calls.includes('open-browser')); const onDidFailLoad = didFailLoadHandler as | ((event: unknown, errorCode: number, errorDescription: string, validatedURL: string) => void) | null; if (!onDidFailLoad) throw new Error('missing did fail load handler'); onDidFailLoad({}, -1, 'load failed', 'about:blank'); assert.ok(calls.includes('error:AniList setup window failed to load')); const onClosed = closedHandler as (() => void) | null; if (!onClosed) throw new Error('missing closed handler'); onClosed(); assert.ok(calls.includes('clear-window')); assert.ok(calls.includes('opened:no')); });