294 lines
12 KiB
JavaScript
294 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const { _private } = require('../src/services/openai_oauth_service');
|
|
|
|
const {
|
|
OAUTH_REDIRECT_URI,
|
|
readOpenAIPageSnapshot,
|
|
advanceOpenAIPageProgressTracker,
|
|
shouldDeferOpenAIPageFailure,
|
|
runWithPageCallbackCapture,
|
|
} = _private;
|
|
|
|
const STABLE_WAIT_MS = 2000;
|
|
|
|
function progress(overrides = {}) {
|
|
return {
|
|
url: 'https://auth.openai.com/choose-an-account',
|
|
readyState: 'complete',
|
|
domSignature: 'text:20|html:120|elements:5|resources:3',
|
|
busyCount: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function advance(tracker, snapshot, nowMs, key = 'account-choice') {
|
|
return advanceOpenAIPageProgressTracker(tracker, key, snapshot, nowMs);
|
|
}
|
|
|
|
function shouldDefer(tracker, nowMs) {
|
|
return shouldDeferOpenAIPageFailure(tracker, nowMs, { stableMs: STABLE_WAIT_MS });
|
|
}
|
|
|
|
function createCallbackPage(initialUrl) {
|
|
const listeners = new Map();
|
|
const state = { url: initialUrl };
|
|
return {
|
|
state,
|
|
url: () => state.url,
|
|
mainFrame: () => ({ url: () => state.url }),
|
|
on: (name, listener) => listeners.set(name, listener),
|
|
off: (name, listener) => {
|
|
if (listeners.get(name) === listener) listeners.delete(name);
|
|
},
|
|
waitForTimeout: async () => {},
|
|
};
|
|
}
|
|
|
|
(async () => {
|
|
assert.strictEqual(typeof readOpenAIPageSnapshot, 'function', '应导出页面进度读取 helper');
|
|
assert.strictEqual(typeof advanceOpenAIPageProgressTracker, 'function', '应导出页面稳定追踪 helper');
|
|
assert.strictEqual(typeof shouldDeferOpenAIPageFailure, 'function', '应导出延迟失败判断 helper');
|
|
|
|
const readState = {
|
|
url: 'https://auth.openai.com/choose-an-account',
|
|
snapshot: progress({ readyState: 'loading', busyCount: 1 }),
|
|
};
|
|
const readPage = {
|
|
url: () => readState.url,
|
|
evaluate: async () => ({ ...readState.snapshot, url: undefined }),
|
|
};
|
|
const readSnapshot = await readOpenAIPageSnapshot(readPage);
|
|
assert.strictEqual(readSnapshot.url, readState.url, '读取结果应包含读取时的页面URL');
|
|
assert.strictEqual(readSnapshot.readyState, 'loading');
|
|
assert.strictEqual(readSnapshot.busyCount, 1);
|
|
|
|
// 只要文档仍明确处于 loading,就不能因为稳定窗口已过去而宣布结构失败。
|
|
let tracker = advance(null, progress({ readyState: 'loading' }), 0);
|
|
tracker = advance(tracker, progress({ readyState: 'loading' }), STABLE_WAIT_MS * 2);
|
|
assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS * 2), true, 'loading页面必须继续等待');
|
|
|
|
// DOM或资源进度变化会重置稳定计时;必须从最后一次变化重新累计。
|
|
tracker = advance(null, progress({ domSignature: 'dom-a' }), 0);
|
|
tracker = advance(tracker, progress({ domSignature: 'dom-a' }), 1500);
|
|
assert.strictEqual(shouldDefer(tracker, 1500), true);
|
|
tracker = advance(tracker, progress({ domSignature: 'dom-b' }), 1600);
|
|
assert.strictEqual(shouldDefer(tracker, 3500), true, 'DOM持续变化后不应沿用旧稳定时间');
|
|
tracker = advance(tracker, progress({ domSignature: 'dom-b' }), 3600);
|
|
assert.strictEqual(shouldDefer(tracker, 3600), false, '最后一次DOM变化稳定到阈值后才允许失败');
|
|
|
|
// 即使DOM签名相同,URL变化也代表一次新页面,稳定计时必须清零。
|
|
tracker = advance(null, progress({ url: 'https://auth.openai.com/oauth/authorize' }), 0);
|
|
tracker = advance(tracker, progress({ url: 'https://auth.openai.com/oauth/authorize' }), STABLE_WAIT_MS);
|
|
assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS), false);
|
|
tracker = advance(tracker, progress({ url: 'https://auth.openai.com/choose-an-account' }), STABLE_WAIT_MS + 1);
|
|
assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS * 2), true, 'URL变化后必须重新等待稳定窗口');
|
|
tracker = advance(tracker, progress({ url: 'https://auth.openai.com/choose-an-account' }), STABLE_WAIT_MS * 2 + 1);
|
|
assert.strictEqual(shouldDefer(tracker, STABLE_WAIT_MS * 2 + 1), false);
|
|
|
|
// 同一URL/DOM被用于不同页面判断时,状态key变化也必须开启新的观察窗口。
|
|
tracker = advance(null, progress({ domSignature: 'shared-dom' }), 5000, 'unknown-page');
|
|
tracker = advance(tracker, progress({ domSignature: 'shared-dom' }), 7000, 'unknown-page');
|
|
assert.strictEqual(shouldDefer(tracker, 7000), false);
|
|
tracker = advance(tracker, progress({ domSignature: 'shared-dom' }), 7001, 'account-choice');
|
|
assert.strictEqual(shouldDefer(tracker, 8000), true, '状态key变化后必须重置稳定计时');
|
|
tracker = advance(tracker, progress({ domSignature: 'shared-dom' }), 9001, 'account-choice');
|
|
assert.strictEqual(shouldDefer(tracker, 9001), false);
|
|
|
|
// 完成加载且URL/DOM持续不变时,阈值之前延迟,恰好达到阈值才允许终态失败。
|
|
tracker = advance(null, progress(), 10_000);
|
|
tracker = advance(tracker, progress(), 10_000 + STABLE_WAIT_MS - 1);
|
|
assert.strictEqual(shouldDefer(tracker, 10_000 + STABLE_WAIT_MS - 1), true);
|
|
tracker = advance(tracker, progress(), 10_000 + STABLE_WAIT_MS);
|
|
assert.strictEqual(shouldDefer(tracker, 10_000 + STABLE_WAIT_MS), false);
|
|
|
|
// 回调可能在helper开始监听前已经落到当前URL;必须主动读取,而不能只依赖事件。
|
|
const startPage = createCallbackPage(`${OAUTH_REDIRECT_URI}?code=start-code&state=start-state`);
|
|
const startCapture = await runWithPageCallbackCapture(startPage, 'start-state', async () => 'started');
|
|
assert.strictEqual(startCapture.callback && startCapture.callback.code, 'start-code');
|
|
|
|
// action结束时URL刚变成回调但没有触发mock事件,也必须通过结束主动读取捕获。
|
|
const endPage = createCallbackPage('https://auth.openai.com/oauth/authorize');
|
|
const endCapture = await runWithPageCallbackCapture(endPage, 'end-state', async () => {
|
|
endPage.state.url = `${OAUTH_REDIRECT_URI}?code=end-code&state=end-state`;
|
|
return 'ended';
|
|
});
|
|
assert.strictEqual(endCapture.callback && endCapture.callback.code, 'end-code');
|
|
|
|
// 账号卡片分批渲染:首帧只有其他邮箱,下一帧才出现目标邮箱。流程必须
|
|
// 继续观察,且绝不能提交非目标账号。
|
|
let accountUrl = 'https://auth.openai.com/choose-an-account';
|
|
let accountRound = 0;
|
|
let targetFormReads = 0;
|
|
let wrongAccountSubmitted = false;
|
|
const wrongAccount = {
|
|
isVisible: async () => true,
|
|
innerText: async () => 'Other account\nother@gmail.com',
|
|
evaluate: async () => { wrongAccountSubmitted = true; },
|
|
};
|
|
const targetAccount = {
|
|
isVisible: async () => true,
|
|
innerText: async () => 'Target account\ntarget@gmail.com',
|
|
evaluate: async () => {
|
|
targetFormReads++;
|
|
if (targetFormReads === 1) {
|
|
return {
|
|
action: 'https://auth.openai.com/choose-an-account',
|
|
method: 'POST',
|
|
fields: { session_id: 'target-session' },
|
|
};
|
|
}
|
|
accountUrl = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent';
|
|
return undefined;
|
|
},
|
|
};
|
|
const hiddenTarget = {
|
|
isVisible: async () => false,
|
|
click: async () => { throw new Error('hidden target must not be clicked'); },
|
|
};
|
|
const accountPage = {
|
|
url: () => accountUrl,
|
|
isClosed: () => false,
|
|
waitForTimeout: async () => {},
|
|
evaluate: async () => ({
|
|
readyState: accountRound < 2 ? 'loading' : 'complete',
|
|
domSignature: `account-round:${accountRound}`,
|
|
busyCount: accountRound < 2 ? 1 : 0,
|
|
}),
|
|
locator: (selector) => {
|
|
if (selector === 'body') {
|
|
return {
|
|
innerText: async () => (accountUrl.includes('/choose-an-account')
|
|
? (accountRound < 2 ? 'Other account other@gmail.com' : 'Target account target@gmail.com')
|
|
: 'OpenAI consent'),
|
|
};
|
|
}
|
|
if (selector === 'button[name="session_id"], button[data-dd-action-name="Select existing session"]') {
|
|
accountRound++;
|
|
const candidate = accountRound < 2 ? wrongAccount : targetAccount;
|
|
return { count: async () => 1, nth: () => candidate };
|
|
}
|
|
return { first: () => hiddenTarget };
|
|
},
|
|
};
|
|
const delayedAccount = await require('../src/services/openai_oauth_service')
|
|
.loginOpenAIOAuthWithGoogle(accountPage, { email: 'target@gmail.com' }, () => {}, {
|
|
pageMinimumWaitMs: 20,
|
|
pageStableMs: 5,
|
|
pageLoadingTimeoutMs: 100,
|
|
pagePollMs: 0,
|
|
});
|
|
assert.strictEqual(delayedAccount.ok, true, '目标账号稍后出现时应继续授权');
|
|
assert.strictEqual(wrongAccountSubmitted, false, '不得提交先出现的其他账号');
|
|
|
|
// OpenAI 账号选择偶发返回 Route Error 时应自动点击 Try again,最多 5 次,
|
|
// 不能像旧逻辑一样第一次遇到错误就直接结束任务。
|
|
let routeErrorRetryClicks = 0;
|
|
const routeErrorRetryButton = {
|
|
isVisible: async () => true,
|
|
click: async () => { routeErrorRetryClicks++; },
|
|
};
|
|
const routeErrorPage = {
|
|
url: () => 'https://auth.openai.com/choose-an-account',
|
|
isClosed: () => false,
|
|
waitForTimeout: async () => {},
|
|
locator: (selector) => {
|
|
if (selector === 'body') {
|
|
return {
|
|
innerText: async () => 'Oops, an error occurred! Route Error (400 Invalid content type: text/html; charset=UTF-8)',
|
|
};
|
|
}
|
|
if (selector === 'button:text-is("Try again")') {
|
|
return { first: () => routeErrorRetryButton };
|
|
}
|
|
return { first: () => hiddenTarget };
|
|
},
|
|
};
|
|
const routeErrorResult = await require('../src/services/openai_oauth_service')
|
|
.loginOpenAIOAuthWithGoogle(routeErrorPage, { email: 'target@gmail.com' }, () => {});
|
|
assert.strictEqual(routeErrorResult.ok, false);
|
|
assert.strictEqual(routeErrorResult.code, 'OPENAI_ACCOUNT_SELECT_REQUEST_FAILED');
|
|
assert.strictEqual(routeErrorRetryClicks, 5, 'Route Error 的 Try again 最多只能点击 5 次');
|
|
|
|
let recoveredUrl = 'https://auth.openai.com/choose-an-account';
|
|
let recoveredRetryClicks = 0;
|
|
const recoveredRetryButton = {
|
|
isVisible: async () => true,
|
|
click: async () => {
|
|
recoveredRetryClicks++;
|
|
recoveredUrl = `${OAUTH_REDIRECT_URI}?code=recovered-code&state=recovered-state`;
|
|
},
|
|
};
|
|
const recoveredRouteErrorPage = {
|
|
url: () => recoveredUrl,
|
|
isClosed: () => false,
|
|
waitForTimeout: async () => {},
|
|
locator: (selector) => {
|
|
if (selector === 'body') {
|
|
return { innerText: async () => 'Oops, an error occurred! Route Error (400 Invalid content type)' };
|
|
}
|
|
if (selector === 'button:text-is("Try again")') {
|
|
return { first: () => recoveredRetryButton };
|
|
}
|
|
return { first: () => hiddenTarget };
|
|
},
|
|
};
|
|
const recoveredRouteError = await require('../src/services/openai_oauth_service')
|
|
.loginOpenAIOAuthWithGoogle(recoveredRouteErrorPage, { email: 'target@gmail.com' }, () => {});
|
|
assert.strictEqual(recoveredRouteError.ok, true, '点击 Try again 后恢复跳转时应继续完成授权');
|
|
assert.strictEqual(recoveredRetryClicks, 1);
|
|
|
|
// 工作区正文先出现、Continue 后出现,也必须等待下一帧再点击。
|
|
let workspaceUrl = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent';
|
|
let workspaceRound = 0;
|
|
let workspaceClicked = false;
|
|
const workspaceButton = {
|
|
isVisible: async () => true,
|
|
innerText: async () => 'Continue',
|
|
getAttribute: async () => null,
|
|
isDisabled: async () => false,
|
|
click: async () => {
|
|
workspaceClicked = true;
|
|
workspaceUrl = `${OAUTH_REDIRECT_URI}?code=workspace-code&state=workspace-state`;
|
|
},
|
|
};
|
|
const workspacePage = {
|
|
url: () => workspaceUrl,
|
|
isClosed: () => false,
|
|
waitForTimeout: async () => {},
|
|
evaluate: async () => ({
|
|
readyState: workspaceRound < 2 ? 'loading' : 'complete',
|
|
domSignature: `workspace-round:${workspaceRound}`,
|
|
busyCount: workspaceRound < 2 ? 1 : 0,
|
|
}),
|
|
locator: (selector) => {
|
|
if (selector === 'body') {
|
|
return { innerText: async () => 'Select a workspace Personal account Continue' };
|
|
}
|
|
if (selector === 'button, [role="button"], input[type="submit"]') {
|
|
workspaceRound++;
|
|
return {
|
|
count: async () => (workspaceRound < 2 ? 0 : 1),
|
|
nth: () => workspaceButton,
|
|
};
|
|
}
|
|
return { first: () => hiddenTarget };
|
|
},
|
|
};
|
|
const delayedWorkspace = await require('../src/services/openai_oauth_service')
|
|
.loginOpenAIOAuthWithGoogle(workspacePage, { email: 'target@gmail.com' }, () => {}, {
|
|
pageMinimumWaitMs: 20,
|
|
pageStableMs: 5,
|
|
pageLoadingTimeoutMs: 100,
|
|
pagePollMs: 0,
|
|
});
|
|
assert.strictEqual(delayedWorkspace.ok, true, 'Continue稍后出现时应继续授权');
|
|
assert.strictEqual(workspaceClicked, true);
|
|
|
|
console.log('openai_oauth_loading.test.js: PASS');
|
|
})().catch(error => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|