fix(flow): harden proxy phone and checkout recovery paths

This commit is contained in:
朴圣佑
2026-05-03 13:58:33 +08:00
committed by QLHazyCoder
parent d851cc4d36
commit 29093e08db
34 changed files with 2958 additions and 293 deletions
+7
View File
@@ -11,6 +11,8 @@ test('address sources normalize supported countries and return local seeds', ()
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
assert.equal(api.normalizeCountryCode('印尼'), 'ID');
assert.equal(api.normalizeCountryCode('日本'), 'JP');
assert.equal(api.normalizeCountryCode('韩国'), 'KR');
assert.equal(api.normalizeCountryCode('South Korea'), 'KR');
assert.equal(api.normalizeCountryCode('unknown'), '');
const deSeed = api.getAddressSeedForCountry('DE');
@@ -30,4 +32,9 @@ test('address sources normalize supported countries and return local seeds', ()
const jpSeed = api.getAddressSeedForCountry('日本');
assert.equal(jpSeed.countryCode, 'JP');
assert.equal(jpSeed.fallback.region, 'Tokyo');
const krSeed = api.getAddressSeedForCountry('Korea');
assert.equal(krSeed.countryCode, 'KR');
assert.equal(krSeed.fallback.region, 'Seoul');
assert.match(krSeed.fallback.postalCode, /^\d{5}$/);
});
+334
View File
@@ -653,6 +653,172 @@ test('auto-run controller skips user_already_exists failures to the next round i
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller skips step 4 repeated 405 recovery failures to the next round', async () => {
const events = {
logs: [],
broadcasts: [],
accountRecords: [],
runCalls: 0,
};
let currentState = {
stepStatuses: {},
vpsUrl: 'https://example.com/vps',
vpsPassword: 'secret',
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: '',
mail2925BaseEmail: '',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
tabRegistry: {},
sourceLastUrls: {},
autoRunRoundSummaries: [],
};
const runtime = {
state: {
autoRunActive: false,
autoRunCurrentRun: 0,
autoRunTotalRuns: 1,
autoRunAttemptRun: 0,
autoRunSessionId: 0,
},
get() {
return { ...this.state };
},
set(updates = {}) {
this.state = { ...this.state, ...updates };
},
};
let sessionSeed = 0;
const controller = api.createAutoRunController({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
appendAccountRunRecord: async (status, _state, reason) => {
events.accountRecords.push({ status, reason });
return { status, reason };
},
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
AUTO_RUN_RETRY_DELAY_MS: 3000,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
broadcastAutoRunStatus: async (phase, payload = {}) => {
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
};
},
broadcastStopToContentScripts: async () => {},
cancelPendingCommands: () => {},
clearStopRequest: () => {},
createAutoRunSessionId: () => {
sessionSeed += 1;
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => error?.message || String(error || ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
getState: async () => ({
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
}),
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: () => false,
isPlusCheckoutNonFreeTrialFailure: () => false,
isRestartCurrentAttemptError: () => false,
isSignupUserAlreadyExistsFailure: () => false,
isStep4Route405RecoveryLimitFailure: (error) => /STEP4_405_RECOVERY_LIMIT::/.test(error?.message || String(error || '')),
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
persistAutoRunTimerPlan: async () => ({}),
resetState: async () => {
currentState = {
...currentState,
stepStatuses: {},
tabRegistry: {},
sourceLastUrls: {},
};
},
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls === 1) {
throw new Error('STEP4_405_RECOVERY_LIMIT::步骤 4:检测到 405 错误页面,已连续点击“重试”恢复 3/3 次仍未恢复,当前轮将结束并进入下一轮。');
}
},
runtime,
setState: async (updates = {}) => {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
};
},
sleepWithStop: async () => {},
throwIfAutoRunSessionStopped: (sessionId) => {
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
throw new Error('流程已被用户停止。');
}
},
waitForRunningStepsToFinish: async () => currentState,
chrome: {
runtime: {
sendMessage() {
return Promise.resolve();
},
},
},
});
await controller.autoRunLoop(2, {
autoRunSkipFailures: true,
mode: 'restart',
});
assert.equal(events.runCalls, 2, 'step 4 repeated 405 failure should skip the current round and continue with the next round');
assert.equal(events.broadcasts.some(({ phase }) => phase === 'retrying'), false, 'step 4 repeated 405 should not retry the same round');
assert.equal(events.accountRecords.length, 1, 'step 4 repeated 405 should persist a failed round record');
assert.equal(events.accountRecords[0].status, 'failed');
assert.match(events.accountRecords[0].reason, /STEP4_405_RECOVERY_LIMIT::/);
assert.ok(events.logs.some(({ message }) => /连续 405.*继续下一轮|继续下一轮/.test(message)));
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller keeps retrying the same custom mail provider pool email until success', async () => {
const events = {
logs: [],
@@ -817,3 +983,171 @@ test('auto-run controller keeps retrying the same custom mail provider pool emai
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
test('auto-run controller retries 5sim rate limit failures instead of treating current add-phone page as fatal', async () => {
const events = {
logs: [],
broadcasts: [],
accountRecords: [],
runCalls: 0,
sleeps: [],
};
let currentState = {
stepStatuses: {},
vpsUrl: 'https://example.com/vps',
vpsPassword: 'secret',
customPassword: '',
autoRunSkipFailures: true,
autoRunFallbackThreadIntervalMinutes: 0,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
mailProvider: '163',
emailGenerator: 'duck',
gmailBaseEmail: '',
mail2925BaseEmail: '',
emailPrefix: 'demo',
inbucketHost: '',
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
tabRegistry: {},
sourceLastUrls: {},
autoRunRoundSummaries: [],
};
const runtime = {
state: {
autoRunActive: false,
autoRunCurrentRun: 0,
autoRunTotalRuns: 1,
autoRunAttemptRun: 0,
autoRunSessionId: 0,
},
get() {
return { ...this.state };
},
set(updates = {}) {
this.state = { ...this.state, ...updates };
},
};
let sessionSeed = 0;
const controller = api.createAutoRunController({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
appendAccountRunRecord: async (status, _state, reason) => {
events.accountRecords.push({ status, reason });
return { status, reason };
},
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
AUTO_RUN_RETRY_DELAY_MS: 3000,
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
broadcastAutoRunStatus: async (phase, payload = {}) => {
events.broadcasts.push({ phase, ...payload });
currentState = {
...currentState,
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
};
},
broadcastStopToContentScripts: async () => {},
cancelPendingCommands: () => {},
clearStopRequest: () => {},
createAutoRunSessionId: () => {
sessionSeed += 1;
return sessionSeed;
},
getAutoRunStatusPayload: (phase, payload = {}) => ({
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
autoRunPhase: phase,
autoRunCurrentRun: payload.currentRun ?? 0,
autoRunTotalRuns: payload.totalRuns ?? 1,
autoRunAttemptRun: payload.attemptRun ?? 0,
autoRunSessionId: payload.sessionId ?? 0,
}),
getErrorMessage: (error) => error?.message || String(error || ''),
getFirstUnfinishedStep: () => 1,
getPendingAutoRunTimerPlan: () => null,
getRunningSteps: () => [],
getState: async () => ({
...currentState,
stepStatuses: { ...(currentState.stepStatuses || {}) },
tabRegistry: { ...(currentState.tabRegistry || {}) },
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
}),
getStopRequested: () => false,
hasSavedProgress: () => false,
isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
isPhoneSmsPlatformRateLimitFailure: (error) => /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(error?.message || String(error || '')),
isRestartCurrentAttemptError: () => false,
isSignupUserAlreadyExistsFailure: () => false,
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
launchAutoRunTimerPlan: async () => false,
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
persistAutoRunTimerPlan: async () => ({}),
resetState: async () => {
currentState = {
...currentState,
stepStatuses: {},
tabRegistry: {},
sourceLastUrls: {},
};
},
runAutoSequenceFromStep: async () => {
events.runCalls += 1;
if (events.runCalls === 1) {
throw new Error('FIVE_SIM_RATE_LIMIT::5sim 购买接口触发限流,请稍后再试:印度 (India): rate limit。当前页面 https://auth.openai.com/add-phone');
}
},
runtime,
setState: async (updates = {}) => {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
};
},
sleepWithStop: async (ms) => {
events.sleeps.push(ms);
},
throwIfAutoRunSessionStopped: (sessionId) => {
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
throw new Error('流程已被用户停止。');
}
},
waitForRunningStepsToFinish: async () => currentState,
chrome: {
runtime: {
sendMessage() {
return Promise.resolve();
},
},
},
});
await controller.autoRunLoop(1, {
autoRunSkipFailures: true,
mode: 'restart',
});
assert.equal(events.runCalls, 2, '5sim rate limit should use same-round retry instead of add-phone fatal skip');
assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 1);
assert.equal(events.accountRecords.length, 0);
assert.ok(events.logs.some(({ message }) => /自动重试/.test(message)));
assert.equal(events.logs.some(({ message }) => /触发 add-phone\/手机号页/.test(message)), false);
assert.equal(events.sleeps.filter((ms) => ms === 3000).length, 1);
assert.equal(runtime.state.autoRunActive, false);
assert.equal(runtime.state.autoRunSessionId, 0);
});
+22
View File
@@ -147,6 +147,10 @@ function getLoginAuthStateLabel(state) {
function getErrorMessage(error) {
return error?.message || String(error || '');
}
function isPhoneSmsPlatformRateLimitFailure(error) {
const message = getErrorMessage(error);
return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message);
}
async function getLoginAuthStateFromContent() {
return ${JSON.stringify(authState)};
}
@@ -254,6 +258,24 @@ test('auto-run does not restart step 7 when phone verification exhausted replace
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run post-login restart decision does not treat 5sim rate limit on add-phone page as add-phone fatal', async () => {
const harness = createHarness({
failureStep: 9,
failureBudget: 1,
failureMessage: 'FIVE_SIM_RATE_LIMIT::5sim 购买接口触发限流,请稍后再试:印度 (India): rate limit。',
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.equal(result.events.invalidations.length, 0);
assert.deepStrictEqual(result.events.steps, [7, 8, 9]);
assert.ok(!result.events.logs.some(({ message }) => /进入 add-phone/.test(message)));
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
});
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
const harness = createHarness({
failureStep: 9,
@@ -106,9 +106,11 @@ const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const FIVE_SIM_COUNTRY_ID = 'england';
const FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const FIVE_SIM_COUNTRY_ID = 'vietnam';
const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
mailProvider: '163',
@@ -156,19 +158,19 @@ return {
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'england');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'england');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '英国 (England)');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'vietnam');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ''), 'vietnam');
assert.equal(api.normalizePersistentSettingValue('fiveSimCountryLabel', ''), '越南 (Vietnam)');
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '9.87654'), '9.8765');
assert.equal(api.normalizePersistentSettingValue('fiveSimMaxPrice', '-1'), '');
assert.equal(api.normalizePersistentSettingValue('fiveSimOperator', ''), 'any');
assert.deepStrictEqual(
api.normalizePersistentSettingValue('fiveSimCountryFallback', [{ id: 'usa', label: 'USA' }, 'thailand:Thailand']),
[{ id: 'usa', label: 'USA' }, { id: 'thailand', label: 'Thailand' }]
[{ id: 'thailand', label: 'Thailand' }]
);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('heroSmsCountryFallback', [{ id: 16, label: 'United Kingdom' }, { id: 52 }]),
[{ id: 16, label: 'United Kingdom' }, { id: 52, label: 'Country #52' }]
[{ id: 52, label: 'Country #52' }]
);
assert.equal(
api.normalizePersistentSettingValue('accountRunHistoryHelperBaseUrl', 'http://127.0.0.1:17373/append-account-log'),
+119
View File
@@ -19,6 +19,18 @@ const IP_PROXY_FETCH_TIMEOUT_MS = 20000;
const IP_PROXY_SETTINGS_SCOPE = 'regular';
const IP_PROXY_BYPASS_LIST = ['<local>', 'localhost', '127.0.0.1'];
const IP_PROXY_ROUTE_ALL_TRAFFIC = true;
const IP_PROXY_FORCE_DIRECT_HOST_PATTERNS = [
'pm-redirects.stripe.com',
'*.pm-redirects.stripe.com',
'hwork.pro',
'*.hwork.pro',
'auth.openai.com',
'auth0.openai.com',
'accounts.openai.com',
'luckyous.com',
'*.luckyous.com',
];
const IP_PROXY_FORCE_DIRECT_FALLBACK = 'PROXY 127.0.0.1:7897';
const IP_PROXY_ACCOUNT_LIST_ENABLED = ${accountListEnabled ? 'true' : 'false'};
const IP_PROXY_TARGET_HOST_PATTERNS = [
'openai.com',
@@ -32,11 +44,17 @@ ${coreSource}
return {
applyExitRegionExpectation,
buildIpProxyPacScript,
buildIpProxyRoutingStatePatch,
applyTargetReachabilityExpectation,
getAccountModeProxyPoolFromState,
normalizeIpProxyAccountList,
normalizeProxyPoolEntries,
parseProxyExitProbePayload,
parseIpProxyLine,
resolveExitProbeEndpoints,
resolveIpProxyAutoSwitchThreshold,
resolveTargetReachabilityEndpoints,
shouldEnableIpProxyLeakGuardForStatus,
};
`)();
}
@@ -75,6 +93,48 @@ test('IP proxy parser ignores disabled lines and normalizes proxy entries', () =
assert.equal(pool[1].port, 8080);
});
test('IP proxy probe payload parser extracts country from common probe endpoints', () => {
const api = loadIpProxyCore();
assert.deepEqual(
api.parseProxyExitProbePayload('ip=219.104.171.52\nloc=JP\ncolo=NRT', 'text/plain'),
{ ip: '219.104.171.52', region: 'JP' }
);
assert.deepEqual(
api.parseProxyExitProbePayload(JSON.stringify({
ip: '219.104.171.52',
country: 'JP',
city: 'Osaka',
}), 'application/json'),
{ ip: '219.104.171.52', region: 'JP' }
);
assert.deepEqual(
api.parseProxyExitProbePayload(JSON.stringify({
ip: '219.104.171.52',
country_code: 'JP',
country: 'Japan',
}), 'application/json'),
{ ip: '219.104.171.52', region: 'JP' }
);
});
test('IP proxy routing state patch keeps exit probe endpoint for diagnostics', () => {
const api = loadIpProxyCore();
const patch = api.buildIpProxyRoutingStatePatch({
applied: true,
reason: 'applied',
provider: '711proxy',
exitIp: '219.104.171.52',
exitRegion: 'JP',
exitSource: 'page_context',
exitEndpoint: 'https://ipinfo.io/json',
});
assert.equal(patch.ipProxyAppliedExitIp, '219.104.171.52');
assert.equal(patch.ipProxyAppliedExitRegion, 'JP');
assert.equal(patch.ipProxyAppliedExitEndpoint, 'https://ipinfo.io/json');
});
test('711 fixed-account mode applies region and sticky session parameters', () => {
const api = loadIpProxyCore();
const pool = api.getAccountModeProxyPoolFromState({
@@ -112,6 +172,15 @@ test('IP proxy PAC keeps local traffic direct and routes target traffic through
assert.match(pac, /PROXY global\.rotgb\.711proxy\.com:10000/);
assert.match(pac, /chatgpt\.com/);
assert.match(pac, /openai\.com/);
assert.match(pac, /pm-redirects\.stripe\.com/);
assert.match(pac, /hwork\.pro/);
assert.match(pac, /auth\.openai\.com/);
assert.match(pac, /auth0\.openai\.com/);
assert.match(pac, /accounts\.openai\.com/);
assert.match(pac, /luckyous\.com/);
assert.match(pac, /forceDirectPatterns/);
assert.match(pac, /PROXY 127\.0\.0\.1:7897/);
assert.doesNotMatch(pac, /PROXY 127\.0\.0\.1:7897; DIRECT/);
});
test('sidepanel loads IP proxy scripts before sidepanel bootstrap', () => {
@@ -161,3 +230,53 @@ test('711 proxy region mismatch with missing auth challenge keeps routing as war
);
assert.match(String(status.warning || ''), /期望 US,实际 BR/);
});
test('711 sticky session keeps IP probe on ipinfo but separately checks ChatGPT target', () => {
const api = loadIpProxyCore();
assert.deepStrictEqual(
api.resolveExitProbeEndpoints({
provider: '711proxy',
username: 'USER794331-zone-custom-region-TH-session-69381850-sessTime-5',
}),
['https://ipinfo.io/json']
);
assert.deepStrictEqual(api.resolveTargetReachabilityEndpoints(), ['https://chatgpt.com/']);
});
test('target reachability failure turns detected exit IP into connectivity_failed', () => {
const api = loadIpProxyCore();
const status = api.applyTargetReachabilityExpectation({
applied: true,
reason: 'applied',
exitIp: '58.10.48.73',
exitRegion: 'TH',
}, {
reachable: false,
endpoint: 'https://chatgpt.com/',
error: 'target:page_context:https://chatgpt.com/:net::ERR_EMPTY_RESPONSE',
});
assert.equal(status.applied, false);
assert.equal(status.reason, 'connectivity_failed');
assert.match(status.error, /已检测到出口 IP 58\.10\.48\.73 \[TH\]/);
assert.match(status.error, /真实目标 chatgpt\.com 不可达/);
assert.match(status.error, /ERR_EMPTY_RESPONSE/);
});
test('connectivity_failed keeps DNR leak guard off so ChatGPT shows proxy error instead of blocked by extension', () => {
const api = loadIpProxyCore();
assert.equal(api.shouldEnableIpProxyLeakGuardForStatus({
enabled: true,
applied: false,
reason: 'connectivity_failed',
}), false);
assert.equal(api.shouldEnableIpProxyLeakGuardForStatus({
enabled: true,
applied: false,
reason: 'missing_proxy_entry',
}), true);
});
+140
View File
@@ -451,6 +451,9 @@ const PERSISTED_SETTING_DEFAULTS = {
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
luckmailDomain: '',
luckmailUsedPurchases: {},
luckmailPreserveTagId: 0,
luckmailPreserveTagName: '保留',
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
function normalizeLuckmailBaseUrl(value) {
@@ -462,6 +465,18 @@ function normalizeLuckmailEmailType(value) {
? String(value || '').trim()
: DEFAULT_LUCKMAIL_EMAIL_TYPE;
}
function normalizeLuckmailPurchaseId(value) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
}
function normalizeLuckmailUsedPurchases(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return Object.entries(value).reduce((result, [key, used]) => {
const normalizedKey = normalizeLuckmailPurchaseId(key);
if (normalizedKey) result[normalizedKey] = Boolean(used);
return result;
}, {});
}
function resolveLegacyAutoStepDelaySeconds() {
return undefined;
}
@@ -487,6 +502,17 @@ return {
luckmailEmailType: 'ms_imap',
luckmailDomain: 'outlook.com',
});
const statePayload = api.buildPersistentSettingsPayload({
luckmailUsedPurchases: { 88: true, 99: false, bad: true },
luckmailPreserveTagId: '9',
luckmailPreserveTagName: ' 保留邮箱 ',
});
assert.deepStrictEqual(statePayload, {
luckmailUsedPurchases: { 88: true, 99: false },
luckmailPreserveTagId: 9,
luckmailPreserveTagName: '保留邮箱',
});
});
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
@@ -879,3 +905,117 @@ return {
assert.deepStrictEqual(snapshot.usedMarker, { purchaseId: 456, used: true });
assert.equal(snapshot.logs.some((entry) => /已在本地标记为已用/.test(entry.message)), true);
});
test('setLuckmailPurchaseUsedState persists used map to storage.local so reload keeps it non-reusable', async () => {
const bundle = [
extractFunction('getLuckmailUsedPurchases'),
extractFunction('setLuckmailUsedPurchasesState'),
extractFunction('setLuckmailPurchaseUsedState'),
].join('\n');
const factory = new Function(`
let sessionState = {
luckmailUsedPurchases: { 7: true },
};
let persistentUpdates = [];
let sessionUpdates = [];
let broadcasts = [];
function normalizeLuckmailPurchaseId(value) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
}
function normalizeLuckmailUsedPurchases(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return Object.entries(value).reduce((result, [key, used]) => {
const normalizedKey = normalizeLuckmailPurchaseId(key);
if (normalizedKey) result[normalizedKey] = Boolean(used);
return result;
}, {});
}
async function getState() {
return { ...sessionState };
}
async function setPersistentSettings(updates) {
persistentUpdates.push(updates);
}
async function setState(updates) {
sessionUpdates.push(updates);
sessionState = { ...sessionState, ...updates };
}
function broadcastDataUpdate(updates) {
broadcasts.push(updates);
}
${bundle}
return {
setLuckmailPurchaseUsedState,
snapshot() {
return { sessionState, persistentUpdates, sessionUpdates, broadcasts };
},
};
`);
const api = factory();
const result = await api.setLuckmailPurchaseUsedState(88, true);
const snapshot = api.snapshot();
assert.deepStrictEqual(result, { purchaseId: 88, used: true });
assert.deepStrictEqual(snapshot.sessionState.luckmailUsedPurchases, { 7: true, 88: true });
assert.deepStrictEqual(snapshot.persistentUpdates, [
{ luckmailUsedPurchases: { 7: true, 88: true } },
]);
assert.deepStrictEqual(snapshot.sessionUpdates, [
{ luckmailUsedPurchases: { 7: true, 88: true } },
]);
assert.deepStrictEqual(snapshot.broadcasts, [
{ luckmailUsedPurchases: { 7: true, 88: true } },
]);
});
test('setLuckmailPreserveTagInfo persists tag cache to storage.local', async () => {
const bundle = extractFunction('setLuckmailPreserveTagInfo');
const factory = new Function(`
let persistentUpdates = [];
let sessionUpdates = [];
let broadcasts = [];
const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';
function normalizeLuckmailTags(tags) {
return (Array.isArray(tags) ? tags : []).map((tag) => ({
id: Number(tag?.id) || 0,
name: String(tag?.name || '').trim(),
})).filter((tag) => tag.id > 0 || tag.name);
}
async function setPersistentSettings(updates) {
persistentUpdates.push(updates);
}
async function setState(updates) {
sessionUpdates.push(updates);
}
function broadcastDataUpdate(updates) {
broadcasts.push(updates);
}
${bundle}
return {
setLuckmailPreserveTagInfo,
snapshot() {
return { persistentUpdates, sessionUpdates, broadcasts };
},
};
`);
const api = factory();
const result = await api.setLuckmailPreserveTagInfo({ id: '12', name: ' 保留邮箱 ' });
const expected = {
luckmailPreserveTagId: 12,
luckmailPreserveTagName: '保留邮箱',
};
assert.deepStrictEqual(result, expected);
assert.deepStrictEqual(api.snapshot().persistentUpdates, [expected]);
assert.deepStrictEqual(api.snapshot().sessionUpdates, [expected]);
assert.deepStrictEqual(api.snapshot().broadcasts, [expected]);
});
@@ -237,3 +237,36 @@ test('platform verify module rejects callback when cpa oauth state mismatches',
globalThis.fetch = originalFetch;
}
});
test('platform verify module sends Plus visible step 13 to SUB2API panel', async () => {
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
const sentMessages = [];
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
const { deps } = createDeps({
getPanelMode: () => 'sub2api',
getTabId: async () => 91,
isTabAlive: async () => true,
sendToContentScript: async (sourceName, message, options) => {
sentMessages.push({ sourceName, message, options });
return { ok: true };
},
});
const executor = api.createStep10Executor(deps);
await executor.executeStep10({
panelMode: 'sub2api',
visibleStep: 13,
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
sub2apiUrl: 'https://sub.example/admin/accounts',
sub2apiEmail: 'admin@example.com',
sub2apiPassword: 'secret',
sub2apiSessionId: 'session-1',
sub2apiOAuthState: 'oauth-state',
});
assert.equal(sentMessages.length, 1);
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
assert.equal(sentMessages[0].message.step, 13);
});
+111 -19
View File
@@ -37,25 +37,31 @@ test('5sim provider maps countries and prices', async () => {
const parsed = new URL(url);
requests.push({ url: parsed, options });
if (parsed.pathname === '/v1/guest/countries') {
return createTextResponse({ england: { text_en: 'England', iso: { GB: 1 }, prefix: { 44: 1 } } });
return createTextResponse({
england: { text_en: 'England', iso: { GB: 1 }, prefix: { 44: 1 } },
indonesia: { text_en: 'Indonesia', iso: { ID: 1 }, prefix: { '+62': 1 } },
thailand: { text_en: 'Thailand', iso: { TH: 1 }, prefix: { '+66': 1 } },
vietnam: { text_en: 'Vietnam', iso: { VN: 1 }, prefix: { '+84': 1 } },
});
}
if (parsed.pathname === '/v1/guest/prices') {
return createTextResponse({ england: { any: { openai: { cost: 10, count: 2 } } } });
return createTextResponse({ vietnam: { any: { openai: { cost: 10, count: 2 } } } });
}
throw new Error(`unexpected ${parsed.pathname}`);
},
});
const countries = await provider.fetchCountries({});
const prices = await provider.fetchPrices({}, { id: 'england', label: 'England' });
const prices = await provider.fetchPrices({}, { id: 'vietnam', label: 'Vietnam' });
const entries = provider.collectPriceEntries(prices, []);
assert.deepStrictEqual(countries[0], {
id: 'england',
label: '英国 (England)',
searchText: 'england 英国 (England) England GB 44',
assert.deepStrictEqual(countries.map((country) => country.id), ['indonesia', 'thailand', 'vietnam']);
assert.deepStrictEqual(countries[2], {
id: 'vietnam',
label: '越南 (Vietnam)',
searchText: 'vietnam 越南 (Vietnam) Vietnam VN +84',
});
assert.equal(requests[1].url.searchParams.get('country'), 'england');
assert.equal(requests[1].url.searchParams.get('country'), 'vietnam');
assert.equal(requests[1].url.searchParams.get('product'), 'openai');
assert.deepStrictEqual(entries, [{ cost: 10, count: 2, inStock: true }]);
});
@@ -66,14 +72,14 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
fetchImpl: async (url, options = {}) => {
const parsed = new URL(url);
requests.push({ url: parsed, options });
if (parsed.pathname === '/v1/guest/products/england/any') {
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
return createTextResponse({ openai: { Category: 'activation', Qty: 4, Price: 8 } });
}
if (parsed.pathname === '/v1/guest/prices') {
return createTextResponse({ england: { any: { openai: { cost: 9.5, count: 4 } } } });
return createTextResponse({ vietnam: { any: { openai: { cost: 9.5, count: 4 } } } });
}
if (parsed.pathname === '/v1/user/buy/activation/england/any/openai') {
return createTextResponse({ id: 1001, phone: '+447911123456', country: 'england', operator: 'any', status: 'PENDING' });
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
return createTextResponse({ id: 1001, phone: '+84901123456', country: 'vietnam', operator: 'any', status: 'PENDING' });
}
if (parsed.pathname === '/v1/user/check/1001') {
return createTextResponse({ id: 1001, phone: '+447911123456', status: 'RECEIVED', sms: [{ text: 'code 112233' }] });
@@ -81,8 +87,8 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
if (parsed.pathname === '/v1/user/finish/1001') return createTextResponse({ status: 'FINISHED' });
if (parsed.pathname === '/v1/user/cancel/1001') return createTextResponse({ status: 'CANCELED' });
if (parsed.pathname === '/v1/user/ban/1001') return createTextResponse({ status: 'BANNED' });
if (parsed.pathname === '/v1/user/reuse/openai/447911123456') {
return createTextResponse({ id: 1002, phone: '+447911123456', country: 'england', status: 'PENDING' });
if (parsed.pathname === '/v1/user/reuse/openai/84901123456') {
return createTextResponse({ id: 1002, phone: '+84901123456', country: 'vietnam', status: 'PENDING' });
}
throw new Error(`unexpected ${parsed.pathname}`);
},
@@ -90,7 +96,7 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
throwIfStopped: () => {},
});
const state = { fiveSimApiKey: 'demo-key', fiveSimCountryId: 'england', fiveSimCountryLabel: 'England', fiveSimMaxPrice: '12', fiveSimOperator: 'any' };
const state = { fiveSimApiKey: 'demo-key', fiveSimCountryId: 'vietnam', fiveSimCountryLabel: '越南 (Vietnam)', fiveSimMaxPrice: '12', fiveSimOperator: 'any' };
const activation = await provider.requestActivation(state);
const code = await provider.pollActivationCode(state, activation, { timeoutMs: 1000, intervalMs: 1, maxRounds: 1 });
await provider.finishActivation(state, activation);
@@ -100,7 +106,7 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
assert.equal(activation.provider, '5sim');
assert.equal(activation.activationId, '1001');
assert.equal(activation.countryId, 'england');
assert.equal(activation.countryId, 'vietnam');
assert.equal(code, '112233');
assert.equal(reused.activationId, '1002');
const buy = requests.find((entry) => entry.url.pathname.includes('/buy/activation'));
@@ -109,14 +115,14 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
[
'/v1/guest/products/england/any',
'/v1/guest/products/vietnam/any',
'/v1/guest/prices',
'/v1/user/buy/activation/england/any/openai',
'/v1/user/buy/activation/vietnam/any/openai',
'/v1/user/check/1001',
'/v1/user/finish/1001',
'/v1/user/cancel/1001',
'/v1/user/ban/1001',
'/v1/user/reuse/openai/447911123456',
'/v1/user/reuse/openai/84901123456',
]
);
});
@@ -168,3 +174,89 @@ test('5sim provider prefers buy-compatible products price over operator detail p
]
);
});
test('5sim provider reports raw buy payload when HTTP 200 response has no activation', async () => {
const provider = api.createProvider({
fetchImpl: async (url) => {
const parsed = new URL(url);
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
return createTextResponse({ openai: { Category: 'activation', Qty: 10, Price: 0.08 } });
}
if (parsed.pathname === '/v1/guest/prices') {
return createTextResponse({ vietnam: { openai: { virtual47: { cost: 0.1282, count: 10 } } } });
}
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
return createTextResponse({ status: 'no free phones', detail: 'operator unavailable' });
}
throw new Error(`unexpected ${parsed.pathname}`);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
() => provider.requestActivation({
fiveSimApiKey: 'demo-key',
fiveSimCountryId: 'vietnam',
fiveSimCountryLabel: '越南 (Vietnam)',
fiveSimOperator: 'any',
}),
/越南 \(Vietnam\): no free phones/
);
});
test('5sim provider reports purchase rate limit separately from no-number countries', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url) => {
const parsed = new URL(url);
requests.push(parsed.pathname);
if (parsed.pathname === '/v1/guest/products/thailand/any') {
return createTextResponse({ openai: { Category: 'activation', Qty: 10, Price: 0.1 } });
}
if (parsed.pathname === '/v1/guest/products/vietnam/any') {
return createTextResponse({ openai: { Category: 'activation', Qty: 10, Price: 0.08 } });
}
if (parsed.pathname === '/v1/guest/prices') {
const country = parsed.searchParams.get('country');
return createTextResponse({ [country]: { any: { openai: { cost: country === 'vietnam' ? 0.08 : 0.1, count: 10 } } } });
}
if (parsed.pathname === '/v1/user/buy/activation/thailand/any/openai') {
return createTextResponse({ status: 'rate limit' });
}
if (parsed.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
return createTextResponse({ status: 'rate limit' });
}
throw new Error(`unexpected ${parsed.pathname}`);
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
() => provider.requestActivation({
fiveSimApiKey: 'demo-key',
fiveSimCountryId: 'thailand',
fiveSimCountryLabel: '泰国 (Thailand)',
fiveSimCountryFallback: [{ id: 'vietnam', label: '越南 (Vietnam)' }],
fiveSimOperator: 'any',
}),
(error) => {
assert.match(error.message, /^FIVE_SIM_RATE_LIMIT::/);
assert.match(error.message, /5sim 购买接口触发限流/);
assert.match(error.message, /泰国 \(Thailand\): rate limit/);
assert.match(error.message, /越南 \(Vietnam\): rate limit/);
assert.doesNotMatch(error.message, /均无可用号码/);
return true;
}
);
assert.deepStrictEqual(
requests.filter((pathname) => pathname.includes('/buy/activation')),
[
'/v1/user/buy/activation/thailand/any/openai',
'/v1/user/buy/activation/vietnam/any/openai',
]
);
});
+18
View File
@@ -100,6 +100,17 @@ test('GoPay approve does not treat phone linking page as debugger iframe action'
assert.match(source, /filter\(\(target\) => target\.type === 'iframe'\)/);
});
test('GoPay approve waits and retries slowly on Hubungkan linking page', () => {
assert.match(source, /GOPAY_LINKING_RETRY_WAIT_MS/);
assert.match(source, /GOPAY_LINKING_STABLE_WAIT_MS/);
assert.match(source, /createGoPayStableStateTracker/);
assert.match(source, /clickGoPayContinueBestEffort/);
assert.match(source, /hubungkan\|sambungkan\|tautkan/);
assert.match(source, /先等待 linking 页面加载\/跳转/);
assert.match(source, /改用兜底点击 Hubungkan\/确认按钮/);
assert.doesNotMatch(source, /GoPay 确认按钮点击后页面仍未变化,已暂停自动重复点击。请手动点击页面上的确认按钮,插件会继续等待后续页面。/);
});
test('background auto-run routes GoPay restart sentinel back to step 6', () => {
const backgroundSource = fs.readFileSync('background.js', 'utf8');
@@ -109,3 +120,10 @@ test('background auto-run routes GoPay restart sentinel back to step 6', () => {
assert.match(backgroundSource, /step = 6/);
assert.match(backgroundSource, /invalidateDownstreamAfterStepRestart\(5/);
});
test('GoPay approve gives PIN precedence over OTP on ambiguous second PIN pages', () => {
assert.match(source, /pageState\.hasPinInput && !pinSubmitted/);
assert.match(source, /pageState\.hasOtpInput && !pageState\.hasPinInput && !otpSubmitted/);
assert.ok(source.indexOf('pageState.hasPinInput && !pinSubmitted') < source.indexOf('pageState.hasOtpInput && !pageState.hasPinInput && !otpSubmitted'));
assert.doesNotMatch(source, /otp\|one\[-\\s\]\*time\|kode\|verification\|whatsapp\|code\|pin-input-field/);
});
+66 -1
View File
@@ -137,6 +137,66 @@ return { getGoPayContinueTarget };
assert.match(target.target, /Link and pay/);
});
test('GoPay continue target recognizes Indonesian Hubungkan linking button', () => {
const bundle = [
extractFunction('normalizeText'),
extractFunction('getActionText'),
extractFunction('isVisibleElement'),
extractFunction('getVisibleControls'),
extractFunction('isEnabledControl'),
extractFunction('findClickableByText'),
extractFunction('findContinueButton'),
extractFunction('describeElement'),
extractFunction('getElementClickRect'),
extractFunction('getGoPayContinueTarget'),
].join('\n');
const button = {
tagName: 'BUTTON',
id: '',
className: 'btn primary',
textContent: 'Hubungkan',
innerText: 'Hubungkan',
value: '',
disabled: false,
hidden: false,
parentElement: null,
getAttribute(name) {
if (name === 'class') return this.className;
return '';
},
getBoundingClientRect() { return { left: 40, top: 720, width: 280, height: 48 }; },
};
const termsLink = {
...button,
tagName: 'A',
className: 'terms-link',
textContent: 'Syarat & Ketentuan',
innerText: 'Syarat & Ketentuan',
getBoundingClientRect() { return { left: 120, top: 650, width: 120, height: 16 }; },
};
const api = new Function('button', 'termsLink', `
const window = {
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
innerWidth: 390,
innerHeight: 844,
};
const document = {
querySelectorAll(selector) {
return selector.includes('button') || selector.includes('a') || selector.includes('[role="button"]') ? [termsLink, button] : [];
},
};
${bundle}
return { findContinueButton, getGoPayContinueTarget };
`)(button, termsLink);
assert.equal(api.findContinueButton(), button);
const target = api.getGoPayContinueTarget();
assert.equal(target.found, true);
assert.match(target.target, /Hubungkan/);
assert.equal(target.rect.centerX, 180);
assert.equal(target.rect.centerY, 744);
});
test('GoPay PIN page detection wins over generic pin-input OTP attributes', () => {
const bundle = [
@@ -145,6 +205,7 @@ test('GoPay PIN page detection wins over generic pin-input OTP attributes', () =
extractFunction('getPageBodyText'),
extractFunction('isGoPayOtpPageText'),
extractFunction('isGoPayPinPageText'),
extractFunction('isGoPayPinInput'),
extractFunction('isVisibleElement'),
extractFunction('getVisibleControls'),
extractFunction('isEnabledControl'),
@@ -155,6 +216,8 @@ test('GoPay PIN page detection wins over generic pin-input OTP attributes', () =
extractFunction('findOtpInput'),
extractFunction('getGoPayPinInputs'),
extractFunction('findPinInput'),
extractFunction('normalizeOtp'),
extractFunction('fillVisibleOtpInputs'),
].join('\n');
const pinInputs = Array.from({ length: 6 }, (_, index) => ({
tagName: 'INPUT',
@@ -185,12 +248,13 @@ const document = {
querySelectorAll(selector) { return selector.includes('input') ? pinInputs : []; },
};
${bundle}
return { isGoPayOtpPageText, isGoPayPinPageText, findOtpInput, findPinInput, getGoPayPinInputs };
return { isGoPayOtpPageText, isGoPayPinPageText, findOtpInput, findPinInput, getGoPayPinInputs, fillVisibleOtpInputs };
`)(pinInputs);
assert.equal(api.isGoPayPinPageText(), true);
assert.equal(api.isGoPayOtpPageText(), false);
assert.equal(api.findOtpInput(), null);
assert.equal(api.fillVisibleOtpInputs('123456'), false);
assert.equal(api.findPinInput(), pinInputs[0]);
assert.equal(api.getGoPayPinInputs().length, 6);
});
@@ -303,6 +367,7 @@ test('GoPay terminal timeout page is reported as retry-required state', () => {
extractFunction('getActionText'),
extractFunction('getPageBodyText'),
extractFunction('isGoPayPinPageText'),
extractFunction('isGoPayPinInput'),
extractFunction('detectGoPayTerminalError'),
extractFunction('isGoPayOtpPageText'),
extractFunction('isVisibleElement'),
+66 -16
View File
@@ -83,6 +83,56 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
});
test('phone verification helper ignores HeroSMS virtual-only stock when physicalCount is zero', 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({ count: 3, physicalCount: 0, cost: 0.05 }),
};
}
if (action === 'getNumber' || action === 'getNumberV2') {
return {
ok: true,
text: async () => 'NO_NUMBERS',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsActivationRetryRounds: 1 }),
/HeroSMS 已尝试 1 个候选国家,均无可用号码/
);
const actions = requests.map((requestUrl) => `${requestUrl.searchParams.get('action')}:${requestUrl.searchParams.get('maxPrice') || ''}`);
assert.deepStrictEqual(actions, [
'getPrices:',
'getPrices:',
'getPrices:',
'getNumber:',
'getNumberV2:',
'getPrices:',
'getPrices:',
'getPrices:',
'getNumber:',
'getNumberV2:',
]);
});
test('phone verification helper retries HeroSMS getPrices until it receives a usable lowest price', async () => {
const requests = [];
let getPricesAttempt = 0;
@@ -4116,8 +4166,8 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
let currentState = {
phoneSmsProvider: '5sim',
fiveSimApiKey: 'demo-key',
fiveSimCountryId: 'england',
fiveSimCountryLabel: 'England',
fiveSimCountryId: 'vietnam',
fiveSimCountryLabel: '越南 (Vietnam)',
fiveSimMaxPrice: '12',
fiveSimOperator: 'any',
verificationResendCount: 0,
@@ -4139,7 +4189,7 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, options });
if (parsedUrl.pathname === '/v1/guest/products/england/any') {
if (parsedUrl.pathname === '/v1/guest/products/vietnam/any') {
return {
ok: true,
status: 200,
@@ -4150,14 +4200,14 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ england: { any: { openai: { cost: 9.5, count: 3 } } } }),
text: async () => JSON.stringify({ vietnam: { any: { openai: { cost: 9.5, count: 3 } } } }),
};
}
if (parsedUrl.pathname === '/v1/user/buy/activation/england/any/openai') {
if (parsedUrl.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ id: 5001, phone: '+447911223344', country: 'england', operator: 'any', status: 'PENDING' }),
text: async () => JSON.stringify({ id: 5001, phone: '+84901122334', country: 'vietnam', operator: 'any', status: 'PENDING' }),
};
}
if (parsedUrl.pathname === '/v1/user/check/5001') {
@@ -4216,9 +4266,9 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
[
'/v1/guest/products/england/any',
'/v1/guest/products/vietnam/any',
'/v1/guest/prices',
'/v1/user/buy/activation/england/any/openai',
'/v1/user/buy/activation/vietnam/any/openai',
'/v1/user/check/5001',
'/v1/user/finish/5001',
]
@@ -4230,8 +4280,8 @@ test('phone verification helper routes 5sim reusable activation through reuse en
let currentState = {
phoneSmsProvider: '5sim',
fiveSimApiKey: 'demo-key',
fiveSimCountryId: 'england',
fiveSimCountryLabel: 'England',
fiveSimCountryId: 'vietnam',
fiveSimCountryLabel: '越南 (Vietnam)',
fiveSimOperator: 'any',
verificationResendCount: 0,
phoneVerificationReplacementLimit: 2,
@@ -4242,11 +4292,11 @@ test('phone verification helper routes 5sim reusable activation through reuse en
currentPhoneActivation: null,
reusablePhoneActivation: {
activationId: '4001',
phoneNumber: '+447911223344',
phoneNumber: '+84901122334',
provider: '5sim',
serviceCode: 'openai',
countryId: 'england',
countryLabel: 'England',
countryId: 'vietnam',
countryLabel: '越南 (Vietnam)',
successfulUses: 1,
maxUses: 3,
},
@@ -4261,8 +4311,8 @@ test('phone verification helper routes 5sim reusable activation through reuse en
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
if (parsedUrl.pathname === '/v1/user/reuse/openai/447911223344') {
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+447911223344', country: 'england', status: 'PENDING' }) };
if (parsedUrl.pathname === '/v1/user/reuse/openai/84901122334') {
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+84901122334', country: 'vietnam', status: 'PENDING' }) };
}
if (parsedUrl.pathname === '/v1/user/check/4002') {
return { ok: true, status: 200, text: async () => JSON.stringify({ id: 4002, phone: '+447911223344', status: 'RECEIVED', sms: [{ code: '654321' }] }) };
@@ -4306,7 +4356,7 @@ test('phone verification helper routes 5sim reusable activation through reuse en
assert.deepStrictEqual(
requests.map((url) => url.pathname),
[
'/v1/user/reuse/openai/447911223344',
'/v1/user/reuse/openai/84901122334',
'/v1/user/check/4002',
'/v1/user/finish/4002',
]
+70
View File
@@ -383,6 +383,7 @@ test('isPayPalPaymentMethodActive requires a selected PayPal control', () => {
extractFunction('getPaymentMethodSearchCandidates'),
extractFunction('getPayPalSearchCandidates'),
extractFunction('hasCreditCardFields'),
extractFunction('hasPaymentMethodSelectionMarker'),
extractFunction('hasSelectedPaymentMethodControl'),
extractFunction('hasSelectedPayPalControl'),
extractFunction('isPaymentMethodActive'),
@@ -725,6 +726,7 @@ test('payment method helpers can find and confirm selected GoPay controls', () =
extractFunction('getGoPaySearchCandidates'),
extractFunction('findPaymentMethodTarget'),
extractFunction('findGoPayPaymentMethodTarget'),
extractFunction('hasPaymentMethodSelectionMarker'),
extractFunction('hasSelectedPaymentMethodControl'),
extractFunction('hasSelectedGoPayControl'),
extractFunction('isPaymentMethodActive'),
@@ -774,6 +776,74 @@ return { findGoPayPaymentMethodTarget, getGoPaySearchCandidates, hasSelectedGoPa
assert.equal(api.isGoPayPaymentMethodActive(), true);
});
test('GoPay active detection accepts nested selected radio inside payment card', () => {
const bundle = [
"const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';",
"const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';",
"const PAYMENT_METHOD_CONFIGS = { paypal: { id: 'paypal', label: 'PayPal', patterns: [/paypal/i] }, gopay: { id: 'gopay', label: 'GoPay', patterns: [/gopay|go\\s*pay/i] } };",
extractFunction('isVisibleElement'),
extractFunction('normalizeText'),
extractFunction('getActionText'),
extractFunction('getSearchText'),
extractFunction('getFieldText'),
extractFunction('getCombinedSearchText'),
extractFunction('getVisibleControls'),
extractFunction('isDocumentLevelContainer'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getPaymentMethodConfig'),
extractFunction('getPaymentMethodSearchCandidates'),
extractFunction('hasPaymentMethodSelectionMarker'),
extractFunction('hasSelectedPaymentMethodControl'),
extractFunction('hasSelectedGoPayControl'),
extractFunction('isPaymentMethodActive'),
extractFunction('isGoPayPaymentMethodActive'),
].join('\n');
const radio = createElement({
tagName: 'INPUT',
attrs: {
type: 'radio',
role: 'radio',
'aria-checked': 'true',
value: 'gopay',
},
});
radio.checked = true;
const textNode = createElement({ tagName: 'SPAN', text: 'GoPay' });
const card = createElement({ tagName: 'DIV', text: 'GoPay' });
radio.parentElement = card;
textNode.parentElement = card;
card.children = [radio, textNode];
card.querySelector = (selector) => String(selector || '').includes('radio') ? radio : null;
const elements = [card, radio, textNode];
const documentMock = {
documentElement: {},
body: {},
querySelectorAll: (selector) => {
if (String(selector || '').includes('label[for=')) return [];
return elements.filter((element) => {
if (String(selector || '').includes('input[type="radio"]')) return element === radio || element === card || element === textNode;
return true;
});
},
};
const windowMock = {
innerWidth: 1200,
innerHeight: 900,
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
const cssMock = {
escape: (value) => String(value),
};
const api = new Function('window', 'document', 'CSS', `
${bundle}
return { isGoPayPaymentMethodActive };
`)(windowMock, documentMock, cssMock);
assert.equal(api.isGoPayPaymentMethodActive(), true);
});
test('fillIfEmpty can overwrite invalid structured address values in the dropdown branch', () => {
const bundle = [
extractFunction('fillIfEmpty'),
@@ -50,6 +50,20 @@ function createIdAddressSeed() {
};
}
function createKrAddressSeed() {
return {
countryCode: 'KR',
query: 'Seoul Jung-gu',
suggestionIndex: 1,
fallback: {
address1: 'Sejong-daero 110',
city: 'Jung-gu',
region: 'Seoul',
postalCode: '04524',
},
};
}
function createSuccessfulBillingResult() {
return {
countryText: 'Germany',
@@ -68,6 +82,7 @@ function createExecutorHarness({
fetchImpl = null,
getAddressSeedForCountry = () => createAddressSeed(),
markCurrentRegistrationAccountUsed = async () => {},
probeIpProxyExit = null,
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
}) {
const api = loadPlusCheckoutBillingModule();
@@ -149,6 +164,7 @@ function createExecutorHarness({
assert.equal(matcher(submitRedirectUrl), true);
return { id: tabId, url: submitRedirectUrl };
},
...(typeof probeIpProxyExit === 'function' ? { probeIpProxyExit } : {}),
});
return { checkoutTab, events, executor };
@@ -240,7 +256,7 @@ test('Plus checkout billing sends the billing command to the iframe that contain
assert.equal(events.completed[0].step, 7);
});
test('Plus checkout billing forces Indonesia address for GoPay even when page country differs', async () => {
test('Plus checkout billing uses proxy exit country for GoPay address when available', async () => {
const requestedCountries = [];
const fetchRequests = [];
const { events, executor } = createExecutorHarness({
@@ -263,7 +279,17 @@ test('Plus checkout billing forces Indonesia address for GoPay even when page co
},
getAddressSeedForCountry: (countryValue) => {
requestedCountries.push(countryValue);
return countryValue === 'ID' ? createIdAddressSeed() : createAddressSeed();
return countryValue === 'JP' ? {
countryCode: 'JP',
query: 'Tokyo Marunouchi',
suggestionIndex: 1,
fallback: {
address1: 'Marunouchi 1-1',
city: 'Chiyoda-ku',
region: 'Tokyo',
postalCode: '100-0005',
},
} : createIdAddressSeed();
},
fetchImpl: async (url, init) => {
fetchRequests.push({ url, init });
@@ -273,10 +299,11 @@ test('Plus checkout billing forces Indonesia address for GoPay even when page co
json: async () => ({
status: 'ok',
address: {
Address: 'Jl. M.H. Thamrin No. 10',
City: 'Jakarta',
State: 'DKI Jakarta',
Zip_Code: '10310',
Address: 'トウキョウト, チヨダク, マルノウチ, 1-1',
Trans_Address: 'Marunouchi 1-1, Chiyoda-ku, Tokyo',
City: 'Tokyo',
State: 'Tokyo',
Zip_Code: '100-0005',
},
}),
};
@@ -284,17 +311,206 @@ test('Plus checkout billing forces Indonesia address for GoPay even when page co
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
});
await executor.executePlusCheckoutBilling({ plusPaymentMethod: 'gopay', plusCheckoutCountry: 'US' });
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gopay',
plusCheckoutCountry: 'ID',
ipProxyAppliedExitRegion: 'JP',
});
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
assert.equal(requestedCountries[0], 'ID');
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'ID');
assert.equal(requestedCountries[0], 'JP');
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'JP');
assert.equal(fillMessage.message.payload.addressSeed.source, 'meiguodizhi');
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
city: 'Jakarta',
path: '/id-address',
city: 'Chiyoda-ku',
path: '/jp-address',
method: 'refresh',
});
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 JP/.test(entry.message)), true);
});
test('Plus checkout billing refreshes stale GoPay proxy country before filling address', async () => {
const requestedCountries = [];
const probeCalls = [];
const { events, executor } = createExecutorHarness({
frames: [
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
],
stateByFrame: {
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
8: {
hasPayPal: false,
hasGoPay: false,
paypalCandidates: [],
gopayCandidates: [],
billingFieldsVisible: true,
countryText: 'Indonesia',
},
},
getAddressSeedForCountry: (countryValue) => {
requestedCountries.push(countryValue);
return countryValue === 'JP' ? {
countryCode: 'JP',
query: 'Tokyo Chiyoda-ku',
suggestionIndex: 1,
fallback: {
address1: 'Marunouchi 1-1',
city: 'Chiyoda-ku',
region: 'Tokyo',
postalCode: '100-0005',
},
} : createKrAddressSeed();
},
fetchImpl: async () => ({
ok: false,
status: 503,
json: async () => ({ status: 'error' }),
}),
probeIpProxyExit: async (options) => {
probeCalls.push(options);
return {
proxyRouting: {
exitRegion: 'JP',
exitIp: '203.0.113.8',
exitSource: 'page_context',
exitEndpoint: 'https://ipinfo.io/json',
},
};
},
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gopay',
plusCheckoutCountry: 'ID',
ipProxyAppliedExitRegion: 'KR',
});
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
assert.equal(probeCalls.length, 1);
assert.equal(probeCalls[0].detectWhenDisabled, true);
assert.equal(requestedCountries[0], 'JP');
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'JP');
assert.equal(events.logs.some((entry) => entry.message.includes('当前代理出口复测结果:JP / 203.0.113.8')), true);
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 JP/.test(entry.message)), true);
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 KR/.test(entry.message)), false);
});
test('Plus checkout billing refuses to reuse stale GoPay proxy country when refresh has no region', async () => {
const requestedCountries = [];
const { events, executor } = createExecutorHarness({
frames: [
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
],
stateByFrame: {
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
8: {
hasPayPal: false,
hasGoPay: false,
paypalCandidates: [],
gopayCandidates: [],
billingFieldsVisible: true,
countryText: 'Indonesia',
},
},
getAddressSeedForCountry: (countryValue) => {
requestedCountries.push(countryValue);
return createKrAddressSeed();
},
probeIpProxyExit: async () => ({
proxyRouting: {
reason: 'disabled_probe_only',
exitIp: '203.0.113.9',
exitRegion: '',
exitError: 'missing_region',
},
}),
});
await assert.rejects(
() => executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gopay',
plusCheckoutCountry: 'ID',
ipProxyAppliedExitRegion: 'KR',
}),
/本次复测没有拿到国家码/
);
assert.equal(requestedCountries.length, 0);
assert.equal(events.logs.some((entry) => /已清空旧出口地区 KR/.test(entry.message)), true);
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 KR/.test(entry.message)), false);
});
test('Plus checkout billing normalizes legacy Korean postal code for GoPay address', async () => {
const requestedCountries = [];
const fetchRequests = [];
const { events, executor } = createExecutorHarness({
frames: [
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
],
stateByFrame: {
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
8: {
hasPayPal: false,
hasGoPay: false,
paypalCandidates: [],
gopayCandidates: [],
billingFieldsVisible: true,
countryText: 'United States',
},
},
getAddressSeedForCountry: (countryValue) => {
requestedCountries.push(countryValue);
return countryValue === 'KR' ? createKrAddressSeed() : createIdAddressSeed();
},
fetchImpl: async (url, init) => {
fetchRequests.push({ url, init });
return {
ok: true,
status: 200,
json: async () => ({
status: 'ok',
address: {
Address: '서울특별시 중구 세종대로 110',
Trans_Address: 'Sejong-daero 110, Jung-gu, Seoul',
City: 'Jung-gu',
State: 'Seoul',
Zip_Code: '150-300',
},
}),
};
},
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gopay',
plusCheckoutCountry: 'ID',
ipProxyAppliedExitRegion: 'KR',
});
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
assert.equal(requestedCountries[0], 'KR');
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'KR');
assert.equal(fillMessage.message.payload.addressSeed.source, 'meiguodizhi');
assert.equal(fillMessage.message.payload.addressSeed.fallback.address1, 'Sejong-daero 110, Jung-gu, Seoul');
assert.equal(fillMessage.message.payload.addressSeed.fallback.postalCode, '04524');
assert.match(fillMessage.message.payload.addressSeed.fallback.postalCode, /^\d{5}$/);
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
city: 'Jung-gu',
path: '/kr-address',
method: 'refresh',
});
assert.equal(events.logs.some((entry) => /GoPay 账单地址将按当前代理出口地区 KR/.test(entry.message)), true);
});
test('Plus checkout billing selects GoPay and waits for a GoPay redirect', async () => {
+6 -2
View File
@@ -180,11 +180,13 @@ test('collectSettingsPayload omits custom password and local sync settings in co
const api = new Function('normalizeIcloudTargetMailboxType', 'normalizeIcloudForwardMailProvider', `
let latestState = { contributionMode: true };
const window = {};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
const inputVpsUrl = { value: 'https://panel.example.com' };
const inputVpsPassword = { value: 'panel-secret' };
const inputSub2ApiUrl = { value: 'https://sub.example.com' };
@@ -237,9 +239,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
+10 -4
View File
@@ -85,11 +85,13 @@ test('collectSettingsPayload persists icloud target mailbox settings', () => {
const api = new Function(`
let latestState = { contributionMode: false };
const window = {};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: '' };
const selectTempEmailDomain = { value: '' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
@@ -136,9 +138,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
@@ -421,9 +425,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
+6 -2
View File
@@ -158,11 +158,13 @@ let latestState = {
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
};
const window = {};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
function getSelectedPlusPaymentMethod() { return 'paypal'; }
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
@@ -207,9 +209,11 @@ const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
@@ -293,6 +293,7 @@ return {
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
const api = new Function('normalizeIcloudTargetMailboxType', 'normalizeIcloudForwardMailProvider', `
const window = {};
let latestState = {
contributionMode: false,
mail2925UseAccountPool: false,
@@ -390,9 +391,11 @@ const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const selectHeroSmsCountry = {
value: '52',
selectedIndex: 0,
@@ -403,6 +406,7 @@ function normalizeCloudflareDomainValue(value) { return String(value || '').trim
function getCloudflareTempEmailDomainsFromState() { return { domains: [], activeDomain: '' }; }
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedPlusPaymentMethod() { return 'paypal'; }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() { return { gmailBaseEmail: '', mail2925BaseEmail: '', emailPrefix: '' }; }
@@ -473,7 +477,7 @@ return { collectSettingsPayload };
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
assert.deepStrictEqual(payload.heroSmsCountryFallback, [{ id: 16, label: 'United Kingdom' }]);
assert.equal(payload.fiveSimApiKey, '');
assert.equal(payload.fiveSimCountryId, 'england');
assert.equal(payload.fiveSimCountryId, 'vietnam');
});
test('switchPhoneSmsProvider saves API keys independently when the select value has already changed', async () => {
@@ -487,18 +491,20 @@ let latestState = {
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
heroSmsCountryFallback: [],
fiveSimCountryId: 'england',
fiveSimCountryLabel: '英国 (England)',
fiveSimCountryId: 'vietnam',
fiveSimCountryLabel: '越南 (Vietnam)',
fiveSimCountryFallback: [],
fiveSimOperator: 'any',
};
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
const inputHeroSmsApiKey = { value: 'hero-live' };
const inputHeroSmsMaxPrice = { value: '0.22' };
@@ -530,7 +536,7 @@ function getSelectedHeroSmsCountryOption() {
}
function syncHeroSmsFallbackSelectionOrderFromSelect() {
return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM
? [{ id: 'england', label: '英国 (England)' }]
? [{ id: 'vietnam', label: '越南 (Vietnam)' }]
: [{ id: 52, label: 'Thailand' }];
}
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
@@ -574,14 +580,40 @@ return {
assert.equal(api.savedPayload.fiveSimApiKey, 'five-live');
});
test('formatPhoneSmsPriceEntriesSummary treats HeroSMS physicalCount=0 as out of stock even when count is positive', () => {
const api = new Function(`
${extractFunction('normalizeHeroSmsPriceForPreview')}
${extractFunction('collectHeroSmsPriceEntriesForPreview')}
${extractFunction('formatPhoneSmsPriceEntriesSummary')}
return { formatPhoneSmsPriceEntriesSummary };
`)();
const summary = api.formatPhoneSmsPriceEntriesSummary({
52: {
dr: {
cost: 0.05,
count: 3,
physicalCount: 0,
},
},
});
assert.deepStrictEqual(summary.inStockPrices, []);
assert.deepStrictEqual(summary.allPrices, [0.05]);
assert.equal(summary.entries[0].inStock, false);
assert.equal(summary.entries[0].stockCount, 0);
});
test('previewHeroSmsPriceTiers prefers 5sim products price for buy-compatible any operator', async () => {
const api = new Function(`
let latestState = { phoneSmsProvider: '5sim', fiveSimOperator: 'any' };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const inputHeroSmsMaxPrice = { value: '' };
const inputHeroSmsApiKey = { value: '' };
const inputFiveSimOperator = { value: 'any' };