diff --git a/background.js b/background.js index 0ecbd43..b566f8d 100644 --- a/background.js +++ b/background.js @@ -21,6 +21,7 @@ importScripts( 'gopay-utils.js', 'phone-sms/providers/hero-sms.js', 'phone-sms/providers/five-sim.js', + 'phone-sms/providers/nexsms.js', 'phone-sms/providers/madao.js', 'phone-sms/providers/registry.js', 'background/phone-verification-flow.js', diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index c67fc45..de0923c 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -6963,8 +6963,8 @@ ); }; - const rotateCurrentActivation = async (reason = '', releaseAction = 'cancel') => { - const normalizedActivation = normalizeActivation(activation); + const rotateCurrentActivation = async (activationCandidate, reason = '', releaseAction = 'cancel') => { + const normalizedActivation = normalizeActivation(activationCandidate); if (!normalizedActivation || getActivationProviderId(normalizedActivation, state) !== PHONE_SMS_PROVIDER_MADAO) { return { handled: false, nextActivation: null }; } @@ -6984,9 +6984,6 @@ if (!nextActivation) { return { handled: true, nextActivation: null }; } - activation = nextActivation; - shouldCancelActivation = true; - await persistCurrentActivation(nextActivation); await addLog( `步骤 9:MaDao 已通过路由替换切换到新号码 ${nextActivation.phoneNumber}。`, 'info' @@ -7004,13 +7001,19 @@ `步骤 9:添加手机号失败后正在更换号码(${formatStep9Reason(failureReason)},${usedNumberReplacementAttempts}/${maxNumberReplacementAttempts})。`, 'warn' ); - const rotated = shouldCancelActivation && activation + const failedActivation = activation; + const rotated = shouldCancelActivation && failedActivation ? await rotateCurrentActivation( + failedActivation, failureCode || failureReason, getPhoneReplacementReleaseAction(failureCode || failureReason) ) : { handled: false, nextActivation: null }; if (rotated.nextActivation) { + activation = normalizeActivation(rotated.nextActivation); + shouldCancelActivation = true; + await clearCurrentActivation(); + await persistCurrentActivation(activation); preferReuseExistingActivationOnAddPhone = false; addPhoneReentryWithSameActivation = 0; pageState = { @@ -7126,9 +7129,14 @@ ); } const rotated = shouldCancelActivation && activation - ? await rotateCurrentActivation('returned_to_add_phone_loop', 'cancel') + ? await rotateCurrentActivation(activation, 'returned_to_add_phone_loop', 'cancel') : { handled: false, nextActivation: null }; - if (!rotated.nextActivation) { + if (rotated.nextActivation) { + activation = normalizeActivation(rotated.nextActivation); + shouldCancelActivation = true; + await clearCurrentActivation(); + await persistCurrentActivation(activation); + } else { if (!rotated.handled && shouldCancelActivation && activation) { await cancelPhoneActivation(state, activation); } @@ -7191,9 +7199,14 @@ ); } const rotated = shouldCancelActivation && activation - ? await rotateCurrentActivation('phone_number_used', 'ban') + ? await rotateCurrentActivation(activation, 'phone_number_used', 'ban') : { handled: false, nextActivation: null }; - if (!rotated.nextActivation) { + if (rotated.nextActivation) { + activation = normalizeActivation(rotated.nextActivation); + shouldCancelActivation = true; + await clearCurrentActivation(); + await persistCurrentActivation(activation); + } else { if (!rotated.handled && shouldCancelActivation && activation) { await banPhoneActivation(state, activation); } @@ -7282,6 +7295,8 @@ let shouldReplaceNumber = false; let replaceReason = ''; + let failedActivationForReplacement = null; + let replacementPreparedInAdvance = false; for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) { throwIfStopped(); @@ -7291,6 +7306,7 @@ await markPreferredActivationExhausted(codeResult.reason || 'sms_timeout'); shouldReplaceNumber = true; replaceReason = codeResult.reason || 'sms_not_received'; + failedActivationForReplacement = activation; break; } @@ -7326,6 +7342,7 @@ if (isPhoneNumberUsedError(invalidErrorText)) { shouldReplaceNumber = true; replaceReason = 'phone_number_used'; + failedActivationForReplacement = activation; await discardPhoneActivationFromReuse( `目标站拒绝该号码(${invalidErrorText})。`, activation, @@ -7336,13 +7353,19 @@ `自动白嫖复用号码 ${activation.phoneNumber} 被目标站拒绝。` ); } - if ( - shouldCancelActivation - && activation - && getActivationProviderId(activation, state) !== PHONE_SMS_PROVIDER_MADAO - ) { - await banPhoneActivation(state, activation); - shouldCancelActivation = false; + if (shouldCancelActivation && activation) { + const rotated = await rotateCurrentActivation(activation, 'phone_number_used', 'ban'); + if (rotated.nextActivation) { + activation = normalizeActivation(rotated.nextActivation); + shouldCancelActivation = true; + replacementPreparedInAdvance = true; + } else if (rotated.handled) { + activation = null; + shouldCancelActivation = false; + } else { + await banPhoneActivation(state, activation); + shouldCancelActivation = false; + } } await addLog( `步骤 9:手机号被提示已使用(${invalidErrorText}),立即更换新号码。`, @@ -7354,13 +7377,20 @@ if (attempt >= DEFAULT_PHONE_SUBMIT_ATTEMPTS) { shouldReplaceNumber = true; replaceReason = 'code_rejected'; - if ( - shouldCancelActivation - && activation - && getActivationProviderId(activation, state) !== PHONE_SMS_PROVIDER_MADAO - ) { - await banPhoneActivation(state, activation); - shouldCancelActivation = false; + failedActivationForReplacement = activation; + if (shouldCancelActivation && activation) { + const rotated = await rotateCurrentActivation(activation, 'code_rejected', 'ban'); + if (rotated.nextActivation) { + activation = normalizeActivation(rotated.nextActivation); + shouldCancelActivation = true; + replacementPreparedInAdvance = true; + } else if (rotated.handled) { + activation = null; + shouldCancelActivation = false; + } else { + await banPhoneActivation(state, activation); + shouldCancelActivation = false; + } } await addLog( `步骤 9:手机验证码连续 ${DEFAULT_PHONE_SUBMIT_ATTEMPTS} 次被拒(${invalidErrorText}),将更换号码。`, @@ -7466,33 +7496,47 @@ throw buildPhoneReplacementLimitError(maxNumberReplacementAttempts, replaceReason || 'unknown'); } - const rotated = shouldCancelActivation && activation - ? await rotateCurrentActivation( - replaceReason || 'replace_number', - getPhoneReplacementReleaseAction(replaceReason || 'replace_number') - ) - : { handled: false, nextActivation: null }; - if (shouldRetireFreeReusableActivationOnFailure(await getState(), activation)) { + const failedActivation = failedActivationForReplacement || activation; + const rotated = replacementPreparedInAdvance + ? { handled: true, nextActivation: activation } + : ( + shouldCancelActivation && failedActivation + ? await rotateCurrentActivation( + failedActivation, + replaceReason || 'replace_number', + getPhoneReplacementReleaseAction(replaceReason || 'replace_number') + ) + : { handled: false, nextActivation: null } + ); + if (shouldRetireFreeReusableActivationOnFailure(await getState(), failedActivation)) { await retireFreeReusableActivation( - `自动白嫖复用号码 ${activation.phoneNumber} 在失败后被更换。` + `自动白嫖复用号码 ${failedActivation.phoneNumber} 在失败后被更换。` ); } - if (isPhoneNumberUsedFailureReason(replaceReason)) { + if (isPhoneNumberUsedFailureReason(replaceReason) && failedActivation) { await discardPhoneActivationFromReuse( `目标站拒绝该号码(${replaceReason})。`, - activation, + failedActivation, await getState() ); } - if (!rotated.nextActivation) { - if (!rotated.handled && shouldCancelActivation && activation) { - await cancelPhoneActivation(state, activation); + await clearCurrentActivation(); + if (rotated.nextActivation) { + activation = normalizeActivation(rotated.nextActivation); + shouldCancelActivation = Boolean(activation); + if (activation) { + await persistCurrentActivation(activation); + } + } else { + if (!rotated.handled && shouldCancelActivation && failedActivation) { + await cancelPhoneActivation(state, failedActivation); } - await clearCurrentActivation(); activation = null; shouldCancelActivation = false; } addPhoneReentryWithSameActivation = 0; + failedActivationForReplacement = null; + replacementPreparedInAdvance = false; let returnResult = null; try { diff --git a/phone-sms/providers/nexsms.js b/phone-sms/providers/nexsms.js new file mode 100644 index 0000000..d3a189a --- /dev/null +++ b/phone-sms/providers/nexsms.js @@ -0,0 +1,265 @@ +// phone-sms/providers/nexsms.js - NexSMS provider registry adapter +(function attachNexSmsProvider(root, factory) { + root.PhoneSmsNexSmsProvider = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createNexSmsProviderModule() { + const PROVIDER_ID = 'nexsms'; + const DEFAULT_BASE_URL = 'https://api.nexsms.net'; + const DEFAULT_SERVICE_CODE = 'ot'; + const DEFAULT_SERVICE_LABEL = 'OpenAI'; + const DEFAULT_COUNTRY_ID = 1; + const DEFAULT_COUNTRY_LABEL = 'Country #1'; + const DEFAULT_REQUEST_TIMEOUT_MS = 20000; + + function normalizeBaseUrl(value = '') { + const trimmed = String(value || '').trim() || DEFAULT_BASE_URL; + try { + return new URL(trimmed).toString().replace(/\/+$/, ''); + } catch { + return DEFAULT_BASE_URL; + } + } + + function normalizeText(value = '', fallback = '') { + return String(value || '').trim() || fallback; + } + + function normalizeNexSmsCountryId(value, fallback = DEFAULT_COUNTRY_ID) { + const parsed = Math.floor(Number(value)); + if (Number.isFinite(parsed) && parsed >= 0) { + return parsed; + } + const fallbackParsed = Math.floor(Number(fallback)); + if (Number.isFinite(fallbackParsed) && fallbackParsed >= 0) { + return fallbackParsed; + } + return DEFAULT_COUNTRY_ID; + } + + function normalizeNexSmsCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) { + return normalizeText(value, fallback); + } + + function normalizeNexSmsServiceCode(value = '', fallback = DEFAULT_SERVICE_CODE) { + const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); + if (normalized) { + return normalized; + } + const fallbackNormalized = String(fallback || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); + return fallbackNormalized || DEFAULT_SERVICE_CODE; + } + + function normalizeNexSmsCountryOrder(value = []) { + const source = Array.isArray(value) + ? value + : String(value || '') + .split(/[\r\n,,;;]+/) + .map((entry) => String(entry || '').trim()) + .filter(Boolean); + const normalized = []; + const seen = new Set(); + + source.forEach((entry) => { + let id = -1; + let label = ''; + if (entry && typeof entry === 'object' && !Array.isArray(entry)) { + id = normalizeNexSmsCountryId(entry.id ?? entry.countryId, -1); + label = normalizeText(entry.label ?? entry.countryLabel, ''); + } else { + const text = String(entry || '').trim(); + const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/); + id = normalizeNexSmsCountryId(structured?.[1] || text, -1); + label = normalizeText(structured?.[2], ''); + } + if (id < 0 || seen.has(id)) { + return; + } + seen.add(id); + normalized.push({ + id, + label: label || `Country #${id}`, + }); + }); + + return normalized.slice(0, 20); + } + + function resolveCountryCandidates(state = {}) { + const candidates = normalizeNexSmsCountryOrder(state?.nexSmsCountryOrder); + if (candidates.length) { + return candidates; + } + return [{ + id: normalizeNexSmsCountryId(state?.nexSmsCountryId, DEFAULT_COUNTRY_ID), + label: normalizeNexSmsCountryLabel(state?.nexSmsCountryLabel, DEFAULT_COUNTRY_LABEL), + }]; + } + + function parsePayload(text) { + const trimmed = String(text || '').trim(); + if (!trimmed) { + return ''; + } + try { + return JSON.parse(trimmed); + } catch { + return trimmed; + } + } + + function describePayload(raw) { + if (typeof raw === 'string') { + return raw.trim(); + } + if (raw && typeof raw === 'object') { + const message = normalizeText(raw.message || raw.error || raw.msg || raw.statusText, ''); + if (message) { + return message; + } + try { + return JSON.stringify(raw); + } catch { + return String(raw); + } + } + return String(raw || '').trim(); + } + + function isSuccessPayload(payload) { + return Boolean(payload && typeof payload === 'object' && !Array.isArray(payload) && Number(payload.code) === 0); + } + + function resolveConfig(state = {}, deps = {}) { + return { + apiKey: normalizeText(state?.nexSmsApiKey), + baseUrl: normalizeBaseUrl(state?.nexSmsBaseUrl || DEFAULT_BASE_URL), + serviceCode: normalizeNexSmsServiceCode(state?.nexSmsServiceCode, DEFAULT_SERVICE_CODE), + fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null), + requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, + }; + } + + async function fetchPayload(config, path, actionLabel, options = {}) { + if (!config.fetchImpl) { + throw new Error('NexSMS 网络请求实现不可用。'); + } + if (!config.apiKey) { + throw new Error('NexSMS API Key 缺失,请先在侧边栏保存接码 API Key。'); + } + const controller = typeof AbortController === 'function' ? new AbortController() : null; + const timeoutId = controller + ? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS) + : null; + + try { + const method = String(options.method || 'GET').trim().toUpperCase() || 'GET'; + const requestUrl = new URL(path.replace(/^\/+/, ''), `${config.baseUrl.replace(/\/+$/, '')}/`); + requestUrl.searchParams.set('apiKey', config.apiKey); + Object.entries(options.query || {}).forEach(([key, value]) => { + if (value === undefined || value === null || value === '') { + return; + } + requestUrl.searchParams.set(key, String(value)); + }); + const headers = { + Accept: 'application/json', + ...(options.headers && typeof options.headers === 'object' ? options.headers : {}), + }; + const requestInit = { + method, + headers, + signal: controller?.signal, + }; + if (method !== 'GET' && method !== 'HEAD' && options.body !== undefined) { + requestInit.body = typeof options.body === 'string' + ? options.body + : JSON.stringify(options.body); + if (!requestInit.headers['Content-Type']) { + requestInit.headers['Content-Type'] = 'application/json'; + } + } + const response = await config.fetchImpl(requestUrl.toString(), requestInit); + const text = await response.text(); + const payload = parsePayload(text); + if (!response.ok) { + const error = new Error(`${actionLabel}失败:${describePayload(payload) || response.status}`); + error.payload = payload; + error.status = response.status; + throw error; + } + return payload; + } catch (error) { + if (error?.name === 'AbortError') { + throw new Error(`${actionLabel}超时。`); + } + throw error; + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } + } + + async function fetchBalance(state = {}, deps = {}) { + const config = resolveConfig(state, deps); + const payload = await fetchPayload(config, '/api/user/getBalance', 'NexSMS get balance'); + if (!isSuccessPayload(payload)) { + throw new Error(`NexSMS get balance 失败:${describePayload(payload) || 'empty response'}`); + } + const balance = Number(payload?.data?.balance); + return { + balance: Number.isFinite(balance) ? balance : null, + raw: payload, + }; + } + + async function fetchPrices(state = {}, countryConfig = {}, deps = {}) { + const config = resolveConfig(state, deps); + const countryId = normalizeNexSmsCountryId(countryConfig?.id, DEFAULT_COUNTRY_ID); + return fetchPayload(config, '/api/getCountryByService', 'NexSMS getCountryByService', { + query: { + serviceCode: config.serviceCode, + countryId, + }, + }); + } + + function createProvider(deps = {}) { + const providerDeps = { + fetchImpl: deps.fetchImpl, + requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS, + }; + return { + id: PROVIDER_ID, + label: 'NexSMS', + defaultCountryId: DEFAULT_COUNTRY_ID, + defaultCountryLabel: DEFAULT_COUNTRY_LABEL, + defaultProduct: DEFAULT_SERVICE_LABEL, + defaultServiceCode: DEFAULT_SERVICE_CODE, + normalizeCountryId: normalizeNexSmsCountryId, + normalizeCountryLabel: normalizeNexSmsCountryLabel, + normalizeCountryOrder: normalizeNexSmsCountryOrder, + normalizeServiceCode: normalizeNexSmsServiceCode, + resolveCountryCandidates, + fetchBalance: (state) => fetchBalance(state, providerDeps), + fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps), + describePayload, + isSuccessPayload, + }; + } + + return { + PROVIDER_ID, + DEFAULT_BASE_URL, + DEFAULT_COUNTRY_ID, + DEFAULT_COUNTRY_LABEL, + DEFAULT_SERVICE_CODE, + DEFAULT_SERVICE_LABEL, + createProvider, + describePayload, + isSuccessPayload, + normalizeNexSmsCountryId, + normalizeNexSmsCountryLabel, + normalizeNexSmsCountryOrder, + normalizeNexSmsServiceCode, + }; +}); diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 19809da..13855ad 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -1896,6 +1896,7 @@ + diff --git a/tests/phone-sms-provider-registry.test.js b/tests/phone-sms-provider-registry.test.js index 9b4d61a..18666b0 100644 --- a/tests/phone-sms-provider-registry.test.js +++ b/tests/phone-sms-provider-registry.test.js @@ -3,6 +3,7 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const source = fs.readFileSync('phone-sms/providers/registry.js', 'utf8'); +const nexSmsSource = fs.readFileSync('phone-sms/providers/nexsms.js', 'utf8'); function loadRegistry(root = {}) { return new Function('self', `${source}; return self.PhoneSmsProviderRegistry;`)(root); @@ -16,6 +17,9 @@ test('phone sms provider registry normalizes ids, order and labels consistently' PhoneSmsFiveSimProvider: { createProvider: (deps = {}) => ({ provider: '5sim', deps }), }, + PhoneSmsNexSmsProvider: { + createProvider: (deps = {}) => ({ provider: 'nexsms', deps }), + }, PhoneSmsMaDaoProvider: { createProvider: (deps = {}) => ({ provider: 'madao', deps }), }, @@ -52,5 +56,29 @@ test('phone sms provider registry normalizes ids, order and labels consistently' registry.createProvider('madao', { foo: 2 }), { provider: 'madao', deps: { foo: 2 } } ); - assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/); + assert.deepStrictEqual( + registry.createProvider('nexsms', { foo: 3 }), + { provider: 'nexsms', deps: { foo: 3 } } + ); +}); + +test('phone sms provider registry creates the real NexSMS provider module', () => { + const root = {}; + const nexSmsModule = new Function('self', `${nexSmsSource}; return self.PhoneSmsNexSmsProvider;`)(root); + const registry = loadRegistry({ + PhoneSmsNexSmsProvider: nexSmsModule, + }); + + const provider = registry.createProvider('nexsms', { fetchImpl: async () => ({ ok: true, text: async () => '{}' }) }); + + assert.equal(provider.id, 'nexsms'); + assert.equal(provider.label, 'NexSMS'); + assert.equal(provider.defaultServiceCode, 'ot'); + assert.deepStrictEqual( + provider.normalizeCountryOrder(['1:Thailand', { id: 2, label: 'United States' }, '1:Duplicate']), + [ + { id: 1, label: 'Thailand' }, + { id: 2, label: 'United States' }, + ] + ); }); diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 4682467..f2e6852 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -8955,6 +8955,148 @@ test('phone verification helper uses MaDao routing replace when add-phone reject assert.equal(currentState.currentPhoneActivation, null); }); +test('phone verification helper keeps failed MaDao activation separate after routing replace on invalid code', async () => { + const requests = []; + const submittedPhones = []; + const removedReusePhones = []; + let verificationSubmitCount = 0; + let currentState = { + phoneSmsProvider: 'madao', + madaoBaseUrl: 'http://127.0.0.1:7822', + madaoMode: 'routing_plan', + madaoRoutingPlanId: 'rp-openai', + verificationResendCount: 0, + phoneVerificationReplacementLimit: 2, + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 1, + currentPhoneActivation: null, + reusablePhoneActivation: null, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + createMaDaoProvider: maDaoModule.createProvider, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url, options = {}) => { + const parsedUrl = new URL(url); + requests.push({ url: parsedUrl, body: options.body ? JSON.parse(options.body) : null }); + if (parsedUrl.pathname === '/api/acquire') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + ticket_id: 'madao-old', + phone_number: '+441111111111', + service: 'openai', + country: 'GB', + routing_plan_id: 'rp-openai', + routing_item_id: 'route-old', + status: 'waiting_code', + }), + }; + } + if (parsedUrl.pathname === '/api/routing/replace') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + current_ticket_id: 'madao-old', + current_ticket_release: { ticket_id: 'madao-old', status: 'banned' }, + next_ticket: { + ticket_id: 'madao-new', + phone_number: '+442222222222', + service: 'openai', + country: 'GB', + routing_plan_id: 'rp-openai', + routing_item_id: 'route-new', + status: 'waiting_code', + }, + }), + }; + } + if (parsedUrl.pathname === '/api/poll') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + ticket_id: parsedUrl.pathname, + status: 'code_received', + code: requests.filter((entry) => entry.url.pathname === '/api/poll').length === 1 ? '111111' : '222222', + }), + }; + } + if (parsedUrl.pathname === '/api/release') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ ok: true }), + }; + } + throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + sendToContentScriptResilient: async (_source, message) => { + if (message.type === 'SUBMIT_PHONE_NUMBER') { + submittedPhones.push(message.payload.phoneNumber); + return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + verificationSubmitCount += 1; + if (verificationSubmitCount === 1) { + return { + invalidCode: true, + errorText: 'This phone number is already linked to another account', + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + assert.equal(message.payload.code, '222222'); + return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' }; + } + if (message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'STEP8_GET_STATE') { + return { addPhonePage: true, phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone' }; + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + if (Array.isArray(updates.reusablePhoneActivationPool)) { + updates.reusablePhoneActivationPool.forEach((entry) => removedReusePhones.push(entry.phoneNumber)); + } + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const result = await helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }); + + assert.deepStrictEqual(result, { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }); + assert.deepStrictEqual(submittedPhones, ['+441111111111', '+442222222222']); + assert.deepStrictEqual( + requests.map((entry) => entry.url.pathname), + ['/api/acquire', '/api/poll', '/api/routing/replace', '/api/poll', '/api/release'] + ); + assert.deepStrictEqual(requests[2].body, { + ticket_id: 'madao-old', + release_action: 'ban', + failed_item_id: 'route-old', + reason: 'phone_number_used', + }); + assert.deepStrictEqual(requests[4].body, { ticket_id: 'madao-new', action: 'finish' }); + assert.equal(currentState.currentPhoneActivation, null); + assert.equal(removedReusePhones.includes('+442222222222'), false); +}); + test('phone verification helper reacquires MaDao direct numbers after releasing rejected number', async () => { const requests = []; const submittedPhones = []; diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js index 814e0d4..a042622 100644 --- a/tests/sidepanel-phone-verification-settings.test.js +++ b/tests/sidepanel-phone-verification-settings.test.js @@ -88,6 +88,7 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro assert.match(html, /id="select-phone-sms-provider"/); assert.match(html, /\.\.\/phone-sms\/providers\/hero-sms\.js/); assert.match(html, /\.\.\/phone-sms\/providers\/five-sim\.js/); + assert.match(html, /\.\.\/phone-sms\/providers\/nexsms\.js/); assert.match(html, /\.\.\/phone-sms\/providers\/madao\.js/); assert.match(html, /\.\.\/phone-sms\/providers\/registry\.js/); assert.match(html, /