完善 HeroSMS 取号回退与 V2 状态轮询
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
|
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
|
||||||
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
|
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
|
||||||
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
|
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
|
||||||
|
const DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS = 3;
|
||||||
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
||||||
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
|
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
if (!activationId || !phoneNumber) {
|
if (!activationId || !phoneNumber) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const statusAction = String(record.statusAction || '').trim();
|
||||||
return {
|
return {
|
||||||
activationId,
|
activationId,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
countryId: Number(record.countryId) || HERO_SMS_COUNTRY_ID,
|
countryId: Number(record.countryId) || HERO_SMS_COUNTRY_ID,
|
||||||
successfulUses: normalizeUseCount(record.successfulUses),
|
successfulUses: normalizeUseCount(record.successfulUses),
|
||||||
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
|
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
|
||||||
|
...(statusAction ? { statusAction } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +185,10 @@
|
|||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const payload = parseHeroSmsPayload(text);
|
const payload = parseHeroSmsPayload(text);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`${actionLabel} failed: ${describeHeroSmsPayload(payload) || response.status}`);
|
const requestError = new Error(`${actionLabel} failed: ${describeHeroSmsPayload(payload) || response.status}`);
|
||||||
|
requestError.payload = payload;
|
||||||
|
requestError.status = response.status;
|
||||||
|
throw requestError;
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -212,10 +218,15 @@
|
|||||||
const normalizedFallback = normalizeActivation(fallback);
|
const normalizedFallback = normalizeActivation(fallback);
|
||||||
const directActivation = normalizeActivation(payload);
|
const directActivation = normalizeActivation(payload);
|
||||||
if (directActivation) {
|
if (directActivation) {
|
||||||
|
const statusAction = normalizedFallback?.statusAction || directActivation.statusAction;
|
||||||
return {
|
return {
|
||||||
...directActivation,
|
...directActivation,
|
||||||
|
provider: normalizedFallback?.provider || directActivation.provider,
|
||||||
|
serviceCode: normalizedFallback?.serviceCode || directActivation.serviceCode,
|
||||||
|
countryId: normalizedFallback?.countryId || directActivation.countryId,
|
||||||
successfulUses: normalizedFallback?.successfulUses || directActivation.successfulUses,
|
successfulUses: normalizedFallback?.successfulUses || directActivation.successfulUses,
|
||||||
maxUses: normalizedFallback?.maxUses || directActivation.maxUses,
|
maxUses: normalizedFallback?.maxUses || directActivation.maxUses,
|
||||||
|
...(statusAction ? { statusAction } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +241,7 @@
|
|||||||
countryId: normalizedFallback?.countryId || HERO_SMS_COUNTRY_ID,
|
countryId: normalizedFallback?.countryId || HERO_SMS_COUNTRY_ID,
|
||||||
successfulUses: normalizedFallback?.successfulUses || 0,
|
successfulUses: normalizedFallback?.successfulUses || 0,
|
||||||
maxUses: normalizedFallback?.maxUses || DEFAULT_PHONE_NUMBER_MAX_USES,
|
maxUses: normalizedFallback?.maxUses || DEFAULT_PHONE_NUMBER_MAX_USES,
|
||||||
|
...(normalizedFallback?.statusAction ? { statusAction: normalizedFallback.statusAction } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,21 +252,178 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveActivationStatusAction(activation) {
|
||||||
|
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeroSmsPrice(value) {
|
||||||
|
const price = Number(value);
|
||||||
|
if (!Number.isFinite(price) || price < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHeroSmsPriceCandidates(payload, candidates = []) {
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
payload.forEach((entry) => collectHeroSmsPriceCandidates(entry, candidates));
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cost = normalizeHeroSmsPrice(payload.cost);
|
||||||
|
if (cost !== null) {
|
||||||
|
const count = Number(payload.count);
|
||||||
|
const physicalCount = Number(payload.physicalCount);
|
||||||
|
const hasCount = Number.isFinite(count);
|
||||||
|
const hasPhysicalCount = Number.isFinite(physicalCount);
|
||||||
|
if ((!hasCount && !hasPhysicalCount) || count > 0 || physicalCount > 0) {
|
||||||
|
candidates.push(cost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(payload).forEach((value) => collectHeroSmsPriceCandidates(value, candidates));
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLowestHeroSmsPrice(payload) {
|
||||||
|
const candidates = collectHeroSmsPriceCandidates(payload, []);
|
||||||
|
if (!candidates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.min(...candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHeroSmsNoNumbersPayload(payload) {
|
||||||
|
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHeroSmsWrongMaxPrice(payload) {
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
const title = String(payload.title || '').trim();
|
||||||
|
const minPrice = normalizeHeroSmsPrice(payload.info?.min);
|
||||||
|
if (/^WRONG_MAX_PRICE$/i.test(title) && minPrice !== null) {
|
||||||
|
return minPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = describeHeroSmsPayload(payload);
|
||||||
|
const match = text.match(/\bWRONG_MAX_PRICE:(\d+(?:\.\d+)?)\b/i);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeHeroSmsPrice(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNetworkFetchFailure(error) {
|
||||||
|
const message = String(error?.message || '').trim();
|
||||||
|
return /failed to fetch|networkerror|load failed/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCheapestPhoneActivationPrice(config, countryConfig) {
|
||||||
|
for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const payload = await fetchHeroSmsPayload(config, {
|
||||||
|
action: 'getPrices',
|
||||||
|
service: HERO_SMS_SERVICE_CODE,
|
||||||
|
country: countryConfig.id,
|
||||||
|
}, 'HeroSMS getPrices');
|
||||||
|
const price = findLowestHeroSmsPrice(payload);
|
||||||
|
if (price !== null) {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort lookup only.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
|
||||||
|
const query = {
|
||||||
|
action,
|
||||||
|
service: HERO_SMS_SERVICE_CODE,
|
||||||
|
country: countryConfig.id,
|
||||||
|
};
|
||||||
|
if (options.maxPrice !== null && options.maxPrice !== undefined) {
|
||||||
|
query.maxPrice = options.maxPrice;
|
||||||
|
query.fixedPrice = 'true';
|
||||||
|
}
|
||||||
|
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestPhoneActivationWithPrice(config, countryConfig, action, maxPrice) {
|
||||||
|
let nextMaxPrice = maxPrice;
|
||||||
|
let retriedWithUpdatedPrice = false;
|
||||||
|
let retriedWithoutPrice = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await fetchPhoneActivationPayload(config, countryConfig, action, {
|
||||||
|
maxPrice: nextMaxPrice,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const updatedMaxPrice = extractHeroSmsWrongMaxPrice(error?.payload || error?.message);
|
||||||
|
if (
|
||||||
|
nextMaxPrice !== null
|
||||||
|
&& nextMaxPrice !== undefined
|
||||||
|
&& !retriedWithUpdatedPrice
|
||||||
|
&& updatedMaxPrice !== null
|
||||||
|
) {
|
||||||
|
nextMaxPrice = updatedMaxPrice;
|
||||||
|
retriedWithUpdatedPrice = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextMaxPrice !== null
|
||||||
|
&& nextMaxPrice !== undefined
|
||||||
|
&& !retriedWithoutPrice
|
||||||
|
&& isNetworkFetchFailure(error)
|
||||||
|
) {
|
||||||
|
nextMaxPrice = null;
|
||||||
|
retriedWithoutPrice = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function requestPhoneActivation(state = {}) {
|
async function requestPhoneActivation(state = {}) {
|
||||||
const config = resolvePhoneConfig(state);
|
const config = resolvePhoneConfig(state);
|
||||||
const countryConfig = resolveCountryConfig(state);
|
const countryConfig = resolveCountryConfig(state);
|
||||||
const payload = await fetchHeroSmsPayload(config, {
|
const maxPrice = await resolveCheapestPhoneActivationPrice(config, countryConfig);
|
||||||
action: 'getNumber',
|
const buildFallbackActivation = (requestAction) => ({
|
||||||
service: HERO_SMS_SERVICE_CODE,
|
|
||||||
country: countryConfig.id,
|
|
||||||
}, 'HeroSMS getNumber');
|
|
||||||
|
|
||||||
const activation = parseActivationPayload(payload, {
|
|
||||||
countryId: countryConfig.id,
|
countryId: countryConfig.id,
|
||||||
|
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
|
||||||
});
|
});
|
||||||
|
let requestAction = 'getNumber';
|
||||||
|
let payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
|
||||||
|
} catch (error) {
|
||||||
|
if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
requestAction = 'getNumberV2';
|
||||||
|
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
|
||||||
|
if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) {
|
||||||
|
requestAction = 'getNumberV2';
|
||||||
|
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
|
||||||
|
activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
|
||||||
|
}
|
||||||
|
|
||||||
if (!activation) {
|
if (!activation) {
|
||||||
const text = describeHeroSmsPayload(payload);
|
const text = describeHeroSmsPayload(payload);
|
||||||
throw new Error(`HeroSMS getNumber failed: ${text || 'empty response'}`);
|
throw new Error(`HeroSMS ${requestAction} failed: ${text || 'empty response'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return activation;
|
return activation;
|
||||||
@@ -318,6 +487,7 @@
|
|||||||
if (!normalizedActivation) {
|
if (!normalizedActivation) {
|
||||||
throw new Error('Phone activation is missing.');
|
throw new Error('Phone activation is missing.');
|
||||||
}
|
}
|
||||||
|
const statusAction = resolveActivationStatusAction(normalizedActivation);
|
||||||
|
|
||||||
const config = resolvePhoneConfig(state);
|
const config = resolvePhoneConfig(state);
|
||||||
const configuredTimeoutMs = Math.max(1000, Number(options.timeoutMs) || 0);
|
const configuredTimeoutMs = Math.max(1000, Number(options.timeoutMs) || 0);
|
||||||
@@ -337,9 +507,9 @@
|
|||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const payload = await fetchHeroSmsPayload(config, {
|
const payload = await fetchHeroSmsPayload(config, {
|
||||||
action: 'getStatus',
|
action: statusAction,
|
||||||
id: normalizedActivation.activationId,
|
id: normalizedActivation.activationId,
|
||||||
}, 'HeroSMS getStatus');
|
}, `HeroSMS ${statusAction}`);
|
||||||
const text = describeHeroSmsPayload(payload);
|
const text = describeHeroSmsPayload(payload);
|
||||||
lastResponse = text;
|
lastResponse = text;
|
||||||
pollCount += 1;
|
pollCount += 1;
|
||||||
@@ -354,6 +524,28 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractVerificationCode = (rawCode) => {
|
||||||
|
const trimmed = String(rawCode || '').trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const digitMatch = trimmed.match(/\b(\d{4,8})\b/);
|
||||||
|
return digitMatch?.[1] || trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const v2Code = (
|
||||||
|
payload
|
||||||
|
&& typeof payload === 'object'
|
||||||
|
&& !Array.isArray(payload)
|
||||||
|
&& (
|
||||||
|
extractVerificationCode(payload.sms?.code)
|
||||||
|
|| extractVerificationCode(payload.call?.code)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (v2Code) {
|
||||||
|
return v2Code;
|
||||||
|
}
|
||||||
|
|
||||||
const okMatch = text.match(/^STATUS_OK:(.+)$/i);
|
const okMatch = text.match(/^STATUS_OK:(.+)$/i);
|
||||||
if (okMatch) {
|
if (okMatch) {
|
||||||
const rawCode = String(okMatch[1] || '').trim();
|
const rawCode = String(okMatch[1] || '').trim();
|
||||||
@@ -366,11 +558,16 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusAction === 'getStatusV2' && payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||||
|
await sleepWithStop(intervalMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (/^STATUS_CANCEL$/i.test(text)) {
|
if (/^STATUS_CANCEL$/i.test(text)) {
|
||||||
throw new Error('HeroSMS activation was cancelled before the SMS arrived.');
|
throw new Error('HeroSMS activation was cancelled before the SMS arrived.');
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`HeroSMS getStatus failed: ${text || 'empty response'}`);
|
throw new Error(`HeroSMS ${statusAction} failed: ${text || 'empty response'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw buildPhoneCodeTimeoutError(lastResponse);
|
throw buildPhoneCodeTimeoutError(lastResponse);
|
||||||
|
|||||||
@@ -6,13 +6,47 @@ const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
|
|||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
|
||||||
|
|
||||||
|
function buildHeroSmsPricesPayload({ country = '52', service = 'dr', cost = 0.08, count = 25370, physicalCount = 14528 } = {}) {
|
||||||
|
return JSON.stringify({
|
||||||
|
[country]: {
|
||||||
|
[service]: {
|
||||||
|
cost,
|
||||||
|
count,
|
||||||
|
physicalCount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHeroSmsStatusV2Payload({ smsCode = '', smsText = '', callCode = '' } = {}) {
|
||||||
|
return JSON.stringify({
|
||||||
|
verificationType: 2,
|
||||||
|
sms: {
|
||||||
|
dateTime: '2026-02-18T16:11:33+00:00',
|
||||||
|
code: smsCode,
|
||||||
|
text: smsText,
|
||||||
|
},
|
||||||
|
call: {
|
||||||
|
code: callCode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('phone verification helper requests HeroSMS numbers with fixed OpenAI and Thailand parameters', async () => {
|
test('phone verification helper requests HeroSMS numbers with fixed OpenAI and Thailand parameters', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
ensureStep8SignupPageReady: async () => {},
|
ensureStep8SignupPageReady: async () => {},
|
||||||
fetchImpl: async (url) => {
|
fetchImpl: async (url) => {
|
||||||
requests.push(new URL(url));
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload(),
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||||
@@ -36,11 +70,428 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
|
|||||||
successfulUses: 0,
|
successfulUses: 0,
|
||||||
maxUses: 3,
|
maxUses: 3,
|
||||||
});
|
});
|
||||||
assert.equal(requests.length, 1);
|
assert.equal(requests.length, 2);
|
||||||
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
assert.equal(requests[0].searchParams.get('service'), 'dr');
|
assert.equal(requests[0].searchParams.get('service'), 'dr');
|
||||||
assert.equal(requests[0].searchParams.get('country'), '52');
|
assert.equal(requests[0].searchParams.get('country'), '52');
|
||||||
assert.equal(requests[0].searchParams.get('api_key'), 'demo-key');
|
assert.equal(requests[0].searchParams.get('api_key'), 'demo-key');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
|
||||||
|
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
|
||||||
|
assert.equal(requests[1].searchParams.get('service'), 'dr');
|
||||||
|
assert.equal(requests[1].searchParams.get('country'), '52');
|
||||||
|
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper retries HeroSMS getPrices until it receives a usable lowest price', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let getPricesAttempt = 0;
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
getPricesAttempt += 1;
|
||||||
|
return getPricesAttempt < 3
|
||||||
|
? {
|
||||||
|
ok: true,
|
||||||
|
text: async () => JSON.stringify({ unavailable: true }),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload({ cost: 0.09 }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
|
||||||
|
|
||||||
|
assert.equal(requests.length, 4);
|
||||||
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[2].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[3].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[3].searchParams.get('maxPrice'), '0.09');
|
||||||
|
assert.equal(requests[3].searchParams.get('fixedPrice'), 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper falls back to plain getNumber only after HeroSMS getPrices fails three times', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let getPricesAttempt = 0;
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
getPricesAttempt += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => JSON.stringify({ unavailable: getPricesAttempt }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
|
||||||
|
|
||||||
|
assert.equal(requests.length, 4);
|
||||||
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[2].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[2].searchParams.get('service'), 'dr');
|
||||||
|
assert.equal(requests[2].searchParams.get('country'), '52');
|
||||||
|
assert.equal(requests[2].searchParams.get('api_key'), 'demo-key');
|
||||||
|
assert.equal(requests[3].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[3].searchParams.get('maxPrice'), null);
|
||||||
|
assert.equal(requests[3].searchParams.get('fixedPrice'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper retries with HeroSMS getNumberV2 when getNumber reports NO_NUMBERS', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'NO_NUMBERS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumberV2') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
activationId: '654321',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsCountryId: 16 }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsCountryId: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(activation, {
|
||||||
|
activationId: '654321',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 16,
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
statusAction: 'getStatusV2',
|
||||||
|
});
|
||||||
|
assert.equal(requests.length, 3);
|
||||||
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[0].searchParams.get('country'), '16');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[1].searchParams.get('country'), '16');
|
||||||
|
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
|
||||||
|
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
|
||||||
|
assert.equal(requests[2].searchParams.get('action'), 'getNumberV2');
|
||||||
|
assert.equal(requests[2].searchParams.get('country'), '16');
|
||||||
|
assert.equal(requests[2].searchParams.get('maxPrice'), '0.08');
|
||||||
|
assert.equal(requests[2].searchParams.get('fixedPrice'), 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper uses HeroSMS getStatusV2 after acquiring a number via getNumberV2', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const stateUpdates = [];
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
heroSmsCountryId: 16,
|
||||||
|
heroSmsCountryLabel: 'United Kingdom',
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
reusablePhoneActivation: null,
|
||||||
|
};
|
||||||
|
let statusPollCount = 0;
|
||||||
|
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'NO_NUMBERS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumberV2') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
activationId: '654321',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getStatusV2') {
|
||||||
|
statusPollCount += 1;
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => (
|
||||||
|
statusPollCount === 1
|
||||||
|
? buildHeroSmsStatusV2Payload()
|
||||||
|
: buildHeroSmsStatusV2Payload({ smsCode: '112233', smsText: 'Your code is 112233' })
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'setStatus') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_ACTIVATION',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return {
|
||||||
|
phoneVerificationPage: true,
|
||||||
|
url: 'https://auth.openai.com/phone-verification',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
stateUpdates.push(updates);
|
||||||
|
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(stateUpdates, [
|
||||||
|
{
|
||||||
|
currentPhoneActivation: {
|
||||||
|
activationId: '654321',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 16,
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
statusAction: 'getStatusV2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reusablePhoneActivation: {
|
||||||
|
activationId: '654321',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 16,
|
||||||
|
successfulUses: 1,
|
||||||
|
maxUses: 3,
|
||||||
|
statusAction: 'getStatusV2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const actions = requests.map((url) => url.searchParams.get('action'));
|
||||||
|
assert.deepStrictEqual(actions, [
|
||||||
|
'getPrices',
|
||||||
|
'getNumber',
|
||||||
|
'getNumberV2',
|
||||||
|
'getStatusV2',
|
||||||
|
'getStatusV2',
|
||||||
|
'setStatus',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper refreshes maxPrice when HeroSMS returns WRONG_MAX_PRICE', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let getNumberAttempt = 0;
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
getNumberAttempt += 1;
|
||||||
|
return getNumberAttempt === 1
|
||||||
|
? {
|
||||||
|
ok: false,
|
||||||
|
text: async () => 'WRONG_MAX_PRICE:0.09',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
|
||||||
|
|
||||||
|
assert.deepStrictEqual(activation, {
|
||||||
|
activationId: '123456',
|
||||||
|
phoneNumber: '66959916439',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
});
|
||||||
|
assert.equal(requests.length, 3);
|
||||||
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
|
||||||
|
assert.equal(requests[2].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[2].searchParams.get('maxPrice'), '0.09');
|
||||||
|
assert.equal(requests[2].searchParams.get('fixedPrice'), 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('phone verification helper falls back to plain getNumber when priced request fails to fetch', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let getNumberAttempt = 0;
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getNumber') {
|
||||||
|
getNumberAttempt += 1;
|
||||||
|
if (getNumberAttempt === 1) {
|
||||||
|
throw new TypeError('Failed to fetch');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
|
||||||
|
|
||||||
|
assert.deepStrictEqual(activation, {
|
||||||
|
activationId: '123456',
|
||||||
|
phoneNumber: '66959916439',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 52,
|
||||||
|
successfulUses: 0,
|
||||||
|
maxUses: 3,
|
||||||
|
});
|
||||||
|
assert.equal(requests.length, 3);
|
||||||
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
|
||||||
|
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
|
||||||
|
assert.equal(requests[2].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[2].searchParams.get('maxPrice'), null);
|
||||||
|
assert.equal(requests[2].searchParams.get('fixedPrice'), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => {
|
test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => {
|
||||||
@@ -60,6 +511,12 @@ test('phone verification helper completes add-phone flow, clears current activat
|
|||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
requests.push(parsedUrl);
|
requests.push(parsedUrl);
|
||||||
const action = parsedUrl.searchParams.get('action');
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload(),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (action === 'getNumber') {
|
if (action === 'getNumber') {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -146,7 +603,7 @@ test('phone verification helper completes add-phone flow, clears current activat
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const actions = requests.map((url) => url.searchParams.get('action'));
|
const actions = requests.map((url) => url.searchParams.get('action'));
|
||||||
assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
|
assert.deepStrictEqual(actions, ['getPrices', 'getNumber', 'getStatus', 'setStatus']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('phone verification helper uses the configured HeroSMS country for both number acquisition and add-phone submission', async () => {
|
test('phone verification helper uses the configured HeroSMS country for both number acquisition and add-phone submission', async () => {
|
||||||
@@ -168,6 +625,12 @@ test('phone verification helper uses the configured HeroSMS country for both num
|
|||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
requests.push(parsedUrl);
|
requests.push(parsedUrl);
|
||||||
const action = parsedUrl.searchParams.get('action');
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (action === 'getNumber') {
|
if (action === 'getNumber') {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -225,8 +688,12 @@ test('phone verification helper uses the configured HeroSMS country for both num
|
|||||||
consentReady: true,
|
consentReady: true,
|
||||||
url: 'https://auth.openai.com/authorize',
|
url: 'https://auth.openai.com/authorize',
|
||||||
});
|
});
|
||||||
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
|
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
|
||||||
assert.equal(requests[0].searchParams.get('country'), '16');
|
assert.equal(requests[0].searchParams.get('country'), '16');
|
||||||
|
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
|
||||||
|
assert.equal(requests[1].searchParams.get('country'), '16');
|
||||||
|
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
|
||||||
|
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
|
||||||
assert.deepStrictEqual(submittedPayloads, [{
|
assert.deepStrictEqual(submittedPayloads, [{
|
||||||
phoneNumber: '447911123456',
|
phoneNumber: '447911123456',
|
||||||
countryId: 16,
|
countryId: 16,
|
||||||
@@ -258,6 +725,13 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
|
|||||||
const action = parsedUrl.searchParams.get('action');
|
const action = parsedUrl.searchParams.get('action');
|
||||||
const id = parsedUrl.searchParams.get('id');
|
const id = parsedUrl.searchParams.get('id');
|
||||||
|
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'getNumber') {
|
if (action === 'getNumber') {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -325,6 +799,7 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
|
|||||||
|
|
||||||
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
|
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
|
||||||
assert.deepStrictEqual(actions, [
|
assert.deepStrictEqual(actions, [
|
||||||
|
'getPrices:',
|
||||||
'getNumber:',
|
'getNumber:',
|
||||||
'getStatus:123456',
|
'getStatus:123456',
|
||||||
'setStatus:123456',
|
'setStatus:123456',
|
||||||
@@ -363,6 +838,13 @@ test('phone verification helper replaces the number when code submission returns
|
|||||||
const action = parsedUrl.searchParams.get('action');
|
const action = parsedUrl.searchParams.get('action');
|
||||||
const id = parsedUrl.searchParams.get('id');
|
const id = parsedUrl.searchParams.get('id');
|
||||||
|
|
||||||
|
if (action === 'getPrices') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsPricesPayload(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'getNumber') {
|
if (action === 'getNumber') {
|
||||||
const nextNumber = numbers[numberIndex];
|
const nextNumber = numbers[numberIndex];
|
||||||
numberIndex += 1;
|
numberIndex += 1;
|
||||||
@@ -443,9 +925,11 @@ test('phone verification helper replaces the number when code submission returns
|
|||||||
|
|
||||||
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
|
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
|
||||||
assert.deepStrictEqual(actions, [
|
assert.deepStrictEqual(actions, [
|
||||||
|
'getPrices:',
|
||||||
'getNumber:',
|
'getNumber:',
|
||||||
'getStatus:111111',
|
'getStatus:111111',
|
||||||
'setStatus:111111',
|
'setStatus:111111',
|
||||||
|
'getPrices:',
|
||||||
'getNumber:',
|
'getNumber:',
|
||||||
'getStatus:222222',
|
'getStatus:222222',
|
||||||
'setStatus:222222',
|
'setStatus:222222',
|
||||||
@@ -549,3 +1033,92 @@ test('phone verification helper reuses the same number up to three successful re
|
|||||||
assert.equal(requests[0].searchParams.get('id'), '123456');
|
assert.equal(requests[0].searchParams.get('id'), '123456');
|
||||||
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper keeps maxUses behavior for reused V2 activations', async () => {
|
||||||
|
const requests = [];
|
||||||
|
let currentState = {
|
||||||
|
heroSmsApiKey: 'demo-key',
|
||||||
|
verificationResendCount: 0,
|
||||||
|
currentPhoneActivation: null,
|
||||||
|
reusablePhoneActivation: {
|
||||||
|
activationId: '123456',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
provider: 'hero-sms',
|
||||||
|
serviceCode: 'dr',
|
||||||
|
countryId: 16,
|
||||||
|
successfulUses: 2,
|
||||||
|
maxUses: 3,
|
||||||
|
statusAction: 'getStatusV2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
requests.push(parsedUrl);
|
||||||
|
const action = parsedUrl.searchParams.get('action');
|
||||||
|
if (action === 'reactivate') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
activationId: '222333',
|
||||||
|
phoneNumber: '447911123456',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'getStatusV2') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => buildHeroSmsStatusV2Payload({ smsCode: '654321', smsText: 'Your code is 654321' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action === 'setStatus') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
text: async () => 'ACCESS_ACTIVATION',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||||
|
},
|
||||||
|
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||||
|
getState: async () => ({ ...currentState }),
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||||
|
return {
|
||||||
|
phoneVerificationPage: true,
|
||||||
|
url: 'https://auth.openai.com/phone-verification',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/authorize',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (updates) => {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
const actions = requests.map((url) => url.searchParams.get('action'));
|
||||||
|
assert.deepStrictEqual(actions, ['reactivate', 'getStatusV2', 'setStatus']);
|
||||||
|
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user