209 lines
7.1 KiB
JavaScript
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;
|
|
});
|