fix: enhance step 7 login identifier resolution and add tests for phone/email handling
This commit is contained in:
@@ -40,29 +40,64 @@
|
|||||||
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStep7IdentifierType(value = '') {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
|
return normalized === 'phone' || normalized === 'email' ? normalized : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStep7SignupMethod(value = '') {
|
||||||
|
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseConfiguredPhoneSignup(state = {}) {
|
||||||
|
return normalizeStep7SignupMethod(state?.signupMethod) === 'phone'
|
||||||
|
&& Boolean(state?.phoneVerificationEnabled)
|
||||||
|
&& !Boolean(state?.plusModeEnabled)
|
||||||
|
&& !Boolean(state?.contributionMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') {
|
||||||
|
const explicitIdentifierType = normalizeStep7IdentifierType(state?.accountIdentifierType);
|
||||||
|
if (explicitIdentifierType) {
|
||||||
|
return explicitIdentifierType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod);
|
||||||
|
if (frozenSignupMethod) {
|
||||||
|
return frozenSignupMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseConfiguredPhoneSignup(state)) {
|
||||||
|
return 'phone';
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeStep7IdentifierType(fallbackType) || 'email';
|
||||||
|
}
|
||||||
|
|
||||||
async function executeStep7(state) {
|
async function executeStep7(state) {
|
||||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||||
const resolvedIdentifierType = String(
|
const resolvedIdentifierType = resolveStep7LoginIdentifierType(state);
|
||||||
state?.accountIdentifierType
|
const phoneNumber = resolvedIdentifierType === 'phone'
|
||||||
|| (state?.signupPhoneNumber ? 'phone' : '')
|
? String(
|
||||||
|| ''
|
|
||||||
).trim().toLowerCase() === 'phone'
|
|
||||||
? 'phone'
|
|
||||||
: 'email';
|
|
||||||
const phoneNumber = String(
|
|
||||||
state?.signupPhoneNumber
|
state?.signupPhoneNumber
|
||||||
|| (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
|| (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone' ? state?.accountIdentifier : '')
|
||||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||||
|| state?.signupPhoneActivation?.phoneNumber
|
|| state?.signupPhoneActivation?.phoneNumber
|
||||||
|| ''
|
|| ''
|
||||||
).trim();
|
).trim()
|
||||||
const email = String(
|
: '';
|
||||||
|
const email = resolvedIdentifierType === 'email'
|
||||||
|
? String(
|
||||||
state?.email
|
state?.email
|
||||||
|| (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '')
|
|| (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'email' ? state?.accountIdentifier : '')
|
||||||
|| ''
|
|| ''
|
||||||
).trim();
|
).trim()
|
||||||
if (!email && !phoneNumber) {
|
: '';
|
||||||
|
if (
|
||||||
|
(resolvedIdentifierType === 'phone' && !phoneNumber)
|
||||||
|
|| (resolvedIdentifierType !== 'phone' && !email)
|
||||||
|
) {
|
||||||
throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。');
|
throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,25 +110,23 @@
|
|||||||
try {
|
try {
|
||||||
const currentState = attempt === 1 ? state : await getState();
|
const currentState = attempt === 1 ? state : await getState();
|
||||||
const password = currentState.password || currentState.customPassword || '';
|
const password = currentState.password || currentState.customPassword || '';
|
||||||
const currentIdentifierType = String(
|
const currentIdentifierType = resolveStep7LoginIdentifierType(currentState, resolvedIdentifierType);
|
||||||
currentState?.accountIdentifierType
|
const currentPhoneNumber = currentIdentifierType === 'phone'
|
||||||
|| (currentState?.signupPhoneNumber ? 'phone' : '')
|
? String(
|
||||||
|| resolvedIdentifierType
|
|
||||||
).trim().toLowerCase() === 'phone'
|
|
||||||
? 'phone'
|
|
||||||
: 'email';
|
|
||||||
const currentPhoneNumber = String(
|
|
||||||
currentState?.signupPhoneNumber
|
currentState?.signupPhoneNumber
|
||||||
|| (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '')
|
|| (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'phone' ? currentState?.accountIdentifier : '')
|
||||||
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|
||||||
|| currentState?.signupPhoneActivation?.phoneNumber
|
|| currentState?.signupPhoneActivation?.phoneNumber
|
||||||
|| phoneNumber
|
|| phoneNumber
|
||||||
).trim();
|
).trim()
|
||||||
const currentEmail = String(
|
: '';
|
||||||
|
const currentEmail = currentIdentifierType === 'email'
|
||||||
|
? String(
|
||||||
currentState?.email
|
currentState?.email
|
||||||
|| (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '')
|
|| (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'email' ? currentState?.accountIdentifier : '')
|
||||||
|| email
|
|| email
|
||||||
).trim();
|
).trim()
|
||||||
|
: '';
|
||||||
const accountIdentifier = currentIdentifierType === 'phone'
|
const accountIdentifier = currentIdentifierType === 'phone'
|
||||||
? currentPhoneNumber
|
? currentPhoneNumber
|
||||||
: currentEmail;
|
: currentEmail;
|
||||||
|
|||||||
@@ -5202,9 +5202,9 @@ async function step6OpenLoginEntry(payload, snapshot) {
|
|||||||
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
|
const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7;
|
||||||
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||||
const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber);
|
const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber);
|
||||||
const trigger = preferPhoneLogin
|
const genericEntryTrigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger();
|
||||||
? (currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger())
|
const phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger();
|
||||||
: (currentSnapshot.loginEntryTrigger || findLoginEntryTrigger());
|
const trigger = genericEntryTrigger || (preferPhoneLogin ? phoneEntryTrigger : null);
|
||||||
if (!trigger || !isActionEnabled(trigger)) {
|
if (!trigger || !isActionEnabled(trigger)) {
|
||||||
return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, {
|
return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, {
|
||||||
message: preferPhoneLogin
|
message: preferPhoneLogin
|
||||||
|
|||||||
@@ -358,6 +358,106 @@ test('step 7 forwards phone login identity payload when account identifier is ph
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 7 keeps Plus email login even when phone sms runtime exists', 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 = {
|
||||||
|
payloads: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const executor = api.createStep7Executor({
|
||||||
|
addLog: async () => {},
|
||||||
|
completeStepFromBackground: async () => {},
|
||||||
|
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||||
|
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||||
|
getState: async () => ({
|
||||||
|
plusModeEnabled: true,
|
||||||
|
phoneVerificationEnabled: true,
|
||||||
|
signupMethod: 'phone',
|
||||||
|
email: 'plus.user@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
signupPhoneNumber: '+441111111111',
|
||||||
|
}),
|
||||||
|
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||||
|
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||||
|
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||||
|
reuseOrCreateTab: async () => {},
|
||||||
|
sendToContentScriptResilient: async (_sourceName, message) => {
|
||||||
|
events.payloads.push(message.payload);
|
||||||
|
return {
|
||||||
|
step6Outcome: 'success',
|
||||||
|
state: 'verification_page',
|
||||||
|
loginVerificationRequestedAt: 123456,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
STEP6_MAX_ATTEMPTS: 3,
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executeStep7({
|
||||||
|
plusModeEnabled: true,
|
||||||
|
phoneVerificationEnabled: true,
|
||||||
|
signupMethod: 'phone',
|
||||||
|
email: 'plus.user@example.com',
|
||||||
|
password: 'secret',
|
||||||
|
signupPhoneNumber: '+441111111111',
|
||||||
|
visibleStep: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(events.payloads[0].loginIdentifierType, 'email');
|
||||||
|
assert.equal(events.payloads[0].email, 'plus.user@example.com');
|
||||||
|
assert.equal(events.payloads[0].phoneNumber, '');
|
||||||
|
assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', 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 = {
|
||||||
|
payloads: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const executor = api.createStep7Executor({
|
||||||
|
addLog: async () => {},
|
||||||
|
completeStepFromBackground: async () => {},
|
||||||
|
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||||
|
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||||
|
getState: async () => ({
|
||||||
|
phoneVerificationEnabled: true,
|
||||||
|
signupMethod: 'phone',
|
||||||
|
signupPhoneNumber: '+447780579093',
|
||||||
|
}),
|
||||||
|
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||||
|
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||||
|
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||||
|
reuseOrCreateTab: async () => {},
|
||||||
|
sendToContentScriptResilient: async (_sourceName, message) => {
|
||||||
|
events.payloads.push(message.payload);
|
||||||
|
return {
|
||||||
|
step6Outcome: 'success',
|
||||||
|
state: 'phone_verification_page',
|
||||||
|
loginVerificationRequestedAt: 987654,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
STEP6_MAX_ATTEMPTS: 3,
|
||||||
|
throwIfStopped: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executeStep7({
|
||||||
|
phoneVerificationEnabled: true,
|
||||||
|
signupMethod: 'phone',
|
||||||
|
signupPhoneNumber: '+447780579093',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(events.payloads[0].loginIdentifierType, 'phone');
|
||||||
|
assert.equal(events.payloads[0].phoneNumber, '+447780579093');
|
||||||
|
assert.equal(events.payloads[0].email, '');
|
||||||
|
});
|
||||||
|
|
||||||
test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => {
|
test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => {
|
||||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
|
|||||||
@@ -184,6 +184,50 @@ test('step 7 treats unified OpenAI login page as email input despite phone optio
|
|||||||
assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
|
assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step 7 clicks the generic other-account entry before phone entry', async () => {
|
||||||
|
const api = new Function(`
|
||||||
|
const clicks = [];
|
||||||
|
const genericEntry = { id: 'generic', textContent: '\\u767b\\u5f55\\u81f3\\u53e6\\u4e00\\u4e2a\\u5e10\\u6237' };
|
||||||
|
const phoneEntry = { id: 'phone', textContent: '\\u4f7f\\u7528\\u7535\\u8bdd\\u53f7\\u7801\\u7ee7\\u7eed' };
|
||||||
|
|
||||||
|
function normalizeStep6Snapshot(snapshot) { return snapshot; }
|
||||||
|
function inspectLoginAuthState() {
|
||||||
|
return { state: 'entry_page', loginEntryTrigger: genericEntry, phoneEntryTrigger: phoneEntry };
|
||||||
|
}
|
||||||
|
function findLoginEntryTrigger() { return genericEntry; }
|
||||||
|
function findLoginPhoneEntryTrigger() { return phoneEntry; }
|
||||||
|
function isActionEnabled() { return true; }
|
||||||
|
function getActionText(el) { return el.textContent || ''; }
|
||||||
|
function log() {}
|
||||||
|
async function humanPause() {}
|
||||||
|
function simulateClick(el) { clicks.push(el.id); }
|
||||||
|
async function waitForLoginEntryOpenTransition() { return { state: 'phone_entry_page' }; }
|
||||||
|
async function switchFromEmailPageToPhoneLogin() { return { routed: 'switch-phone' }; }
|
||||||
|
async function step6LoginFromEmailPage() { return { routed: 'email' }; }
|
||||||
|
async function step6LoginFromPasswordPage() { return { routed: 'password' }; }
|
||||||
|
async function step6LoginFromPhonePage() { return { routed: 'phone' }; }
|
||||||
|
async function finalizeStep6VerificationReady() { return { routed: 'verification' }; }
|
||||||
|
function createStep6OAuthConsentSuccessResult() { return { routed: 'oauth' }; }
|
||||||
|
function createStep6AddEmailSuccessResult() { return { routed: 'add-email' }; }
|
||||||
|
async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'recoverable' } }; }
|
||||||
|
function createStep6RecoverableResult(reason, snapshot, options = {}) {
|
||||||
|
return { step6Outcome: 'recoverable', reason, state: snapshot?.state, message: options.message || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
${extractFunction('step6OpenLoginEntry')}
|
||||||
|
|
||||||
|
return { clicks, step6OpenLoginEntry };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const result = await api.step6OpenLoginEntry(
|
||||||
|
{ loginIdentifierType: 'phone', phoneNumber: '+441111111111' },
|
||||||
|
{ state: 'entry_page', loginEntryTrigger: { id: 'generic', textContent: '\u767b\u5f55\u81f3\u53e6\u4e00\u4e2a\u5e10\u6237' }, phoneEntryTrigger: { id: 'phone', textContent: '\u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(api.clicks, ['generic']);
|
||||||
|
assert.equal(result.routed, 'phone');
|
||||||
|
});
|
||||||
|
|
||||||
test('step 7 detects username text input when usernameKind is phone_number', () => {
|
test('step 7 detects username text input when usernameKind is phone_number', () => {
|
||||||
const api = createPhoneLoginEntryApi({
|
const api = createPhoneLoginEntryApi({
|
||||||
phoneUsernameKind: true,
|
phoneUsernameKind: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user