const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const backgroundSource = fs.readFileSync('background.js', 'utf8'); function extractFunction(source, name) { const markers = [`async function ${name}(`, `function ${name}(`]; const start = markers .map((marker) => source.indexOf(marker)) .find((index) => index >= 0); if (start < 0) { throw new Error(`missing function ${name}`); } let parenDepth = 0; let signatureEnded = false; let braceStart = -1; for (let i = start; i < source.length; i += 1) { const ch = source[i]; if (ch === '(') { parenDepth += 1; } else if (ch === ')') { parenDepth -= 1; if (parenDepth === 0) { signatureEnded = true; } } else if (ch === '{' && signatureEnded) { braceStart = i; break; } } let depth = 0; let end = braceStart; for (; end < source.length; end += 1) { const ch = source[end]; if (ch === '{') depth += 1; if (ch === '}') { depth -= 1; if (depth === 0) { end += 1; break; } } } return source.slice(start, end); } function createMockResponse(ok, status, payload) { return { ok, status, async json() { return payload; }, }; } test('background imports contribution oauth module and keeps contribution runtime out of persisted settings', () => { const persistedStart = backgroundSource.indexOf('const PERSISTED_SETTING_DEFAULTS = {'); const persistedEnd = backgroundSource.indexOf('const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);'); const defaultStateStart = backgroundSource.indexOf('const DEFAULT_STATE = {'); const defaultStateEnd = backgroundSource.indexOf('async function getState()'); const persistedBlock = backgroundSource.slice(persistedStart, persistedEnd); const defaultStateBlock = backgroundSource.slice(defaultStateStart, defaultStateEnd); assert.match(backgroundSource, /background\/contribution-oauth\.js/); assert.doesNotMatch(persistedBlock, /contributionSessionId|contributionAuthUrl|contributionCallbackUrl|contributionStatus/); assert.match(defaultStateBlock, /contributionMode:\s*false|CONTRIBUTION_RUNTIME_DEFAULTS/); }); test('contribution oauth module exposes a factory', () => { const source = fs.readFileSync('background/contribution-oauth.js', 'utf8'); const globalScope = {}; const api = new Function('self', 'fetch', `${source}; return self.MultiPageBackgroundContributionOAuth;`)( globalScope, async () => createMockResponse(true, 200, { ok: true }) ); assert.equal(typeof api?.createContributionOAuthManager, 'function'); assert.equal(Array.isArray(api?.RUNTIME_KEYS), true); }); test('buildContributionModeState preserves active contribution runtime while forcing CPA mode', () => { const bundle = extractFunction(backgroundSource, 'buildContributionModeState'); const api = new Function(` const DEFAULT_STATE = { panelMode: 'cpa' }; const CONTRIBUTION_RUNTIME_DEFAULTS = { contributionMode: false, contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', contributionCallbackUrl: '', contributionStatus: '', contributionStatusMessage: '', contributionLastPollAt: 0, contributionCallbackStatus: 'idle', contributionCallbackMessage: '', contributionAuthOpenedAt: 0, contributionAuthTabId: 0, }; const CONTRIBUTION_RUNTIME_KEYS = Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS); ${bundle} return { buildContributionModeState }; `)(); assert.deepStrictEqual( api.buildContributionModeState(true, { panelMode: 'sub2api', customPassword: 'Secret123!', accountRunHistoryTextEnabled: true, }, { contributionSessionId: 'session-001', contributionAuthUrl: 'https://auth.example.com', contributionStatus: 'waiting', contributionCallbackStatus: 'waiting', }), { contributionMode: true, contributionSessionId: 'session-001', contributionAuthUrl: 'https://auth.example.com', contributionAuthState: '', contributionCallbackUrl: '', contributionStatus: 'waiting', contributionStatusMessage: '', contributionLastPollAt: 0, contributionCallbackStatus: 'waiting', contributionCallbackMessage: '', contributionAuthOpenedAt: 0, contributionAuthTabId: 0, panelMode: 'cpa', customPassword: '', accountRunHistoryTextEnabled: false, } ); assert.deepStrictEqual( api.buildContributionModeState(false, { panelMode: 'sub2api', customPassword: 'Secret123!', accountRunHistoryTextEnabled: true, }, { contributionSessionId: 'session-001', contributionAuthUrl: 'https://auth.example.com', contributionStatus: 'waiting', }), { contributionMode: false, contributionSessionId: '', contributionAuthUrl: '', contributionAuthState: '', contributionCallbackUrl: '', contributionStatus: '', contributionStatusMessage: '', contributionLastPollAt: 0, contributionCallbackStatus: 'idle', contributionCallbackMessage: '', contributionAuthOpenedAt: 0, contributionAuthTabId: 0, panelMode: 'sub2api', customPassword: 'Secret123!', accountRunHistoryTextEnabled: true, } ); }); test('resetState preserves contribution runtime across reset', () => { assert.match(backgroundSource, /CONTRIBUTION_RUNTIME_KEYS/); assert.match(backgroundSource, /const contributionModeState = buildContributionModeState/); assert.match(backgroundSource, /\.\.\.contributionModeState/); }); test('message router handles contribution mode, start flow, and status polling messages', async () => { const source = fs.readFileSync('background/message-router.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope); const calls = []; const router = api.createMessageRouter({ ensureManualInteractionAllowed: async () => ({ stepStatuses: { 1: 'pending', 2: 'completed' }, contributionMode: true, }), pollContributionStatus: async (options) => { calls.push({ type: 'poll', options }); return { contributionStatus: 'waiting' }; }, setContributionMode: async (enabled) => { calls.push({ type: 'toggle', enabled }); return { contributionMode: Boolean(enabled), panelMode: 'cpa', }; }, startContributionFlow: async (options) => { calls.push({ type: 'start', options }); return { contributionMode: true, contributionSessionId: 'session-001', contributionStatus: 'started', }; }, }); const enableResponse = await router.handleMessage({ type: 'SET_CONTRIBUTION_MODE', payload: { enabled: true }, }); const startResponse = await router.handleMessage({ type: 'START_CONTRIBUTION_FLOW', payload: { nickname: '阿青' }, }); const pollResponse = await router.handleMessage({ type: 'POLL_CONTRIBUTION_STATUS', payload: { reason: 'test_poll' }, }); assert.equal(enableResponse.ok, true); assert.equal(startResponse.ok, true); assert.equal(pollResponse.ok, true); assert.deepStrictEqual(calls, [ { type: 'toggle', enabled: true }, { type: 'start', options: { nickname: '阿青' } }, { type: 'poll', options: { reason: 'test_poll' } }, ]); }); test('account run history snapshot sync is disabled in contribution mode', () => { const source = fs.readFileSync('background/account-run-history.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope); const helpers = api.createAccountRunHistoryHelpers({ addLog: async () => {}, buildLocalHelperEndpoint: (baseUrl, path) => `${baseUrl}${path}`, chrome: { storage: { local: { get: async () => ({}), set: async () => {}, }, }, }, getErrorMessage: (error) => error?.message || String(error || ''), getState: async () => ({}), normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(), }); assert.equal( helpers.shouldSyncAccountRunHistorySnapshot({ contributionMode: true, accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373', }), false ); assert.equal( helpers.shouldSyncAccountRunHistorySnapshot({ contributionMode: false, accountRunHistoryTextEnabled: true, accountRunHistoryHelperBaseUrl: 'http://127.0.0.1:17373', }), true ); }); test('contribution oauth manager starts session, opens auth url, polls real statuses, and treats callback submit no-op as success', async () => { const source = fs.readFileSync('background/contribution-oauth.js', 'utf8'); const globalScope = {}; const fetchCalls = []; const tabCalls = []; const closeCallbackCalls = []; let currentState = { contributionMode: true, email: 'user@example.com', contributionSessionId: '', contributionStatus: '', contributionCallbackStatus: 'idle', }; const broadcasts = []; const api = new Function('self', 'fetch', `${source}; return self.MultiPageBackgroundContributionOAuth;`)( globalScope, async (url, options = {}) => { fetchCalls.push({ url, options }); if (String(url).endsWith('/start')) { return createMockResponse(true, 200, { ok: true, session_id: 'session-001', state: 'oauth-state-001', auth_url: 'https://auth.example.com/oauth?state=oauth-state-001', message: '登录地址已生成', }); } if (String(url).includes('/status?')) { return createMockResponse(true, 200, { ok: true, session_id: 'session-001', status: 'waiting', message: '等待 OAuth 回调完成', }); } if (String(url).endsWith('/submit-callback')) { return createMockResponse(false, 400, { ok: false, message: '当前已启用自动回调转发,无需手动粘贴回调 URL。', callback_url_received: true, }); } return createMockResponse(true, 200, { ok: true, session_id: 'session-001', status: 'processing', message: '授权已完成,正在自动审核并导入。', }); } ); const manager = api.createContributionOAuthManager({ addLog: async () => {}, broadcastDataUpdate(updates) { broadcasts.push(updates); currentState = { ...currentState, ...updates }; }, chrome: { tabs: { async create(payload) { tabCalls.push(payload); return { id: 88, url: payload.url }; }, async update() { return null; }, onUpdated: { addListener() {} }, }, webNavigation: { onCommitted: { addListener() {} }, onHistoryStateUpdated: { addListener() {} }, }, }, closeLocalhostCallbackTabs: async (callbackUrl) => { closeCallbackCalls.push(callbackUrl); }, getState: async () => currentState, setState: async (updates) => { currentState = { ...currentState, ...updates }; }, }); const startedState = await manager.startContributionFlow(); assert.equal(startedState.contributionSessionId, 'session-001'); assert.equal(startedState.contributionAuthState, 'oauth-state-001'); assert.equal(startedState.contributionStatus, 'waiting'); assert.equal(startedState.contributionAuthTabId, 88); assert.equal(tabCalls.length, 1); assert.match(fetchCalls[0].url, /\/start$/); assert.match(fetchCalls[1].url, /\/status\?/); const callbackState = await manager.handleCapturedCallback( 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001', { source: 'test' } ); assert.equal(callbackState.contributionCallbackUrl, 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001'); assert.equal(callbackState.contributionCallbackStatus, 'not_required'); assert.equal(closeCallbackCalls[0], 'http://localhost:1455/auth/callback?code=abc123&state=oauth-state-001'); assert.ok(fetchCalls.some((call) => String(call.url).endsWith('/submit-callback'))); assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'captured')); assert.ok(broadcasts.some((item) => item.contributionCallbackStatus === 'not_required')); }); test('refreshOAuthUrlBeforeStep6 uses contribution oauth session instead of panel bridge in contribution mode', async () => { const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6'); const calls = []; const api = new Function(` ${bundle} return { refreshOAuthUrlBeforeStep6 }; `)(); globalThis.addLog = async (message) => { calls.push({ type: 'log', message }); }; globalThis.contributionOAuthManager = { async startContributionFlow(options) { calls.push({ type: 'contribution', options }); return { contributionAuthUrl: 'https://auth.example.com/oauth?state=oauth-state-001', }; }, }; globalThis.handleStepData = async (step, payload) => { calls.push({ type: 'step', step, payload }); }; globalThis.getPanelModeLabel = () => 'CPA'; globalThis.requestOAuthUrlFromPanel = async () => { calls.push({ type: 'panel' }); return { oauthUrl: 'https://panel.example.com/oauth' }; }; globalThis.LOG_PREFIX = '[test]'; const oauthUrl = await api.refreshOAuthUrlBeforeStep6({ contributionMode: true, email: 'user@example.com', }); assert.equal(oauthUrl, 'https://auth.example.com/oauth?state=oauth-state-001'); assert.deepStrictEqual(calls, [ { type: 'log', message: '步骤 7:贡献模式正在申请贡献登录地址...' }, { type: 'contribution', options: { nickname: 'user@example.com', openAuthTab: false, stateOverride: { contributionMode: true, email: 'user@example.com', }, }, }, { type: 'step', step: 1, payload: { oauthUrl: 'https://auth.example.com/oauth?state=oauth-state-001', }, }, ]); delete globalThis.addLog; delete globalThis.contributionOAuthManager; delete globalThis.handleStepData; delete globalThis.getPanelModeLabel; delete globalThis.requestOAuthUrlFromPanel; delete globalThis.LOG_PREFIX; });