const assert = require('assert'); const { exchangeCode, _private } = require('../src/services/ag_verify_service'); function emptyLocator() { return { count: async () => 0, nth: () => { throw new Error('empty locator'); }, }; } (async () => { { let currentUrl = 'https://accounts.google.com/signin/challenge/totp'; const input = { isVisible: async () => true, fill: async () => { currentUrl = 'https://accounts.google.com/o/oauth2/approval/v2/approvalnativeapp'; throw new Error('page.evaluate: Execution context was destroyed, most likely because of a navigation'); }, }; const page = { url: () => currentUrl, locator: selector => selector === 'input[id="totpPin"]' ? { count: async () => 1, nth: () => input } : emptyLocator(), }; const result = await _private.fillOAuthTotpIfCurrent(page, '123456'); assert.strictEqual(result.ok, false); assert.strictEqual(result.state, 'page_changed'); } { let locatorCalls = 0; const page = { url: () => 'https://accounts.google.com/o/oauth2/approval/v2/approvalnativeapp', locator: () => { locatorCalls++; return emptyLocator(); }, }; const result = await _private.fillOAuthTotpIfCurrent(page, '123456'); assert.strictEqual(result.state, 'left_totp'); assert.strictEqual(locatorCalls, 0, '离开TOTP后不得继续访问旧输入框'); } assert.strictEqual( _private.isPageNavigationRaceError(new Error('Execution context was destroyed, most likely because of a navigation')), true, ); { let currentUrl = 'https://accounts.google.com/loading'; let marker = 'Loading'; let pollCount = 0; const page = { url: () => currentUrl, evaluate: async () => ({ readyState: 'complete', marker: `${marker.length}:${marker}:` }), waitForLoadState: async () => {}, waitForTimeout: async () => { pollCount++; currentUrl = 'https://accounts.google.com/oauth/consent'; marker = 'Allow access'; }, locator: () => emptyLocator(), }; const beforeState = await _private.captureOAuthPageState(page); const progressed = await _private.waitForOAuthPageProgress(page, beforeState, { timeoutMs: 2000, pollMs: 100, }); assert.strictEqual(progressed, true, 'URL或页面内容变化后应立即结束等待'); assert.strictEqual(pollCount, 1, '页面进展监控不得继续执行固定长等待'); } { let captchaVisible = true; let challengeNotices = 0; let clearedNotices = 0; let polls = 0; const captchaBody = { innerText: async () => captchaVisible ? 'Verify that you are not a robot' : 'Continue to OAuth', }; const page = { url: () => captchaVisible ? 'https://accounts.google.com/signin/challenge/recaptcha' : 'https://accounts.google.com/o/oauth2/approval', isClosed: () => false, locator: selector => selector === 'body' ? captchaBody : emptyLocator(), waitForTimeout: async () => { polls++; captchaVisible = false; }, }; const result = await _private.waitForManualCaptchaClear(page, { timeoutMs: 5000, pollMs: 50, onChallenge: () => { challengeNotices++; }, onCleared: () => { clearedNotices++; }, }); assert.strictEqual(result.cleared, true, '手动验证完成后应自动继续'); assert.strictEqual(polls, 1); assert.strictEqual(challengeNotices, 1); assert.strictEqual(clearedNotices, 1); } { const closedPage = { isClosed: () => true, locator: () => emptyLocator(), }; const result = await _private.waitForManualCaptchaClear(closedPage, { timeoutMs: 10, pollMs: 50 }); assert.strictEqual(result.cleared, false); assert.strictEqual(result.pageClosed, true, '人工验证期间关闭浏览器应立即停止等待'); } { let clicked = ''; const choices = [ { isVisible: async () => true, getAttribute: async () => 'other@example.com', innerText: async () => 'Other\nother@example.com', boundingBox: async () => null, click: async () => { clicked = 'other@example.com'; }, }, { isVisible: async () => true, getAttribute: async () => 'User.Name@gmail.com', innerText: async () => 'User Name\nUser.Name@gmail.com', boundingBox: async () => null, click: async () => { clicked = 'User.Name@gmail.com'; }, }, ]; const page = { locator: selector => selector === '[data-identifier]' ? { count: async () => choices.length, nth: index => choices[index] } : emptyLocator(), }; assert.strictEqual( await _private.chooseExpectedOAuthAccount(page, 'username@gmail.com'), true, '账号选择页应按 Gmail 忽略点号规则匹配当前任务账号', ); assert.strictEqual(clicked, 'User.Name@gmail.com', '不得误点列表中的第一个其它账号'); } { const page = { url: () => 'https://access.workspace.google.com/ServiceNotAllowed?application=923893674115', locator: () => ({ innerText: async () => '' }), }; assert.strictEqual(await _private.isGoogleWorkspaceServiceNotAllowedPage(page), true); } { const page = { url: () => 'https://access.workspace.google.com/', locator: () => ({ innerText: async () => 'We are sorry, but you do not have access to Google Cloud Platform.', }), }; assert.strictEqual(await _private.isGoogleWorkspaceServiceNotAllowedPage(page), true); } { const page = { // 即使与无权限页共用 ServiceNotAllowed 地址,也必须优先按正文判为未满18岁。 url: () => 'https://access.workspace.google.com/ServiceNotAllowed?application=923893674115', locator: () => ({ innerText: async () => "Can't access this service\nThis service is unavailable for Google Workspace for Education users marked as under the age of 18 by their administrator.", }), }; const denial = await _private.detectGoogleWorkspaceAccessDenial(page); assert.strictEqual(denial.reasonCode, 'GOOGLE_UNDER_18'); assert.strictEqual(await _private.isGoogleWorkspaceServiceNotAllowedPage(page), false); } { let evaluateCalls = 0; const page = { evaluate: async () => { evaluateCalls++; if (evaluateCalls < 3) { throw new Error('page.evaluate: Execution context was destroyed, most likely because of a navigation'); } return { statusCode: 200, body: JSON.stringify({ access_token: 'test-access-token', refresh_token: 'test-refresh-token' }), error: null, }; }, waitForLoadState: async () => {}, waitForTimeout: async () => {}, }; const token = await exchangeCode('dummy-authorization-code', page); assert.strictEqual(token.access_token, 'test-access-token'); assert.strictEqual(token.refresh_token, 'test-refresh-token'); assert.strictEqual(evaluateCalls, 3, '页面上下文销毁后应在浏览器内重试'); } console.log('ag_oauth_resilience.test.js: PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });