'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; });