fix(flow): harden proxy phone and checkout recovery paths
This commit is contained in:
@@ -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}$/);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' };
|
||||
|
||||
Reference in New Issue
Block a user