feat: 增加 OAuth 流程6分钟超时处理,优化验证码请求时间窗口逻辑

This commit is contained in:
QLHazyCoder
2026-04-17 19:12:06 +08:00
parent a827ede6f4
commit 4add7477d1
11 changed files with 500 additions and 92 deletions
@@ -76,3 +76,56 @@ test('step 7 retries up to configured limit and then fails', async () => {
assert.equal(events.sendCalls, 3);
assert.equal(events.completed, 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 = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
startedWindows: [],
timeoutRequests: [],
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, options) => {
events.timeoutRequests.push({ defaultTimeoutMs, options });
return 5000;
},
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async (_source, _message, options) => ({
step6Outcome: 'success',
usedTimeoutMs: options.timeoutMs,
}),
shouldSkipLoginVerificationForCpaCallback: () => false,
skipLoginVerificationStepsForCpaCallback: async () => {},
startOAuthFlowTimeoutWindow: async (payload) => {
events.startedWindows.push(payload);
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7({ email: 'user@example.com', password: 'secret' });
assert.deepStrictEqual(events.startedWindows, [
{ step: 7, oauthUrl: 'https://oauth.example/latest' },
]);
assert.deepStrictEqual(events.timeoutRequests, [
{
defaultTimeoutMs: 180000,
options: {
step: 7,
actionLabel: 'OAuth 登录并进入验证码页',
},
},
]);
});
+26 -6
View File
@@ -9,10 +9,13 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep
test('step 8 refreshes CPA oauth via step 7 replay before submitting verification code', async () => {
const calls = {
ensureReady: 0,
ensureReadyOptions: [],
executeStep7: 0,
sleep: [],
resolveOptions: null,
};
const realDateNow = Date.now;
Date.now = () => 123456;
const executor = api.createStep8Executor({
addLog: async () => {},
@@ -23,13 +26,16 @@ test('step 8 refreshes CPA oauth via step 7 replay before submitting verificatio
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => {
ensureStep8VerificationPageReady: async (options) => {
calls.ensureReady += 1;
calls.ensureReadyOptions.push(options || null);
return { state: 'verification_page' };
},
executeStep7: async () => {
calls.executeStep7 += 1;
},
getOAuthFlowRemainingMs: async () => 5000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 5000),
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
@@ -61,17 +67,28 @@ test('step 8 refreshes CPA oauth via step 7 replay before submitting verificatio
throwIfStopped: () => {},
});
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
try {
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
} finally {
Date.now = realDateNow;
}
assert.equal(typeof calls.resolveOptions.beforeSubmit, 'function');
assert.equal(calls.ensureReady, 2);
assert.equal(calls.executeStep7, 1);
assert.deepStrictEqual(calls.sleep, [1200]);
assert.equal(calls.resolveOptions.filterAfterTimestamp, 123456);
assert.equal(typeof calls.resolveOptions.getRemainingTimeMs, 'function');
assert.equal(await calls.resolveOptions.getRemainingTimeMs({ actionLabel: '登录验证码流程' }), 5000);
assert.equal(calls.resolveOptions.resendIntervalMs, 25000);
assert.deepStrictEqual(calls.ensureReadyOptions, [
{ timeoutMs: 5000 },
{ timeoutMs: 5000 },
]);
});
test('step 8 disables resend interval for 2925 mailbox polling', async () => {
@@ -88,6 +105,8 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
executeStep7: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
@@ -124,4 +143,5 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.beforeSubmit, undefined);
assert.equal(typeof capturedOptions.getRemainingTimeMs, 'function');
});
+79 -19
View File
@@ -109,6 +109,70 @@ test('verification flow runs beforeSubmit hook before filling the code', async (
]);
});
test('verification flow caps mail polling timeout to the remaining oauth budget', async () => {
const mailPollCalls = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
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 () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
return {};
}
return {};
},
sendToMailContentScriptResilient: async (_mail, message, options) => {
mailPollCalls.push({
payload: message.payload,
options,
});
return { code: '654321', emailTimestamp: 123 };
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
lastLoginCode: null,
},
{ provider: 'qq', label: 'QQ 邮箱' },
{
getRemainingTimeMs: async () => 5000,
resendIntervalMs: 0,
}
);
assert.ok(mailPollCalls.length >= 1);
assert.equal(mailPollCalls[0].options.timeoutMs, 5000);
assert.equal(mailPollCalls[0].options.responseTimeoutMs, 5000);
assert.equal(mailPollCalls[0].payload.maxAttempts, 2);
});
test('verification flow keeps Hotmail request timestamp filtering on the first poll', async () => {
const pollPayloads = [];
@@ -162,10 +226,8 @@ test('verification flow keeps Hotmail request timestamp filtering on the first p
assert.equal(pollPayloads[0].filterAfterTimestamp, 87654);
});
test('verification flow refreshes Hotmail signup filter timestamp after step 4 resend', async () => {
test('verification flow keeps fixed filter timestamp after step 4 resend', async () => {
const pollPayloads = [];
const realDateNow = Date.now;
Date.now = () => 200000;
let submitCount = 0;
@@ -213,24 +275,22 @@ test('verification flow refreshes Hotmail signup filter timestamp after step 4 r
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
try {
await helpers.resolveVerificationStep(
4,
{
email: 'user@example.com',
signupVerificationRequestedAt: 100000,
lastSignupCode: null,
},
{ provider: 'hotmail-api', label: 'Hotmail' },
{}
);
} finally {
Date.now = realDateNow;
}
await helpers.resolveVerificationStep(
4,
{
email: 'user@example.com',
signupVerificationRequestedAt: 100000,
lastSignupCode: null,
},
{ provider: 'hotmail-api', label: 'Hotmail' },
{
filterAfterTimestamp: 123456,
}
);
assert.equal(pollPayloads.length, 2);
assert.equal(pollPayloads[0].filterAfterTimestamp, 85000);
assert.equal(pollPayloads[1].filterAfterTimestamp, 185000);
assert.equal(pollPayloads[0].filterAfterTimestamp, 123456);
assert.equal(pollPayloads[1].filterAfterTimestamp, 123456);
});
test('verification flow uses configured signup resend count for step 4', async () => {