Files
go_bt/tests/ag_oauth_resilience.test.js
T

209 lines
7.1 KiB
JavaScript

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