Files
go_bt/tests/security_code_challenge.test.js
T

355 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const assert = require('assert');
const { _private: loginPrivate } = require('../src/services/login_service');
const { _private: chatgptPrivate } = require('../src/services/chatgpt_service');
const {
handleIdentityVerification,
handleChallengeSelection,
inputCurrent2fa,
_private: commonAuthPrivate,
} = require('../src/services/common_auth');
function emptyLocator() {
return {
count: async () => 0,
nth: () => { throw new Error('empty locator'); },
};
}
(async () => {
const securityCodeText = [
'Get a code to sign in',
'To get your security code, go to g.co/sc in a new browser window',
'Enter security code',
'Try another way',
].join('\n');
assert.strictEqual(loginPrivate.hasSecurityCodePageText(securityCodeText), true);
assert.strictEqual(commonAuthPrivate.hasSecurityCodePageText(securityCodeText), true);
assert.strictEqual(
loginPrivate.hasSecurityCodePageText('Get a verification code from the Google Authenticator app'),
false,
'Authenticator 页面不能被识别为 g.co 安全码页面',
);
const phoneTabletOnlyText = [
"Verify it's you",
'Choose a way to verify',
"Use your phone or tablet to get a security code (even if it's offline)",
].join('\n');
{
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/selection',
locator: selector => selector === 'body'
? { innerText: async () => phoneTabletOnlyText }
: emptyLocator(),
};
assert.strictEqual(
await commonAuthPrivate.detectPhoneTabletOnlyChallenge(page),
true,
'只有手机/平板安全码时应立即识别',
);
assert.strictEqual(await loginPrivate.detectPhoneTabletOnlyChallenge(page), true);
}
{
let optionsSettled = false;
const makeMissing = () => ({
count: async () => 0,
nth() { return this; },
first() { return this; },
or() { return this; },
waitFor: async () => { throw new Error('not visible'); },
isVisible: async () => false,
textContent: async () => '',
});
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/selection',
locator: selector => {
if (selector === 'body') {
return {
...makeMissing(),
innerText: async () => optionsSettled
? phoneTabletOnlyText
: "Verify it's you\nChoose a way to verify",
};
}
return makeMissing();
},
waitForLoadState: async () => {},
waitForTimeout: async () => { optionsSettled = true; },
};
const result = await handleIdentityVerification(page, {}, () => {});
assert.deepStrictEqual(
result,
[false, '需手机/平板验证', 'PHONE_TABLET_VERIFY'],
'selection 首次正文未稳定时,等待后必须复检唯一手机/平板方式',
);
}
{
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/selection',
locator: selector => selector === 'body'
? { innerText: async () => `${phoneTabletOnlyText}\nGet a verification code from the Google Authenticator app` }
: emptyLocator(),
};
assert.strictEqual(
await commonAuthPrivate.detectPhoneTabletOnlyChallenge(page),
false,
'仍有Authenticator选项时不得误判为只能手机/平板验证',
);
}
{
const heading = {
first() { return this; },
textContent: async () => "Verify it's you",
};
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/ootp',
locator: selector => {
if (selector === 'body') return { innerText: async () => phoneTabletOnlyText };
if (selector.startsWith('h1#headingText')) return heading;
return emptyLocator();
},
};
assert.strictEqual(await commonAuthPrivate.detectVerifyItsYouOotp(page), true);
assert.strictEqual(
await commonAuthPrivate.detectPhoneTabletOnlyChallenge(page),
true,
'OOTP 页面只剩手机/平板安全码时也应直接识别',
);
}
{
const makeMissing = () => ({
count: async () => 0,
nth() { return this; },
first() { return this; },
or() { return this; },
waitFor: async () => { throw new Error('not visible'); },
isVisible: async () => false,
textContent: async () => '',
});
const heading = {
...makeMissing(),
textContent: async () => "Verify it's you",
};
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/ootp',
locator: selector => {
if (selector === 'body') return { ...makeMissing(), innerText: async () => phoneTabletOnlyText };
if (selector.startsWith('h1#headingText')) return heading;
return makeMissing();
},
waitForLoadState: async () => {},
waitForTimeout: async () => {},
};
const result = await handleIdentityVerification(page, {}, () => {});
assert.deepStrictEqual(
result,
[false, '需手机/平板验证', 'PHONE_TABLET_VERIFY'],
'Verify it’s you + 唯一手机/平板方式必须返回明确机器分类',
);
const oauthResult = await chatgptPrivate.handleGoogleOAuth(page, {}, () => {});
assert.strictEqual(oauthResult.code, 'PHONE_TABLET_VERIFY', 'ChatGPT OAuth 必须保留手机/平板分类');
}
{
const makeMissing = () => ({
count: async () => 0,
nth() { return this; },
first() { return this; },
or() { return this; },
waitFor: async () => { throw new Error('not visible'); },
isVisible: async () => false,
textContent: async () => '',
});
const heading = {
...makeMissing(),
textContent: async () => "Verify it's you",
};
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/ootp',
locator: selector => {
if (selector === 'body') return { ...makeMissing(), innerText: async () => "Verify it's you" };
if (selector.startsWith('h1#headingText')) return heading;
return makeMissing();
},
waitForLoadState: async () => {},
waitForTimeout: async () => {},
};
const result = await handleIdentityVerification(page, {}, () => {});
assert.strictEqual(result[0], false);
assert.strictEqual(result[2], 'VERIFY_ITS_YOU');
assert.match(result[1], /Verify it.s you/);
}
{
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/custom',
locator: selector => selector === 'body'
? { innerText: async () => securityCodeText }
: emptyLocator(),
};
assert.strictEqual(await loginPrivate.isSecurityCodeChallenge(page), true);
assert.strictEqual(await commonAuthPrivate.isSecurityCodeChallenge(page), true);
}
{
const page = {
url: () => 'https://accounts.google.com/signin/challenge/selection',
locator: selector => selector === 'body'
? { innerText: async () => `Choose how you want to sign in\n${securityCodeText}\nGoogle Authenticator` }
: emptyLocator(),
};
assert.strictEqual(
await loginPrivate.isSecurityCodeChallenge(page),
false,
'验证方式列表即使展示 g.co 选项,也不能被当作当前安全码页面',
);
assert.strictEqual(await commonAuthPrivate.isSecurityCodeChallenge(page), false);
}
{
let bodyText = securityCodeText;
const tryAnotherButton = {
isVisible: async () => true,
scrollIntoViewIfNeeded: async () => {},
click: async () => {
bodyText = 'Choose how you want to sign in\nGoogle Authenticator';
},
evaluate: async () => true,
};
const page = {
url: () => 'https://accounts.google.com/v3/signin/challenge/custom',
locator: selector => {
if (selector === 'body') return { innerText: async () => bodyText };
if (selector === 'text="Try another way"') {
return { count: async () => 1, nth: () => tryAnotherButton };
}
return emptyLocator();
},
waitForTimeout: async () => {},
};
const result = await loginPrivate.tryLeaveAlternateChallenge(page, () => {}, 50);
assert.strictEqual(result.clicked, true);
assert.strictEqual(result.moved, true, '同一 URL 内切换到验证方式列表也应视为成功离开');
}
{
const attemptedSelectors = [];
let currentUrl = 'https://accounts.google.com/signin/challenge/selection';
const authButton = {
isVisible: async () => true,
scrollIntoViewIfNeeded: async () => {},
click: async () => {
currentUrl = 'https://accounts.google.com/signin/challenge/totp';
},
evaluate: async () => true,
};
const page = {
url: () => currentUrl,
locator: selector => {
attemptedSelectors.push(selector);
if (selector === 'li:has-text("Get a verification code from the Google Authenticator app")') {
return { count: async () => 1, nth: () => authButton };
}
return emptyLocator();
},
waitForTimeout: async () => {},
waitForLoadState: async () => {},
};
assert.strictEqual(await handleChallengeSelection(page, () => {}), true);
assert.strictEqual(
attemptedSelectors.includes('div[data-challengetype="6"]'),
false,
'不稳定的 challenge type 不能再作为 Authenticator 兜底',
);
}
{
const invalidResult = await inputCurrent2fa(
{ url: () => 'https://accounts.google.com/signin/challenge/totp' },
'1//not-a-totp-secret',
() => {},
);
assert.strictEqual(invalidResult.ok, false);
assert.match(invalidResult.msg, /2FA密钥格式不正确|Base32/);
}
{
let filledCode = '';
const input = {
first() { return this; },
waitFor: async () => {},
fill: async value => { if (value) filledCode = value; },
press: async () => {},
};
const missingButton = {
first() { return this; },
waitFor: async () => { throw new Error('button unavailable'); },
};
const page = {
url: () => 'https://accounts.google.com/signin/challenge/totp',
locator: selector => selector.includes('input[id="totpPin"]') ? input : missingButton,
waitForLoadState: async () => {},
};
const result = await inputCurrent2fa(page, 'JBSWY3DPEHPK3PXP', () => {});
assert.strictEqual(result.ok, true, '页面只有totpPin输入框时也应能提交2FA');
assert.match(filledCode, /^\d{6}$/);
}
{
const alert = {
isVisible: async () => true,
innerText: async () => 'Wrong code. Try again.',
};
const page = {
locator: selector => {
if (selector.startsWith('[role="alert"]')) {
return { count: async () => 1, nth: () => alert };
}
if (selector === 'body') return { innerText: async () => 'Wrong code. Try again.' };
return emptyLocator();
},
};
assert.strictEqual(await commonAuthPrivate.detectTotpRejection(page), 'Wrong code. Try again.');
assert.strictEqual(await loginPrivate.detectTotpRejection(page), 'Wrong code. Try again.');
}
{
let secondClicked = false;
const failingButton = {
isVisible: async () => true,
scrollIntoViewIfNeeded: async () => {},
click: async () => { throw new Error('intercepted'); },
evaluate: async () => { throw new Error('intercepted'); },
};
const workingButton = {
isVisible: async () => true,
scrollIntoViewIfNeeded: async () => {},
click: async () => { secondClicked = true; },
evaluate: async () => true,
};
const page = {
locator: selector => {
if (selector === 'first') return { count: async () => 1, nth: () => failingButton };
if (selector === 'second') return { count: async () => 1, nth: () => workingButton };
return emptyLocator();
},
};
const selector = await commonAuthPrivate.clickFirstVisible(page, ['first', 'second'], 20);
assert.strictEqual(selector, 'second');
assert.strictEqual(secondClicked, true, '点击失败后必须继续尝试下一个可点击元素');
}
console.log('security_code_challenge.test.js: PASS');
})().catch(error => {
console.error(error);
process.exitCode = 1;
});