fix: harden GoPay and 5sim PR paths
This commit is contained in:
+4
-3
@@ -84,6 +84,7 @@ const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS
|
|||||||
.map((definition) => Number(definition?.id))
|
.map((definition) => Number(definition?.id))
|
||||||
.filter(Number.isFinite)
|
.filter(Number.isFinite)
|
||||||
.sort((left, right) => left - right);
|
.sort((left, right) => left - right);
|
||||||
|
const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS;
|
||||||
const LAST_STEP_ID = Math.max(
|
const LAST_STEP_ID = Math.max(
|
||||||
NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10,
|
NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10,
|
||||||
PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10,
|
PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10,
|
||||||
@@ -445,7 +446,7 @@ function getStepDefinitionsForState(state = {}) {
|
|||||||
if (!isPlusModeState(state)) {
|
if (!isPlusModeState(state)) {
|
||||||
return NORMAL_STEP_DEFINITIONS;
|
return NORMAL_STEP_DEFINITIONS;
|
||||||
}
|
}
|
||||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
|
||||||
? PLUS_GOPAY_STEP_DEFINITIONS
|
? PLUS_GOPAY_STEP_DEFINITIONS
|
||||||
: PLUS_PAYPAL_STEP_DEFINITIONS;
|
: PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||||
}
|
}
|
||||||
@@ -454,7 +455,7 @@ function getStepIdsForState(state = {}) {
|
|||||||
if (!isPlusModeState(state)) {
|
if (!isPlusModeState(state)) {
|
||||||
return NORMAL_STEP_IDS;
|
return NORMAL_STEP_IDS;
|
||||||
}
|
}
|
||||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
|
||||||
? PLUS_GOPAY_STEP_IDS
|
? PLUS_GOPAY_STEP_IDS
|
||||||
: PLUS_PAYPAL_STEP_IDS;
|
: PLUS_PAYPAL_STEP_IDS;
|
||||||
}
|
}
|
||||||
@@ -10054,7 +10055,7 @@ function getStepRegistryForState(state = {}) {
|
|||||||
if (!isPlusModeState(state)) {
|
if (!isPlusModeState(state)) {
|
||||||
return normalStepRegistry;
|
return normalStepRegistry;
|
||||||
}
|
}
|
||||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
|
||||||
? plusGoPayStepRegistry
|
? plusGoPayStepRegistry
|
||||||
: plusPayPalStepRegistry;
|
: plusPayPalStepRegistry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,13 @@
|
|||||||
return Math.max(500, Math.min(30000, parsed));
|
return Math.max(500, Math.min(30000, parsed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit) {
|
||||||
|
const normalizedOperator = normalizeFiveSimCountryCode(operator, DEFAULT_FIVE_SIM_OPERATOR);
|
||||||
|
if (maxPriceLimit !== null && maxPriceLimit !== undefined && normalizedOperator !== DEFAULT_FIVE_SIM_OPERATOR) {
|
||||||
|
throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeHeroSmsPriceLimit(value) {
|
function normalizeHeroSmsPriceLimit(value) {
|
||||||
if (value === undefined || value === null || String(value).trim() === '') {
|
if (value === undefined || value === null || String(value).trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
@@ -1381,16 +1388,19 @@
|
|||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('5sim API key is missing. Save it in the side panel before running the phone flow.');
|
throw new Error('5sim API key is missing. Save it in the side panel before running the phone flow.');
|
||||||
}
|
}
|
||||||
const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice);
|
const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice);
|
||||||
|
const operator = normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR);
|
||||||
|
const maxPriceLimit = configuredMaxPrice !== null
|
||||||
|
? configuredMaxPrice
|
||||||
|
: normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice);
|
||||||
|
assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit);
|
||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
apiKey,
|
apiKey,
|
||||||
baseUrl: normalizeUrl(state.fiveSimBaseUrl, DEFAULT_FIVE_SIM_BASE_URL).replace(/\/+$/, ''),
|
baseUrl: normalizeUrl(state.fiveSimBaseUrl, DEFAULT_FIVE_SIM_BASE_URL).replace(/\/+$/, ''),
|
||||||
operator: normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR),
|
operator,
|
||||||
product: normalizeFiveSimCountryCode(state.fiveSimProduct, DEFAULT_FIVE_SIM_PRODUCT),
|
product: normalizeFiveSimCountryCode(state.fiveSimProduct, DEFAULT_FIVE_SIM_PRODUCT),
|
||||||
maxPriceLimit: configuredMaxPrice !== null
|
maxPriceLimit,
|
||||||
? configuredMaxPrice
|
|
||||||
: normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice),
|
|
||||||
countryCandidates: resolveFiveSimCountryCandidates(state),
|
countryCandidates: resolveFiveSimCountryCandidates(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -910,13 +910,16 @@
|
|||||||
const target = Number.isInteger(frameId)
|
const target = Number.isInteger(frameId)
|
||||||
? await sendGoPayFrameCommand(tabId, frameId, targetMessageType, {})
|
? await sendGoPayFrameCommand(tabId, frameId, targetMessageType, {})
|
||||||
: await sendGoPayCommand(tabId, targetMessageType, {});
|
: await sendGoPayCommand(tabId, targetMessageType, {});
|
||||||
const rect = target?.rect || null;
|
const rect = target?.rect || null;
|
||||||
if (!target?.found || !rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
|
if (!target?.found || !rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) {
|
||||||
return { clicked: false, reason: 'target_not_found', clickTarget: target?.target || '' };
|
return { clicked: false, reason: 'target_not_found', clickTarget: target?.target || '' };
|
||||||
}
|
}
|
||||||
await clickWithDebugger(tabId, rect);
|
if (Number.isInteger(frameId)) {
|
||||||
return { clicked: true, clickTarget: target.target || '' };
|
return { clicked: false, reason: 'debugger_click_skipped_for_frame_target', clickTarget: target.target || '' };
|
||||||
}
|
}
|
||||||
|
await clickWithDebugger(tabId, rect);
|
||||||
|
return { clicked: true, clickTarget: target.target || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function clickGoPayContinueWithDebugger(tabId, frameId = null) {
|
async function clickGoPayContinueWithDebugger(tabId, frameId = null) {
|
||||||
|
|||||||
@@ -569,14 +569,22 @@
|
|||||||
return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`);
|
return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTerminalError(payloadOrMessage) {
|
function isTerminalError(payloadOrMessage) {
|
||||||
const text = describePayload(payloadOrMessage);
|
const text = describePayload(payloadOrMessage);
|
||||||
return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text);
|
return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) {
|
function assertMaxPriceCompatibleWithOperator(state = {}) {
|
||||||
const config = resolveConfig(state, deps);
|
const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice);
|
||||||
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||||
|
if (maxPrice && operator !== DEFAULT_OPERATOR) {
|
||||||
|
throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) {
|
||||||
|
const config = resolveConfig(state, deps);
|
||||||
|
const operator = normalizeFiveSimOperator(state.fiveSimOperator);
|
||||||
const query = {};
|
const query = {};
|
||||||
if (maxPrice !== null && maxPrice !== undefined) {
|
if (maxPrice !== null && maxPrice !== undefined) {
|
||||||
query.maxPrice = maxPrice;
|
query.maxPrice = maxPrice;
|
||||||
@@ -605,9 +613,11 @@
|
|||||||
return activation;
|
return activation;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestActivation(state = {}, options = {}, deps = {}) {
|
async function requestActivation(state = {}, options = {}, deps = {}) {
|
||||||
const allCountryCandidates = resolveCountryCandidates(state);
|
assertMaxPriceCompatibleWithOperator(state);
|
||||||
const blockedCountryIds = new Set(
|
|
||||||
|
const allCountryCandidates = resolveCountryCandidates(state);
|
||||||
|
const blockedCountryIds = new Set(
|
||||||
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
|
(Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : [])
|
||||||
.map((value) => normalizeFiveSimCountryId(value, ''))
|
.map((value) => normalizeFiveSimCountryId(value, ''))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ test('background imports step registry and shared step definitions', () => {
|
|||||||
assert.match(source, /data\/step-definitions\.js/);
|
assert.match(source, /data\/step-definitions\.js/);
|
||||||
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
|
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
|
||||||
assert.match(source, /getStepRegistryForState\(state\)/);
|
assert.match(source, /getStepRegistryForState\(state\)/);
|
||||||
|
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
|
||||||
|
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
|
||||||
|
assert.match(source, /plusPayPalStepRegistry/);
|
||||||
|
assert.match(source, /plusGoPayStepRegistry/);
|
||||||
|
assert.match(source, /normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\) === PLUS_PAYMENT_METHOD_GOPAY/);
|
||||||
assert.match(source, /activeStepRegistry\.executeStep\(step,\s*\{/);
|
assert.match(source, /activeStepRegistry\.executeStep\(step,\s*\{/);
|
||||||
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
|
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
|
||||||
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
|
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
|
||||||
|
|||||||
@@ -127,9 +127,9 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('5sim provider prefers buy-compatible products price over operator detail price', async () => {
|
test('5sim provider prefers buy-compatible products price over operator detail price', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const provider = api.createProvider({
|
const provider = api.createProvider({
|
||||||
fetchImpl: async (url, options = {}) => {
|
fetchImpl: async (url, options = {}) => {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
requests.push({ url: parsed, options });
|
requests.push({ url: parsed, options });
|
||||||
@@ -172,11 +172,35 @@ test('5sim provider prefers buy-compatible products price over operator detail p
|
|||||||
'/v1/guest/prices',
|
'/v1/guest/prices',
|
||||||
'/v1/user/buy/activation/vietnam/any/openai',
|
'/v1/user/buy/activation/vietnam/any/openai',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('5sim provider reports raw buy payload when HTTP 200 response has no activation', async () => {
|
test('5sim provider rejects maxPrice with custom operator before buying', async () => {
|
||||||
const provider = api.createProvider({
|
const requests = [];
|
||||||
|
const provider = api.createProvider({
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
requests.push(url);
|
||||||
|
throw new Error(`unexpected request ${url}`);
|
||||||
|
},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => provider.requestActivation({
|
||||||
|
fiveSimApiKey: 'demo-key',
|
||||||
|
fiveSimCountryId: 'vietnam',
|
||||||
|
fiveSimCountryLabel: '瓒婂崡 (Vietnam)',
|
||||||
|
fiveSimMaxPrice: '12',
|
||||||
|
fiveSimOperator: 'virtual21',
|
||||||
|
}),
|
||||||
|
/maxPrice only works when operator is "any"/
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(requests, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('5sim provider reports raw buy payload when HTTP 200 response has no activation', async () => {
|
||||||
|
const provider = api.createProvider({
|
||||||
fetchImpl: async (url) => {
|
fetchImpl: async (url) => {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
|
||||||
|
|||||||
@@ -69,7 +69,16 @@ test('GoPay approve handles final payment details iframe as an action frame', ()
|
|||||||
assert.match(source, /最终 Bayar 确认/);
|
assert.match(source, /最终 Bayar 确认/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GoPay approve treats merchant validate-pin iframe as PIN entry frame', () => {
|
test('GoPay debugger click does not reuse iframe-relative rects as top-level coordinates', () => {
|
||||||
|
const body = extractFunction('clickGoPayTargetWithDebugger');
|
||||||
|
assert.match(body, /Number\.isInteger\(frameId\)/);
|
||||||
|
assert.match(body, /debugger_click_skipped_for_frame_target/);
|
||||||
|
assert.ok(
|
||||||
|
body.indexOf('debugger_click_skipped_for_frame_target') < body.indexOf('clickWithDebugger(tabId, rect)')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GoPay approve treats merchant validate-pin iframe as PIN entry frame', () => {
|
||||||
assert.match(source, /GOPAY_PIN_FRAME_URL_PATTERN/);
|
assert.match(source, /GOPAY_PIN_FRAME_URL_PATTERN/);
|
||||||
assert.match(source, /payment\\\/validate-pin/);
|
assert.match(source, /payment\\\/validate-pin/);
|
||||||
assert.match(source, /kind: 'pin'/);
|
assert.match(source, /kind: 'pin'/);
|
||||||
|
|||||||
@@ -969,6 +969,43 @@ test('phone verification helper acquires a number from 5sim with fallback countr
|
|||||||
assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai');
|
assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => {
|
||||||
|
const requests = [];
|
||||||
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
addLog: async () => {},
|
||||||
|
ensureStep8SignupPageReady: async () => {},
|
||||||
|
fetchImpl: async (url) => {
|
||||||
|
requests.push(url);
|
||||||
|
throw new Error(`Unexpected 5sim request: ${url}`);
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
phoneSmsProvider: '5sim',
|
||||||
|
fiveSimApiKey: 'five-token',
|
||||||
|
fiveSimCountryOrder: ['vietnam'],
|
||||||
|
fiveSimOperator: 'virtual21',
|
||||||
|
fiveSimMaxPrice: '0.1',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
}),
|
||||||
|
sendToContentScriptResilient: async () => ({}),
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => helpers.requestPhoneActivation({
|
||||||
|
phoneSmsProvider: '5sim',
|
||||||
|
fiveSimApiKey: 'five-token',
|
||||||
|
fiveSimCountryOrder: ['vietnam'],
|
||||||
|
fiveSimOperator: 'virtual21',
|
||||||
|
fiveSimMaxPrice: '0.1',
|
||||||
|
heroSmsActivationRetryRounds: 1,
|
||||||
|
}),
|
||||||
|
/maxPrice only works when operator is "any"/
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(requests, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('phone verification helper honors price-priority ordering for 5sim countries', async () => {
|
test('phone verification helper honors price-priority ordering for 5sim countries', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const helpers = api.createPhoneVerificationHelpers({
|
const helpers = api.createPhoneVerificationHelpers({
|
||||||
|
|||||||
Reference in New Issue
Block a user