const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const source = fs.readFileSync('background/steps/create-plus-checkout.js', 'utf8'); const globalScope = {}; const api = new Function('self', `${source}; return self.MultiPageBackgroundPlusCheckoutCreate;`)(globalScope); test('Plus checkout create does not wait 20 seconds after opening checkout page', async () => { const events = []; const executor = api.createPlusCheckoutCreateExecutor({ addLog: async (message, level = 'info') => { events.push({ type: 'log', message, level }); }, chrome: { tabs: { create: async (payload) => { events.push({ type: 'tab-create', payload }); return { id: 42 }; }, update: async (tabId, payload) => { events.push({ type: 'tab-update', tabId, payload }); }, }, }, completeStepFromBackground: async (step, payload) => { events.push({ type: 'complete', step, payload }); }, ensureContentScriptReadyOnTabUntilStopped: async () => { events.push({ type: 'ready' }); }, registerTab: async (source, tabId) => { events.push({ type: 'register', source, tabId }); }, sendTabMessageUntilStopped: async (tabId, source, message) => { events.push({ type: 'tab-message', tabId, source, message }); return { checkoutUrl: 'https://checkout.stripe.com/c/pay/session', country: 'US', currency: 'USD', }; }, setState: async (payload) => { events.push({ type: 'set-state', payload }); }, sleepWithStop: async (ms) => { events.push({ type: 'sleep', ms }); }, waitForTabCompleteUntilStopped: async () => { events.push({ type: 'tab-complete' }); }, }); await executor.executePlusCheckoutCreate(); assert.deepEqual( events.find((event) => event.type === 'tab-create'), { type: 'tab-create', payload: { url: 'https://chatgpt.com/', active: true } } ); assert.deepEqual( events.find((event) => event.type === 'register'), { type: 'register', source: 'plus-checkout', tabId: 42 } ); const sleepEvents = events.filter((event) => event.type === 'sleep'); assert.deepStrictEqual(sleepEvents.map((event) => event.ms), [1000, 1000]); assert.deepStrictEqual( events.find((event) => event.type === 'tab-message')?.message?.payload, { paymentMethod: 'paypal' } ); const completeIndex = events.findIndex((event) => event.type === 'complete'); const readyLogIndex = events.findIndex((event) => event.type === 'log' && /已就绪/.test(event.message)); assert.ok(readyLogIndex > -1); assert.ok(completeIndex > readyLogIndex); assert.equal(events.some((event) => event.type === 'sleep' && event.ms === 20000), false); }); test('GoPay plus checkout create forwards gopay payment method to the checkout content script', async () => { const events = []; const executor = api.createPlusCheckoutCreateExecutor({ addLog: async () => {}, chrome: { tabs: { create: async () => ({ id: 99 }), update: async () => {}, }, }, completeStepFromBackground: async () => {}, ensureContentScriptReadyOnTabUntilStopped: async () => {}, registerTab: async () => {}, sendTabMessageUntilStopped: async (_tabId, _source, message) => { events.push(message); return { checkoutUrl: 'https://chatgpt.com/checkout/openai_llc/test-session', country: 'ID', currency: 'IDR', }; }, setState: async () => {}, sleepWithStop: async () => {}, waitForTabCompleteUntilStopped: async () => {}, }); await executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gopay' }); assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' }); }); test('GPC checkout injects Plus script before reading ChatGPT session token and sends card_key', async () => { const events = []; const fetchCalls = []; const executor = api.createPlusCheckoutCreateExecutor({ addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }), chrome: { tabs: { create: async (payload) => { events.push({ type: 'tab-create', payload }); return { id: 77 }; }, remove: async (tabId) => events.push({ type: 'tab-remove', tabId }), }, }, completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }), ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }), fetch: async (url, options = {}) => { fetchCalls.push({ url, options }); return { ok: true, status: 200, json: async () => ({ reference_id: 'ref_123', gopay_guid: 'guid_456', next_action: 'enter_otp', flow_id: 'flow_789', }), }; }, registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }), sendTabMessageUntilStopped: async (tabId, source, message) => { events.push({ type: 'tab-message', tabId, source, message }); return { accessToken: 'session-access-token' }; }, setState: async (payload) => events.push({ type: 'set-state', payload }), sleepWithStop: async (ms) => events.push({ type: 'sleep', ms }), waitForTabCompleteUntilStopped: async () => events.push({ type: 'tab-complete' }), }); await executor.executePlusCheckoutCreate({ email: 'Current.Round+GPC@Example.COM', plusPaymentMethod: 'gpc-helper', gopayHelperApiUrl: 'https://gopay.hwork.pro/', gopayHelperPhoneNumber: '+8613800138000', gopayPhone: '', gopayHelperCountryCode: '+86', gopayHelperPin: '123456', gopayHelperCardKey: 'card_test_123', }); const readyIndex = events.findIndex((event) => event.type === 'ready'); const messageIndex = events.findIndex((event) => event.type === 'tab-message'); assert.ok(readyIndex >= 0); assert.ok(messageIndex > readyIndex); assert.equal(events[messageIndex].message.type, 'PLUS_CHECKOUT_GET_STATE'); assert.deepEqual(events[messageIndex].message.payload, { includeSession: true, includeAccessToken: true, }); assert.equal(fetchCalls.length, 1); assert.equal(fetchCalls[0].url, 'https://gopay.hwork.pro/api/checkout/start'); const helperPayload = JSON.parse(fetchCalls[0].options.body); assert.equal(helperPayload.customer_email, 'current.round+gpc@example.com'); assert.equal(helperPayload.card_key, 'card_test_123'); assert.deepEqual(helperPayload.gopay_link, { type: 'gopay', country_code: '86', phone_number: '13800138000', }); assert.equal(events.find((event) => event.type === 'set-state')?.payload?.plusCheckoutSource, 'gpc-helper'); assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, 'ref_123'); assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperFlowId, 'flow_789'); assert.equal(events.find((event) => event.type === 'complete')?.step, 6); assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper'); }); test('GPC checkout treats non-zero API amount as non-free-trial and does not create order', async () => { const markCalls = []; const fetchCalls = []; const events = []; const executor = api.createPlusCheckoutCreateExecutor({ addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }), chrome: { tabs: { create: async () => { throw new Error('should not open token tab when direct access token exists'); }, remove: async () => {}, }, }, completeStepFromBackground: async () => { throw new Error('should not complete step 6 for non-free-trial checkout'); }, ensureContentScriptReadyOnTabUntilStopped: async () => {}, fetch: async (url, options = {}) => { fetchCalls.push({ url, options }); return { ok: true, status: 200, json: async () => ({ reference_id: 'ref_paid', gopay_guid: 'guid_paid', next_action: 'enter_otp', checkout: { amount_due: 'Rp 29.000' }, }), }; }, markCurrentRegistrationAccountUsed: async (state, options) => { markCalls.push({ state, options }); return { updated: true }; }, registerTab: async () => {}, sendTabMessageUntilStopped: async () => {}, setState: async () => {}, sleepWithStop: async () => {}, waitForTabCompleteUntilStopped: async () => {}, }); await assert.rejects( () => executor.executePlusCheckoutCreate({ email: 'paid@example.com', plusPaymentMethod: 'gpc-helper', gopayHelperApiUrl: 'https://gopay.hwork.pro/', chatgptAccessToken: 'state-access-token', gopayHelperPhoneNumber: '+8613800138000', gopayHelperCountryCode: '+86', gopayHelperPin: '123456', gopayHelperCardKey: 'card_paid_456', }), /PLUS_CHECKOUT_NON_FREE_TRIAL::.*余额非 0/ ); assert.equal(fetchCalls.length, 1); assert.equal(JSON.parse(fetchCalls[0].options.body).card_key, 'card_paid_456'); assert.equal(markCalls.length, 1); assert.equal(markCalls[0].state.email, 'paid@example.com'); assert.equal(markCalls[0].options.reason, 'plus-checkout-non-free-trial'); assert.equal(events.some((event) => event.type === 'log' && /订单已创建/.test(event.message)), false); }); test('GPC checkout does not fall back to browser GoPay phone fields', async () => { const executor = api.createPlusCheckoutCreateExecutor({ addLog: async () => {}, chrome: { tabs: { create: async () => { throw new Error('should not open token tab when direct access token exists'); }, remove: async () => {}, }, }, completeStepFromBackground: async () => {}, ensureContentScriptReadyOnTabUntilStopped: async () => {}, fetch: async () => { throw new Error('should not call helper API without helper phone'); }, registerTab: async () => {}, sendTabMessageUntilStopped: async () => {}, setState: async () => {}, sleepWithStop: async () => {}, waitForTabCompleteUntilStopped: async () => {}, }); await assert.rejects( () => executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gpc-helper', gopayHelperApiUrl: 'https://gopay.hwork.pro/', chatgptAccessToken: 'state-access-token', email: 'helper-phone-test@example.com', gopayPhone: '+8613800138000', gopayCountryCode: '+86', gopayPin: '123456', gopayHelperPhoneNumber: '', gopayHelperPin: '123456', gopayHelperCardKey: 'card_phone_test', }), /缺少手机号/ ); }); test('GPC checkout rejects missing card key before calling helper API', async () => { const executor = api.createPlusCheckoutCreateExecutor({ addLog: async () => {}, chrome: { tabs: { create: async () => { throw new Error('should not open token tab when direct access token exists'); }, remove: async () => {}, }, }, completeStepFromBackground: async () => {}, ensureContentScriptReadyOnTabUntilStopped: async () => {}, fetch: async () => { throw new Error('should not call helper API without card key'); }, registerTab: async () => {}, sendTabMessageUntilStopped: async () => {}, setState: async () => {}, sleepWithStop: async () => {}, waitForTabCompleteUntilStopped: async () => {}, }); await assert.rejects( () => executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gpc-helper', gopayHelperApiUrl: 'https://gopay.hwork.pro/', chatgptAccessToken: 'state-access-token', email: 'missing-card@example.com', gopayHelperPhoneNumber: '+8613800138000', gopayHelperCountryCode: '+86', gopayHelperPin: '123456', gopayHelperCardKey: '', }), /缺少卡密/ ); });