fix: improve iCloud polling and add-phone handoff
This commit is contained in:
@@ -188,6 +188,174 @@ test('step 7 exits internal retry loop immediately when add-phone is detected',
|
||||
);
|
||||
});
|
||||
|
||||
test('step 7 hands direct add-phone to shared phone verification when enabled', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
refreshCalls: 0,
|
||||
phoneCalls: [],
|
||||
completions: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
events.completions.push({ step, payload });
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
phoneVerificationEnabled: true,
|
||||
}),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 91 : 0),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
phoneVerificationHelpers: {
|
||||
completePhoneVerificationFlow: async (tabId, pageState, options) => {
|
||||
events.phoneCalls.push({ tabId, pageState, options });
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize/resume' };
|
||||
},
|
||||
},
|
||||
refreshOAuthUrlBeforeStep6: async () => {
|
||||
events.refreshCalls += 1;
|
||||
return `https://oauth.example/${events.refreshCalls}`;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
phoneVerificationEnabled: true,
|
||||
});
|
||||
|
||||
assert.equal(events.refreshCalls, 1);
|
||||
assert.deepStrictEqual(events.phoneCalls, [
|
||||
{
|
||||
tabId: 91,
|
||||
pageState: {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
state: 'add_phone_page',
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
},
|
||||
options: {
|
||||
step: 7,
|
||||
visibleStep: 7,
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(events.completions, [
|
||||
{
|
||||
step: 7,
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
phoneVerification: true,
|
||||
loginPhoneVerification: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 direct add-phone stays fatal when phone verification is disabled', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
phoneCalls: 0,
|
||||
completions: 0,
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {
|
||||
events.completions += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: false }),
|
||||
getTabId: async () => 91,
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
phoneVerificationHelpers: {
|
||||
completePhoneVerificationFlow: async () => {
|
||||
events.phoneCalls += 1;
|
||||
return {};
|
||||
},
|
||||
},
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: false }),
|
||||
/手机号页面.*接码|phone verification/i
|
||||
);
|
||||
assert.equal(events.phoneCalls, 0);
|
||||
assert.equal(events.completions, 0);
|
||||
});
|
||||
|
||||
test('step 7 propagates fatal errors from shared add-phone verification', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
phoneCalls: 0,
|
||||
completions: 0,
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {
|
||||
events.completions += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: true }),
|
||||
getTabId: async () => 91,
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
phoneVerificationHelpers: {
|
||||
completePhoneVerificationFlow: async () => {
|
||||
events.phoneCalls += 1;
|
||||
throw new Error('步骤 9:没有可用手机号。');
|
||||
},
|
||||
},
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: https://auth.openai.com/add-phone');
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep7({ email: 'user@example.com', password: 'secret', phoneVerificationEnabled: true }),
|
||||
/没有可用手机号/
|
||||
);
|
||||
assert.equal(events.phoneCalls, 1);
|
||||
assert.equal(events.completions, 0);
|
||||
});
|
||||
|
||||
test('step 7 starts a new oauth timeout window for each refreshed oauth url', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -6,6 +6,43 @@ const source = fs.readFileSync('background/verification-flow.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
|
||||
|
||||
function createVerificationFlowTestHelpers(overrides = {}) {
|
||||
return api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
remove: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
CLOUD_MAIL_PROVIDER: 'cloudmail',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollCloudMailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToMailContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('verification flow keeps 2925 polling cadence in the default payload', () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
@@ -43,6 +80,68 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
|
||||
assert.equal(step8Payload.intervalMs, 15000);
|
||||
});
|
||||
|
||||
test('verification flow keeps iCloud step 4 polling at least five attempts under a short remaining budget', async () => {
|
||||
const pollRequests = [];
|
||||
const helpers = createVerificationFlowTestHelpers({
|
||||
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
|
||||
pollRequests.push({ payload: message.payload, options });
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.pollFreshVerificationCode(
|
||||
4,
|
||||
{ email: 'user@example.com', mailProvider: 'icloud', lastSignupCode: null },
|
||||
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
|
||||
{
|
||||
filterAfterTimestamp: 123456,
|
||||
getRemainingTimeMs: async () => 3000,
|
||||
maxResendRequests: 0,
|
||||
}
|
||||
),
|
||||
/邮箱轮询结束|无法获取/
|
||||
);
|
||||
|
||||
assert.equal(pollRequests.length, 1);
|
||||
const [{ payload, options }] = pollRequests;
|
||||
assert.equal(payload.maxAttempts >= 5, true);
|
||||
assert.equal(payload.intervalMs >= 3000, true);
|
||||
assert.equal(options.responseTimeoutMs >= (payload.maxAttempts * payload.intervalMs) + 10000, true);
|
||||
});
|
||||
|
||||
test('verification flow keeps iCloud step 8 polling at least five attempts before no-code failure', async () => {
|
||||
const pollRequests = [];
|
||||
const helpers = createVerificationFlowTestHelpers({
|
||||
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
|
||||
pollRequests.push({ payload: message.payload, options });
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.pollFreshVerificationCodeWithResendInterval(
|
||||
8,
|
||||
{ email: 'user@example.com', mailProvider: 'icloud', lastLoginCode: null },
|
||||
{ source: 'icloud-mail', provider: 'icloud', label: 'iCloud 邮箱' },
|
||||
{
|
||||
filterAfterTimestamp: 123456,
|
||||
getRemainingTimeMs: async () => 3000,
|
||||
maxResendRequests: 0,
|
||||
resendIntervalMs: 25000,
|
||||
}
|
||||
),
|
||||
/空轮询循环|停止当前链路/
|
||||
);
|
||||
|
||||
assert.equal(pollRequests.length >= 1, true);
|
||||
assert.equal(pollRequests.every(({ payload }) => payload.maxAttempts >= 5), true);
|
||||
assert.equal(
|
||||
pollRequests.every(({ payload, options }) => options.responseTimeoutMs >= (payload.maxAttempts * payload.intervalMs) + 10000),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('verification flow only enables 2925 target email matching in receive mode', () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
@@ -1599,8 +1698,8 @@ test('verification flow stops iCloud poll-only loop after repeated no-code round
|
||||
assert.equal(resendRequests, 0);
|
||||
});
|
||||
|
||||
test('verification flow caps iCloud polling response timeout to avoid long silent stalls', async () => {
|
||||
const pollTimeouts = [];
|
||||
test('verification flow derives iCloud polling response timeout from the configured polling window', async () => {
|
||||
const pollRequests = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
@@ -1630,8 +1729,9 @@ test('verification flow caps iCloud polling response timeout to avoid long silen
|
||||
}
|
||||
return {};
|
||||
},
|
||||
sendToMailContentScriptResilient: async (_mail, _message, options = {}) => {
|
||||
pollTimeouts.push({
|
||||
sendToMailContentScriptResilient: async (_mail, message, options = {}) => {
|
||||
pollRequests.push({
|
||||
payload: message.payload,
|
||||
timeoutMs: Number(options.timeoutMs) || 0,
|
||||
responseTimeoutMs: Number(options.responseTimeoutMs) || 0,
|
||||
});
|
||||
@@ -1659,10 +1759,13 @@ test('verification flow caps iCloud polling response timeout to avoid long silen
|
||||
/空轮询循环|停止当前链路/
|
||||
);
|
||||
|
||||
assert.equal(pollTimeouts.length > 0, true);
|
||||
assert.equal(pollTimeouts.every(({ timeoutMs }) => timeoutMs > 0 && timeoutMs <= 22000), true);
|
||||
assert.equal(pollRequests.length > 0, true);
|
||||
assert.equal(
|
||||
pollTimeouts.every(({ responseTimeoutMs }) => responseTimeoutMs > 0 && responseTimeoutMs <= 18000),
|
||||
pollRequests.every(({ payload, responseTimeoutMs }) => responseTimeoutMs >= (payload.maxAttempts * payload.intervalMs) + 10000),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
pollRequests.every(({ timeoutMs, responseTimeoutMs }) => timeoutMs >= responseTimeoutMs),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user