修改登陆及后续逻辑,统一使用一个逻辑
This commit is contained in:
@@ -44,4 +44,5 @@ test('logging/status add-phone detection ignores step 2 phone-entry switch failu
|
||||
true
|
||||
);
|
||||
assert.equal(loggingStatus.getLoginAuthStateLabel('phone_verification_page'), '手机验证码页');
|
||||
assert.equal(loggingStatus.getLoginAuthStateLabel('oauth_consent_page'), 'OAuth 授权页');
|
||||
});
|
||||
|
||||
@@ -59,6 +59,8 @@ function createRouter(overrides = {}) {
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getSourceLabel: () => '',
|
||||
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
|
||||
getStepDefinitionForState: overrides.getStepDefinitionForState,
|
||||
getStepIdsForState: overrides.getStepIdsForState,
|
||||
getTabId: overrides.getTabId || (async () => null),
|
||||
getStopRequested: () => false,
|
||||
handleAutoRunLoopUnhandledError: async () => {},
|
||||
@@ -184,6 +186,49 @@ test('message router skips step 5 when step 4 reports already logged-in transiti
|
||||
assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。');
|
||||
});
|
||||
|
||||
test('message router skips login-code step when oauth login lands on consent page', async () => {
|
||||
const stepKeys = {
|
||||
7: 'oauth-login',
|
||||
8: 'fetch-login-code',
|
||||
9: 'confirm-oauth',
|
||||
};
|
||||
const { router, events } = createRouter({
|
||||
state: { stepStatuses: { 7: 'completed', 8: 'pending', 9: 'pending' } },
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }),
|
||||
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
});
|
||||
|
||||
await router.handleStepData(7, {
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.stepStatuses, [{ step: 8, status: 'skipped' }]);
|
||||
assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 8/.test(message)), true);
|
||||
});
|
||||
|
||||
test('message router skips Plus login-code step when oauth login lands on consent page', async () => {
|
||||
const stepKeys = {
|
||||
10: 'oauth-login',
|
||||
11: 'fetch-login-code',
|
||||
12: 'confirm-oauth',
|
||||
13: 'platform-verify',
|
||||
};
|
||||
const { router, events } = createRouter({
|
||||
state: { plusModeEnabled: true, stepStatuses: { 10: 'completed', 11: 'pending', 12: 'pending', 13: 'pending' } },
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: stepKeys[step] || '' }),
|
||||
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
||||
});
|
||||
|
||||
await router.handleStepData(10, {
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.stepStatuses, [{ step: 11, status: 'skipped' }]);
|
||||
assert.equal(events.logs.some(({ message }) => /OAuth 授权页.*步骤 11/.test(message)), true);
|
||||
});
|
||||
|
||||
test('message router finalizes step 3 before marking it completed', async () => {
|
||||
const { router, events } = createRouter();
|
||||
|
||||
|
||||
@@ -178,6 +178,55 @@ test('step 7 starts a new oauth timeout window for each refreshed oauth url', as
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 forwards direct OAuth consent skip metadata when completing', 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 = {
|
||||
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' }),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => ({
|
||||
step6Outcome: 'success',
|
||||
state: 'oauth_consent_page',
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
}),
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
visibleStep: 10,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(events.completions, [
|
||||
{
|
||||
step: 10,
|
||||
payload: {
|
||||
loginVerificationRequestedAt: null,
|
||||
skipLoginVerificationStep: true,
|
||||
directOAuthConsentPage: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 7 stops immediately when management secret is missing', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -85,8 +85,78 @@ test('step 8 submits login verification directly without replaying step 7', asyn
|
||||
{ step8VerificationTargetEmail: 'display.user@example.com' },
|
||||
]);
|
||||
assert.deepStrictEqual(calls.ensureReadyOptions, [
|
||||
{ timeoutMs: 5000 },
|
||||
{ visibleStep: 8, authLoginStep: 7, timeoutMs: 5000 },
|
||||
]);
|
||||
assert.equal(calls.resolveOptions.completionStep, 8);
|
||||
});
|
||||
|
||||
test('Plus login-code step reuses step 8 verification logic but completes visible step 11', async () => {
|
||||
let resolvedStep = null;
|
||||
let resolvedOptions = null;
|
||||
let readyOptions = null;
|
||||
const remainingStepCalls = [];
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async (options) => {
|
||||
readyOptions = options;
|
||||
return { state: 'verification_page', displayedEmail: 'plus.user@example.com' };
|
||||
},
|
||||
rerunStep7ForStep8Recovery: async () => {},
|
||||
getOAuthFlowRemainingMs: async (details) => {
|
||||
remainingStepCalls.push(details.step);
|
||||
return 9000;
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, details) => {
|
||||
remainingStepCalls.push(details.step);
|
||||
return Math.min(defaultTimeoutMs, 9000);
|
||||
},
|
||||
getMailConfig: () => ({
|
||||
provider: 'qq',
|
||||
label: 'QQ 邮箱',
|
||||
source: 'mail-qq',
|
||||
url: 'https://mail.qq.com',
|
||||
navigateOnReuse: false,
|
||||
}),
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret', plusModeEnabled: true }),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
isVerificationMailPollingError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
resolveVerificationStep: async (step, _state, _mail, options) => {
|
||||
resolvedStep = step;
|
||||
resolvedOptions = options;
|
||||
await options.getRemainingTimeMs({ actionLabel: '登录验证码流程' });
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
visibleStep: 11,
|
||||
plusModeEnabled: true,
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(resolvedStep, 8);
|
||||
assert.equal(resolvedOptions.completionStep, 11);
|
||||
assert.equal(resolvedOptions.targetEmail, 'plus.user@example.com');
|
||||
assert.deepStrictEqual(readyOptions, { visibleStep: 11, authLoginStep: 10, timeoutMs: 9000 });
|
||||
assert.deepStrictEqual(remainingStepCalls, [11, 11]);
|
||||
});
|
||||
|
||||
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
|
||||
|
||||
@@ -216,3 +216,43 @@ return {
|
||||
assert.equal(modalPayload.actions[1].id, 'add_phone');
|
||||
assert.equal(modalPayload.actions[1].label, '出现手机号验证');
|
||||
});
|
||||
|
||||
test('sidepanel custom verification dialog exposes add-phone action for Plus login code step', async () => {
|
||||
const bundle = [
|
||||
extractFunction('getCustomVerificationPromptCopy'),
|
||||
extractFunction('openCustomVerificationConfirmDialog'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let openActionModalPayload = null;
|
||||
|
||||
async function openActionModal(options) {
|
||||
openActionModalPayload = options;
|
||||
return options.buildResult('add_phone');
|
||||
}
|
||||
|
||||
async function openConfirmModal() {
|
||||
throw new Error('Plus login code step should use action modal');
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
getCustomVerificationPromptCopy,
|
||||
openCustomVerificationConfirmDialog,
|
||||
getOpenActionModalPayload: () => openActionModalPayload,
|
||||
};
|
||||
`)();
|
||||
|
||||
const prompt = api.getCustomVerificationPromptCopy(11);
|
||||
assert.equal(prompt.phoneActionLabel, '出现手机号验证');
|
||||
|
||||
const result = await api.openCustomVerificationConfirmDialog(11);
|
||||
assert.deepEqual(result, {
|
||||
confirmed: false,
|
||||
addPhoneDetected: true,
|
||||
});
|
||||
|
||||
const modalPayload = api.getOpenActionModalPayload();
|
||||
assert.equal(modalPayload.actions[1].id, 'add_phone');
|
||||
});
|
||||
|
||||
@@ -44,14 +44,15 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'paypal-approve',
|
||||
'plus-checkout-return',
|
||||
'oauth-login',
|
||||
'fetch-login-code',
|
||||
'confirm-oauth',
|
||||
'platform-verify',
|
||||
]
|
||||
);
|
||||
assert.equal(plusSteps.some((step) => step.key === 'clear-login-cookies'), false);
|
||||
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), false);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 12);
|
||||
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 13);
|
||||
});
|
||||
|
||||
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
|
||||
|
||||
@@ -215,17 +215,20 @@ return {
|
||||
consentReady: true,
|
||||
});
|
||||
|
||||
const inspected = api.inspectLoginAuthState();
|
||||
assert.strictEqual(inspected.state, 'oauth_consent_page');
|
||||
|
||||
const snapshot = api.normalizeStep6Snapshot({
|
||||
state: 'oauth_consent_page',
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
|
||||
assert.strictEqual(snapshot.state, 'unknown', '第六步应忽略 oauth_consent_page 状态');
|
||||
assert.strictEqual(snapshot.state, 'oauth_consent_page', '第六步应保留 oauth_consent_page 状态');
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
!extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
|
||||
'inspectLoginAuthState 不应再产出 oauth_consent_page 状态'
|
||||
extractFunction('inspectLoginAuthState').includes("state: 'oauth_consent_page'"),
|
||||
'inspectLoginAuthState 应产出 oauth_consent_page 状态'
|
||||
);
|
||||
|
||||
console.log('step6 login state tests passed');
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/signup-page.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const char = source[index];
|
||||
if (char === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (char === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (char === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const char = source[end];
|
||||
if (char === '{') depth += 1;
|
||||
if (char === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('password submit treats direct OAuth consent as a login-code skip', async () => {
|
||||
const api = new Function(`
|
||||
const location = { href: 'https://auth.openai.com/authorize' };
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: 'oauth_consent_page',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {
|
||||
throw new Error('should not wait once oauth consent is detected');
|
||||
}
|
||||
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||
${extractFunction('createStep6RecoverableResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('getStep6OptionMessage')}
|
||||
${extractFunction('resolveStep6PostSubmitSnapshot')}
|
||||
${extractFunction('waitForStep6PostSubmitTransition')}
|
||||
${extractFunction('waitForStep6PasswordSubmitTransition')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return waitForStep6PasswordSubmitTransition(123, 1000);
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const transition = await api.run();
|
||||
|
||||
assert.equal(transition.action, 'done');
|
||||
assert.equal(transition.result.state, 'oauth_consent_page');
|
||||
assert.equal(transition.result.skipLoginVerificationStep, true);
|
||||
assert.equal(transition.result.directOAuthConsentPage, true);
|
||||
assert.equal(transition.result.loginVerificationRequestedAt, null);
|
||||
});
|
||||
|
||||
test('step 7 entry succeeds when the auth page is already on OAuth consent', async () => {
|
||||
const logs = [];
|
||||
const api = new Function(`
|
||||
const location = { href: 'https://auth.openai.com/authorize' };
|
||||
const logs = arguments[0];
|
||||
|
||||
function inspectLoginAuthState() {
|
||||
return {
|
||||
state: 'oauth_consent_page',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
function log(message, level = 'info') {
|
||||
logs.push({ message, level });
|
||||
}
|
||||
|
||||
${extractFunction('createStep6SuccessResult')}
|
||||
${extractFunction('createStep6OAuthConsentSuccessResult')}
|
||||
${extractFunction('normalizeStep6Snapshot')}
|
||||
${extractFunction('waitForKnownLoginAuthState')}
|
||||
${extractFunction('step6_login')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return step6_login({ email: 'user@example.com' });
|
||||
},
|
||||
};
|
||||
`)(logs);
|
||||
|
||||
const result = await api.run();
|
||||
|
||||
assert.equal(result.step6Outcome, 'success');
|
||||
assert.equal(result.state, 'oauth_consent_page');
|
||||
assert.equal(result.skipLoginVerificationStep, true);
|
||||
assert.equal(result.directOAuthConsentPage, true);
|
||||
assert.equal(logs.some(({ level }) => level === 'ok'), true);
|
||||
});
|
||||
@@ -833,6 +833,69 @@ test('verification flow uses configured login resend count for step 8', async ()
|
||||
assert.equal(pollCalls, 3);
|
||||
});
|
||||
|
||||
test('verification flow can complete Plus visible login-code step with shared step 8 semantics', async () => {
|
||||
const completed = [];
|
||||
const fillMessages = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
},
|
||||
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') {
|
||||
fillMessages.push(message);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => ({ code: '654321', emailTimestamp: 456 }),
|
||||
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 邮箱' },
|
||||
{
|
||||
completionStep: 11,
|
||||
requestFreshCodeFirst: false,
|
||||
maxResendRequests: 0,
|
||||
resendIntervalMs: 0,
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(fillMessages.map((message) => message.step), [8]);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 11,
|
||||
payload: {
|
||||
emailTimestamp: 456,
|
||||
code: '654321',
|
||||
phoneVerificationRequired: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow waits during resend cooldown instead of tight-looping', async () => {
|
||||
const sleepCalls = [];
|
||||
let pollCalls = 0;
|
||||
|
||||
Reference in New Issue
Block a user