feat: add phone verification flow support
This commit is contained in:
@@ -215,6 +215,20 @@ test('auto-run stops restarting on generic phone-page failure messages even with
|
||||
assert.ok(!result.events.logs.some(({ message }) => /回到步骤 7 重新开始授权流程/.test(message)));
|
||||
});
|
||||
|
||||
test('auto-run restarts from step 7 when phone verification cannot receive SMS after resend', async () => {
|
||||
const harness = createHarness({
|
||||
failureStep: 9,
|
||||
failureBudget: 1,
|
||||
failureMessage: 'Phone verification could not receive an SMS after resend. Restart step 7 with a new number. Current number: 66959916439.',
|
||||
authState: { state: 'add_phone_page', url: 'https://auth.openai.com/add-phone' },
|
||||
});
|
||||
|
||||
const events = await harness.run();
|
||||
|
||||
assert.equal(events.invalidations.length, 1);
|
||||
assert.deepStrictEqual(events.steps, [7, 8, 9, 7, 8, 9, 10]);
|
||||
});
|
||||
|
||||
test('auto-run stop errors after step 7 are rethrown immediately instead of restarting', async () => {
|
||||
const harness = createHarness({
|
||||
failureStep: 9,
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
|
||||
|
||||
test('phone verification helper requests HeroSMS numbers with fixed OpenAI and Thailand parameters', async () => {
|
||||
const requests = [];
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
requests.push(new URL(url));
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||
};
|
||||
},
|
||||
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
|
||||
|
||||
assert.deepStrictEqual(activation, {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
});
|
||||
assert.equal(requests.length, 1);
|
||||
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
|
||||
assert.equal(requests[0].searchParams.get('service'), 'dr');
|
||||
assert.equal(requests[0].searchParams.get('country'), '52');
|
||||
assert.equal(requests[0].searchParams.get('api_key'), 'demo-key');
|
||||
});
|
||||
|
||||
test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => {
|
||||
const requests = [];
|
||||
const stateUpdates = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
verificationResendCount: 1,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
|
||||
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 === 'getNumber') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||
};
|
||||
}
|
||||
if (action === 'getStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'STATUS_OK:654321',
|
||||
};
|
||||
}
|
||||
if (action === 'setStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_ACTIVATION',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
url: 'https://auth.openai.com/phone-verification',
|
||||
};
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
stateUpdates.push(updates);
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.deepStrictEqual(stateUpdates, [
|
||||
{
|
||||
currentPhoneActivation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
reusablePhoneActivation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
currentPhoneActivation: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const actions = requests.map((url) => url.searchParams.get('action'));
|
||||
assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
|
||||
});
|
||||
|
||||
test('phone verification helper uses the configured HeroSMS country for both number acquisition and add-phone submission', async () => {
|
||||
const requests = [];
|
||||
const submittedPayloads = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
heroSmsCountryId: 16,
|
||||
heroSmsCountryLabel: 'United Kingdom',
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
|
||||
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 === 'getNumber') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_NUMBER:654321:447911123456',
|
||||
};
|
||||
}
|
||||
if (action === 'getStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'STATUS_OK:112233',
|
||||
};
|
||||
}
|
||||
if (action === 'setStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_ACTIVATION',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
submittedPayloads.push(message.payload);
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
url: 'https://auth.openai.com/phone-verification',
|
||||
};
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
|
||||
assert.equal(requests[0].searchParams.get('country'), '16');
|
||||
assert.deepStrictEqual(submittedPayloads, [{
|
||||
phoneNumber: '447911123456',
|
||||
countryId: 16,
|
||||
countryLabel: 'United Kingdom',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('phone verification helper throws a step-7 restart error after 60 seconds plus one resend window without SMS', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
const statusCallsById = {};
|
||||
const realDateNow = Date.now;
|
||||
let fakeNow = 0;
|
||||
Date.now = () => fakeNow;
|
||||
|
||||
try {
|
||||
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');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
|
||||
if (action === 'getNumber') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_NUMBER:123456:66959916439',
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'getStatus') {
|
||||
statusCallsById[id] = (statusCallsById[id] || 0) + 1;
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'STATUS_WAIT_CODE',
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'setStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_ACTIVATION',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push(message.type);
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
url: 'https://auth.openai.com/phone-verification',
|
||||
};
|
||||
}
|
||||
if (message.type === 'RESEND_PHONE_VERIFICATION_CODE') {
|
||||
return {
|
||||
resent: true,
|
||||
url: 'https://auth.openai.com/phone-verification',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {
|
||||
fakeNow += 61000;
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/Restart step 7 with a new number/i
|
||||
);
|
||||
assert.ok(statusCallsById['123456'] >= 2, 'first number should be polled twice before being replaced');
|
||||
assert.deepStrictEqual(messages, [
|
||||
'SUBMIT_PHONE_NUMBER',
|
||||
'RESEND_PHONE_VERIFICATION_CODE',
|
||||
]);
|
||||
|
||||
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
|
||||
assert.deepStrictEqual(actions, [
|
||||
'getNumber:',
|
||||
'getStatus:123456',
|
||||
'setStatus:123456',
|
||||
'getStatus:123456',
|
||||
'setStatus:123456',
|
||||
]);
|
||||
assert.equal(currentState.currentPhoneActivation, null);
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
});
|
||||
|
||||
test('phone verification helper reuses the same number up to three successful registrations', async () => {
|
||||
const requests = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: {
|
||||
activationId: '123456',
|
||||
phoneNumber: '66959916439',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 2,
|
||||
maxUses: 3,
|
||||
},
|
||||
};
|
||||
|
||||
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 === 'reactivate') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => JSON.stringify({
|
||||
activationId: '222333',
|
||||
phoneNumber: '66959916439',
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (action === 'getStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'STATUS_OK:654321',
|
||||
};
|
||||
}
|
||||
if (action === 'setStatus') {
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => 'ACCESS_ACTIVATION',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
url: 'https://auth.openai.com/phone-verification',
|
||||
};
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.equal(requests[0].searchParams.get('action'), 'reactivate');
|
||||
assert.equal(requests[0].searchParams.get('id'), '123456');
|
||||
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
||||
});
|
||||
@@ -53,6 +53,8 @@ function extractFunction(name) {
|
||||
const bundle = [
|
||||
extractFunction('getPageTextSnapshot'),
|
||||
extractFunction('getLoginVerificationDisplayedEmail'),
|
||||
extractFunction('getPhoneVerificationDisplayedPhone'),
|
||||
extractFunction('isPhoneVerificationPageReady'),
|
||||
extractFunction('inspectLoginAuthState'),
|
||||
extractFunction('normalizeStep6Snapshot'),
|
||||
].join('\n');
|
||||
@@ -69,6 +71,9 @@ const document = {
|
||||
innerText: ${JSON.stringify(overrides.pageText || '')},
|
||||
textContent: ${JSON.stringify(overrides.pageText || '')},
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
function getLoginTimeoutErrorPageState() {
|
||||
@@ -103,6 +108,10 @@ function isAddPhonePageReady() {
|
||||
return ${JSON.stringify(Boolean(overrides.addPhonePage))};
|
||||
}
|
||||
|
||||
function isVisibleElement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isStep8Ready() {
|
||||
return ${JSON.stringify(Boolean(overrides.consentReady))};
|
||||
}
|
||||
@@ -115,6 +124,7 @@ ${bundle}
|
||||
|
||||
return {
|
||||
inspectLoginAuthState,
|
||||
isPhoneVerificationPageReady,
|
||||
normalizeStep6Snapshot,
|
||||
};
|
||||
`)();
|
||||
@@ -146,6 +156,38 @@ return {
|
||||
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
pathname: '/email-verification',
|
||||
href: 'https://auth.openai.com/email-verification',
|
||||
verificationTarget: { id: 'otp' },
|
||||
pageText: 'We just sent to display.user@example.com. Enter it below.',
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
api.isPhoneVerificationPageReady(),
|
||||
false,
|
||||
'邮箱验证码页不应被误判为手机验证码页'
|
||||
);
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(snapshot.state, 'verification_page');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
pathname: '/phone-verification',
|
||||
href: 'https://auth.openai.com/phone-verification',
|
||||
verificationTarget: { id: 'otp' },
|
||||
pageText: 'Check your phone. We just sent a code to +66 81 234 5678.',
|
||||
});
|
||||
|
||||
assert.strictEqual(api.isPhoneVerificationPageReady(), true);
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(snapshot.state, 'phone_verification_page');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
oauthConsentPage: true,
|
||||
|
||||
@@ -238,7 +238,7 @@ test('verification flow clears 2925 mailbox before polling and after successful
|
||||
assert.deepStrictEqual(mailMessages, ['DELETE_ALL_EMAILS', 'POLL_EMAIL', 'DELETE_ALL_EMAILS']);
|
||||
});
|
||||
|
||||
test('verification flow treats add-phone after login code submit as fatal instead of completing step 8', async () => {
|
||||
test('verification flow completes step 8 and flags phone verification when add-phone appears after login code submit', async () => {
|
||||
const events = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
@@ -288,18 +288,21 @@ test('verification flow treats add-phone after login code submit as fatal instea
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => helpers.resolveVerificationStep(
|
||||
8,
|
||||
{ email: 'user@example.com', lastLoginCode: null },
|
||||
{ provider: 'qq', label: 'QQ 邮箱' },
|
||||
{}
|
||||
),
|
||||
/验证码提交后页面进入手机号页面/
|
||||
const result = await helpers.resolveVerificationStep(
|
||||
8,
|
||||
{ email: 'user@example.com', lastLoginCode: null },
|
||||
{ provider: 'qq', label: 'QQ Mail' },
|
||||
{}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
phoneVerificationRequired: true,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
assert.deepStrictEqual(events, [
|
||||
['submit', '654321'],
|
||||
['state', '654321'],
|
||||
['complete', '654321'],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user