1071 lines
40 KiB
JavaScript
1071 lines
40 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { loginOrRegisterChatGPT, _private } = require('../src/services/chatgpt_service');
|
|
const { RECIPES, ATOM_STEPS } = require('../src/pipeline/recipes');
|
|
const { getFailureLabel } = require('../src/utils/error_classifier');
|
|
|
|
assert.strictEqual(_private.normalizeAge(undefined), 23, '未配置年龄时应使用固定值 23');
|
|
assert.strictEqual(_private.normalizeAge(''), 23, '空年龄应使用固定值 23');
|
|
assert.strictEqual(_private.normalizeAge(17), 18, '年龄不能小于 18');
|
|
assert.strictEqual(_private.normalizeAge(120), 99, '年龄不能大于 99');
|
|
assert.strictEqual(_private.normalizeAge('35'), 35, '合法年龄应保持不变');
|
|
|
|
assert.strictEqual(_private.isAuthenticatedSession(null), false);
|
|
assert.strictEqual(_private.isAuthenticatedSession({}), false);
|
|
assert.strictEqual(_private.isAuthenticatedSession({ user: { email: 'target@example.com' } }), true);
|
|
assert.strictEqual(_private.isAuthenticatedSession({ accessToken: 'local-token' }), false, '只有Token、没有用户邮箱不能证明账号已登录');
|
|
assert.strictEqual(_private.isAuthenticatedSession({ expires: '2099-01-01T00:00:00.000Z' }), false, '只有过期时间不能证明已经登录');
|
|
assert.strictEqual(
|
|
_private.isAuthenticatedSession({ user: { email: 'target@example.com' }, expires: '2000-01-01T00:00:00.000Z' }),
|
|
false,
|
|
'已过期 Session 不能判定登录成功',
|
|
);
|
|
assert.strictEqual(
|
|
_private.sessionAccountMismatch({ user: { email: 'other@example.com' } }, 'target@example.com'),
|
|
true,
|
|
'Session 用户邮箱不一致时不能标记当前任务成功',
|
|
);
|
|
assert.strictEqual(
|
|
_private.sessionAccountMismatch({ user: { email: 'TARGET@example.com' } }, 'target@example.com'),
|
|
false,
|
|
);
|
|
assert.strictEqual(_private.normalizeIdentityEmail('0.966527426kk@gmail.com'), '0966527426kk@gmail.com');
|
|
assert.strictEqual(_private.normalizeIdentityEmail('0.966527426kk+test@googlemail.com'), '0966527426kk@gmail.com');
|
|
assert.strictEqual(_private.normalizeIdentityEmail('first.last@example.com'), 'first.last@example.com');
|
|
assert.strictEqual(_private.buildChatGPTFullName('samsung samsung1'), 'Samsung User');
|
|
assert.strictEqual(_private.buildChatGPTFullName('', 'john doe2'), 'John User');
|
|
assert.strictEqual(_private.buildChatGPTFullName('', '', '123456'), 'Alex Morgan');
|
|
assert.strictEqual(
|
|
_private.sessionAccountMismatch(
|
|
{ user: { email: '0966527426kk@gmail.com' } },
|
|
'0.966527426kk@gmail.com',
|
|
),
|
|
false,
|
|
'Gmail Session 返回无点号地址时仍应识别为同一账号',
|
|
);
|
|
|
|
const matchingConsent = 'Sign in to OpenAI target@example.com Email address';
|
|
const mismatchingConsent = 'Sign in to OpenAI another@example.com Email address';
|
|
assert.strictEqual(_private.consentAccountMismatch(matchingConsent, 'target@example.com'), false);
|
|
assert.strictEqual(_private.consentAccountMismatch(mismatchingConsent, 'target@example.com'), true);
|
|
assert.strictEqual(_private.consentAccountMismatch('Sign in to OpenAI', 'target@example.com'), false);
|
|
assert.strictEqual(
|
|
_private.consentAccountMismatch(
|
|
'Sign in to OpenAI 0966527426kk@gmail.com Email address',
|
|
'0.966527426kk@gmail.com',
|
|
),
|
|
false,
|
|
'Google 授权页中的 Gmail 无点号形式不应被误判为其它账号',
|
|
);
|
|
|
|
assert.strictEqual(_private.isOpenAIPage('https://chatgpt.com/'), true);
|
|
assert.strictEqual(_private.isOpenAIPage('https://auth.openai.com/create-account'), true);
|
|
assert.strictEqual(_private.isOpenAIPage('https://accounts.google.com/signin'), false);
|
|
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/'), true);
|
|
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/?model=auto#top'), true);
|
|
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/auth/callback?code=redacted'), false);
|
|
assert.strictEqual(_private.isChatGPTHomeUrl('https://chatgpt.com/auth/error'), false);
|
|
assert.strictEqual(_private.isChatGPTAuthRoute('https://chatgpt.com/auth/callback?code=redacted'), true);
|
|
assert.strictEqual(_private.isChatGPTAuthRoute('https://chatgpt.com/'), false);
|
|
assert.strictEqual(_private.isChatGPTAuthErrorRoute('https://chatgpt.com/auth/error?error=redacted'), true);
|
|
assert.strictEqual(_private.extendDeadlineForPhase(1120, 1000, 1250), 1370);
|
|
|
|
function makeGoogleIdentifierPage({ transitionOnClick }) {
|
|
const state = {
|
|
url: 'https://accounts.google.com/v3/signin/identifier',
|
|
value: '',
|
|
inputVisible: true,
|
|
};
|
|
|
|
const input = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.inputVisible,
|
|
inputValue: async () => state.value,
|
|
fill: async value => { state.value = String(value); },
|
|
pressSequentially: async text => { state.value += String(text); },
|
|
press: async () => {},
|
|
click: async () => {},
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
evaluate: async (_, value) => { state.value = String(value); },
|
|
};
|
|
const next = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => true,
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
click: async () => {
|
|
if (transitionOnClick) {
|
|
state.inputVisible = false;
|
|
state.url = 'https://accounts.google.com/signin/challenge/pwd';
|
|
}
|
|
},
|
|
};
|
|
const missing = {
|
|
count: async () => 0,
|
|
nth() { return this; },
|
|
};
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
locator: selector => {
|
|
if (selector === '#identifierId') return input;
|
|
if (selector === '#identifierNext button') return next;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
function makeAgePage({ transitionOnClick, transitionOnAttempt, submitLabel = 'Finish creating account' }) {
|
|
const successfulAttempt = transitionOnAttempt || (transitionOnClick ? 1 : Number.POSITIVE_INFINITY);
|
|
const state = {
|
|
url: 'https://auth.openai.com/create-account',
|
|
age: '',
|
|
inputVisible: true,
|
|
clicks: 0,
|
|
};
|
|
const ageInput = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.inputVisible,
|
|
inputValue: async () => state.age,
|
|
fill: async value => { state.age = String(value); },
|
|
click: async () => {},
|
|
press: async () => {},
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
evaluate: async (_, value) => { state.age = String(value); },
|
|
};
|
|
const finishButton = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => true,
|
|
click: async () => {
|
|
state.clicks++;
|
|
if (state.clicks >= successfulAttempt) {
|
|
state.inputVisible = false;
|
|
state.url = 'https://chatgpt.com/';
|
|
}
|
|
},
|
|
};
|
|
const missing = {
|
|
count: async () => 0,
|
|
nth() { return this; },
|
|
};
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => 'How old are you? Age' };
|
|
if (selector === 'input[placeholder="Age"]') return ageInput;
|
|
if (selector === `button:has-text("${submitLabel}")`) return finishButton;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
function makeTryAnotherWayPage({ captcha = true } = {}) {
|
|
const state = {
|
|
url: captcha
|
|
? 'https://accounts.google.com/v3/signin/challenge/recaptcha'
|
|
: 'https://accounts.google.com/o/oauth2/auth',
|
|
visible: true,
|
|
clicks: 0,
|
|
};
|
|
const button = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.visible,
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
click: async () => {
|
|
state.clicks++;
|
|
state.visible = false;
|
|
state.url = 'https://auth.openai.com/callback';
|
|
},
|
|
};
|
|
const missing = { count: async () => 0, nth() { return this; } };
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
locator: selector => {
|
|
if (selector === 'body') return {
|
|
innerText: async () => captcha
|
|
? "Verify it's you Confirm you're not a robot"
|
|
: "Verify it's you Choose another way to sign in",
|
|
};
|
|
if (selector === 'button:has-text("Try another way")') return button;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
function makeAccountRecoveryPasswordPage() {
|
|
const state = {
|
|
url: 'https://accounts.google.com/v3/signin/challenge/pwd',
|
|
password: '',
|
|
nextClicks: 0,
|
|
alternateClicks: 0,
|
|
};
|
|
|
|
function locator(selector) {
|
|
const isPassword = selector === 'input[type="password"]';
|
|
const isNext = selector.includes('button:has-text("Next")');
|
|
const isAlternate = selector.includes('Try another way');
|
|
const element = {
|
|
count: async () => (isPassword || isNext || isAlternate ? 1 : 0),
|
|
nth() { return this; },
|
|
first() { return this; },
|
|
or() { return this; },
|
|
isVisible: async () => isPassword || isNext || isAlternate,
|
|
waitFor: async () => {},
|
|
fill: async value => { if (isPassword) state.password = String(value); },
|
|
press: async () => {},
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
click: async () => {
|
|
if (isAlternate) state.alternateClicks++;
|
|
},
|
|
evaluate: async () => {
|
|
if (isNext) {
|
|
state.nextClicks++;
|
|
state.url = 'https://auth.openai.com/callback';
|
|
}
|
|
if (isAlternate) state.alternateClicks++;
|
|
return true;
|
|
},
|
|
innerText: async () => 'Account recovery Enter your password Try another way Next',
|
|
textContent: async () => '',
|
|
};
|
|
return element;
|
|
}
|
|
|
|
return {
|
|
state,
|
|
page: {
|
|
url: () => state.url,
|
|
locator,
|
|
waitForTimeout: async () => {},
|
|
waitForLoadState: async () => {},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeBirthdayPage() {
|
|
const state = {
|
|
url: 'https://auth.openai.com/create-account',
|
|
birthday: '',
|
|
inputVisible: true,
|
|
};
|
|
const birthdayInput = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.inputVisible,
|
|
inputValue: async () => state.birthday,
|
|
getAttribute: async name => (name === 'type' ? 'text' : ''),
|
|
fill: async value => { state.birthday = String(value); },
|
|
click: async () => {},
|
|
press: async () => {},
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
evaluate: async (_, value) => { state.birthday = String(value); },
|
|
};
|
|
const finishButton = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.inputVisible,
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
click: async () => {
|
|
state.inputVisible = false;
|
|
state.url = 'https://chatgpt.com/';
|
|
},
|
|
};
|
|
const missing = { count: async () => 0, nth() { return this; } };
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
waitForLoadState: async () => {},
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => "Let's confirm your age Birthday" };
|
|
if (selector === 'input[aria-label*="Birthday" i]') return birthdayInput;
|
|
if (selector === 'button:has-text("Finish creating account")') return finishButton;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
function makeSegmentedBirthdayPage() {
|
|
const state = {
|
|
url: 'https://auth.openai.com/create-account',
|
|
visible: true,
|
|
values: ['', '', ''],
|
|
};
|
|
const labels = ['month, Birthday', 'day, Birthday', 'year, Birthday'];
|
|
const segments = labels.map((label, index) => ({
|
|
isVisible: async () => state.visible,
|
|
getAttribute: async name => (name === 'aria-label' ? label : ''),
|
|
click: async () => {},
|
|
press: async () => {},
|
|
pressSequentially: async value => { state.values[index] = String(value); },
|
|
}));
|
|
const segmentList = {
|
|
count: async () => segments.length,
|
|
nth: index => segments[index],
|
|
};
|
|
const birthdayGroup = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.visible,
|
|
locator: selector => {
|
|
assert.strictEqual(selector, '[role="spinbutton"]');
|
|
return segmentList;
|
|
},
|
|
};
|
|
const finishButton = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.visible,
|
|
click: async () => {
|
|
state.visible = false;
|
|
state.url = 'https://chatgpt.com/';
|
|
},
|
|
};
|
|
const missing = { count: async () => 0, nth() { return this; } };
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
waitForLoadState: async () => {},
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => "Let's confirm your age Birthday" };
|
|
if (selector === '[role="group"][aria-label*="Birthday" i]') return birthdayGroup;
|
|
if (selector === 'button:has-text("Finish creating account")') return finishButton;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
function makeGenericBirthdayYearPage() {
|
|
const state = {
|
|
url: 'https://auth.openai.com/create-account',
|
|
visible: true,
|
|
year: '2026',
|
|
};
|
|
const yearElement = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.visible,
|
|
textContent: async () => state.year,
|
|
innerText: async () => state.year,
|
|
getAttribute: async () => '',
|
|
click: async () => {},
|
|
press: async () => {},
|
|
pressSequentially: async value => { state.year = String(value); },
|
|
};
|
|
const finishButton = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.visible,
|
|
click: async () => {
|
|
state.visible = false;
|
|
state.url = 'https://chatgpt.com/';
|
|
},
|
|
};
|
|
const missing = { count: async () => 0, nth() { return this; } };
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
waitForLoadState: async () => {},
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => "Let's confirm your age Birthday" };
|
|
if (selector === '[id^="react-aria-"] *, [id^="react-aria-"]') return yearElement;
|
|
if (selector === 'button:has-text("Finish creating account")') return finishButton;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
function makeAllSetPage() {
|
|
const state = {
|
|
url: 'https://chatgpt.com/',
|
|
visible: true,
|
|
clicks: 0,
|
|
};
|
|
const continueButton = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => state.visible,
|
|
scrollIntoViewIfNeeded: async () => {},
|
|
click: async () => {
|
|
state.clicks++;
|
|
state.visible = false;
|
|
},
|
|
};
|
|
const missing = { count: async () => 0, nth() { return this; } };
|
|
const page = {
|
|
url: () => state.url,
|
|
waitForTimeout: async () => {},
|
|
waitForLoadState: async () => {},
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => "You're all set" };
|
|
if (selector === 'button:has-text("Continue")') return continueButton;
|
|
return missing;
|
|
},
|
|
};
|
|
return { page, state };
|
|
}
|
|
|
|
(async () => {
|
|
const email = 'target@example.com';
|
|
|
|
const signedOutState = { restarted: false, gotoCount: 0 };
|
|
const signedOutMissing = { count: async () => 0, nth() { return this; } };
|
|
const loginButton = {
|
|
count: async () => signedOutState.restarted ? 0 : 1,
|
|
nth() { return this; },
|
|
isVisible: async () => !signedOutState.restarted,
|
|
};
|
|
const signedOutPage = {
|
|
url: () => 'https://chatgpt.com/',
|
|
waitForTimeout: async () => {},
|
|
goto: async () => { signedOutState.restarted = true; signedOutState.gotoCount++; },
|
|
evaluate: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
text: signedOutState.restarted
|
|
? JSON.stringify({ user: { email }, expires: '2099-01-01T00:00:00.000Z' })
|
|
: JSON.stringify({ expires: '2099-01-01T00:00:00.000Z' }),
|
|
}),
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => signedOutState.restarted ? 'Where should we begin?' : 'Log in Sign up for free' };
|
|
if (selector.includes('Log in')) return loginButton;
|
|
return signedOutMissing;
|
|
},
|
|
};
|
|
const signedOutResult = await _private.waitForChatGPTResult(signedOutPage, { email }, 23, () => {});
|
|
assert.strictEqual(signedOutResult.ok, true, '未登录首页重新发起 Google 登录后应继续检测会话');
|
|
assert.strictEqual(signedOutState.gotoCount, 1, '检测到未登录首页后应重新进入 Google 登录入口');
|
|
|
|
const routeRaceState = {
|
|
url: 'https://chatgpt.com/',
|
|
probePages: 0,
|
|
};
|
|
const routeRaceMissing = {
|
|
count: async () => 0,
|
|
nth() { return this; },
|
|
isVisible: async () => false,
|
|
};
|
|
const routeRacePage = {
|
|
url: () => routeRaceState.url,
|
|
evaluate: async () => {
|
|
// 模拟 ChatGPT 首页读取 Session 期间,OAuth 异步跳转到 Google。
|
|
routeRaceState.url = 'https://accounts.google.com/signin/rejected';
|
|
return { ok: true, status: 200, text: '{}' };
|
|
},
|
|
context: () => ({
|
|
newPage: async () => {
|
|
routeRaceState.probePages++;
|
|
return routeRaceMissing;
|
|
},
|
|
}),
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => 'Google sign in was rejected' };
|
|
return routeRaceMissing;
|
|
},
|
|
waitForTimeout: async () => {},
|
|
};
|
|
const routeRaceResult = await _private.waitForChatGPTResult(routeRacePage, { email }, 23, () => {});
|
|
assert.strictEqual(routeRaceResult.code, 'SIGNIN_REJECTED', '页面异步跳到 Google 后应重新路由到 OAuth 处理');
|
|
assert.strictEqual(routeRaceState.probePages, 0, 'Google 页面不能被误判为首页并创建 Session 重试页');
|
|
|
|
const loginWithState = {
|
|
url: 'https://chatgpt.com/auth/login_with?callback_path=%2F',
|
|
reloads: 0,
|
|
probePages: 0,
|
|
};
|
|
const loginWithPage = {
|
|
url: () => loginWithState.url,
|
|
reload: async () => {
|
|
loginWithState.reloads++;
|
|
loginWithState.url = 'https://accounts.google.com/signin/rejected';
|
|
},
|
|
context: () => ({
|
|
newPage: async () => {
|
|
loginWithState.probePages++;
|
|
return routeRaceMissing;
|
|
},
|
|
}),
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => 'Google sign in was rejected' };
|
|
return routeRaceMissing;
|
|
},
|
|
waitForTimeout: async () => {},
|
|
};
|
|
const loginWithResult = await _private.waitForChatGPTResult(loginWithPage, { email }, 23, () => {});
|
|
assert.strictEqual(loginWithResult.code, 'SIGNIN_REJECTED', '登录中转页停滞时应刷新并按新的 Google 路由继续处理');
|
|
assert.strictEqual(loginWithState.reloads, 1, '登录中转页停滞时应主动刷新');
|
|
assert.strictEqual(loginWithState.probePages, 0, '登录中转页不能被误判为 ChatGPT 首页并创建 Session 重试页');
|
|
|
|
const loadingLoginWithState = {
|
|
url: 'https://chatgpt.com/auth/login_with?callback_path=%2F&connection=google-oauth2',
|
|
ticks: 0,
|
|
reloads: 0,
|
|
};
|
|
const loadingLoginWithPage = {
|
|
url: () => loadingLoginWithState.url,
|
|
reload: async () => { loadingLoginWithState.reloads++; },
|
|
evaluate: async () => ({
|
|
available: true,
|
|
readyState: 'loading',
|
|
bodyTextLength: 0,
|
|
bodyHtmlLength: 20,
|
|
bodyElementCount: 1,
|
|
resourceCount: loadingLoginWithState.ticks >= 20 ? 1 : 0,
|
|
resourceTransferSize: loadingLoginWithState.ticks >= 20 ? 1024 : 0,
|
|
navigationResponseEnd: 0,
|
|
}),
|
|
locator: selector => {
|
|
if (selector === 'body') {
|
|
return {
|
|
innerText: async () => loadingLoginWithState.url.includes('signin/rejected')
|
|
? 'Google sign in was rejected'
|
|
: '',
|
|
};
|
|
}
|
|
return routeRaceMissing;
|
|
},
|
|
waitForTimeout: async () => {
|
|
loadingLoginWithState.ticks++;
|
|
if (loadingLoginWithState.ticks === 50) {
|
|
loadingLoginWithState.url = 'https://accounts.google.com/signin/rejected';
|
|
}
|
|
},
|
|
};
|
|
const loadingLoginWithResult = await _private.waitForChatGPTResult(loadingLoginWithPage, { email }, 23, () => {});
|
|
assert.strictEqual(loadingLoginWithResult.code, 'SIGNIN_REJECTED', '登录中转页仍有加载进展时应继续等待真实路由');
|
|
assert.strictEqual(loadingLoginWithState.reloads, 0, '网络较慢但页面仍在加载时不能误刷新');
|
|
|
|
const existingLoginState = { url: 'https://chatgpt.com/', mainNavigations: [], probeOpened: 0 };
|
|
const existingLoginPage = {
|
|
url: () => existingLoginState.url,
|
|
goto: async url => {
|
|
existingLoginState.mainNavigations.push(url);
|
|
existingLoginState.url = url;
|
|
},
|
|
evaluate: async () => ({ ok: true, status: 200, text: '{}' }),
|
|
locator: selector => {
|
|
if (selector === 'body') {
|
|
return { innerText: async () => 'ChatGPT New chat Images Library Upgrade Free' };
|
|
}
|
|
return routeRaceMissing;
|
|
},
|
|
context: () => ({
|
|
newPage: async () => {
|
|
existingLoginState.probeOpened++;
|
|
return {
|
|
goto: async () => {},
|
|
locator: () => ({
|
|
innerText: async () => JSON.stringify({
|
|
user: { email },
|
|
expires: '2099-01-01T00:00:00.000Z',
|
|
}),
|
|
}),
|
|
close: async () => {},
|
|
};
|
|
},
|
|
}),
|
|
waitForTimeout: async () => {},
|
|
};
|
|
const existingLoginResult = await loginOrRegisterChatGPT(existingLoginPage, { email }, () => {});
|
|
assert.strictEqual(existingLoginResult.ok, true, '已登录首页首次读取失败后应继续提取 Session');
|
|
assert.strictEqual(existingLoginState.probeOpened, 1);
|
|
assert.strictEqual(existingLoginState.mainNavigations.some(url => /login_with/.test(url)), false, '已登录首页不能重新走 Google OAuth');
|
|
|
|
const unknownHomeState = { url: 'https://chatgpt.com/', mainNavigations: [], probeOpened: 0 };
|
|
const unknownHomePage = {
|
|
url: () => unknownHomeState.url,
|
|
goto: async url => {
|
|
unknownHomeState.mainNavigations.push(url);
|
|
unknownHomeState.url = url;
|
|
},
|
|
evaluate: async () => ({ ok: true, status: 200, text: '{}' }),
|
|
locator: selector => {
|
|
if (selector === 'body') {
|
|
return { innerText: async () => "What's on the agenda today?" };
|
|
}
|
|
return routeRaceMissing;
|
|
},
|
|
context: () => ({
|
|
newPage: async () => {
|
|
unknownHomeState.probeOpened++;
|
|
return {
|
|
goto: async () => ({ status: () => 200, headers: async () => ({ 'content-type': 'application/json' }) }),
|
|
locator: () => ({
|
|
innerText: async () => JSON.stringify({
|
|
user: { email },
|
|
expires: '2099-01-01T00:00:00.000Z',
|
|
}),
|
|
}),
|
|
close: async () => {},
|
|
};
|
|
},
|
|
}),
|
|
waitForTimeout: async () => {},
|
|
};
|
|
const unknownHomeResult = await loginOrRegisterChatGPT(unknownHomePage, { email }, () => {});
|
|
assert.strictEqual(unknownHomeResult.ok, true, '主页文案变更但未明确退出时仍应补试 Session');
|
|
assert.strictEqual(unknownHomeState.probeOpened, 1);
|
|
assert.strictEqual(
|
|
unknownHomeState.mainNavigations.some(url => /login_with/.test(url)),
|
|
false,
|
|
'未知主页布局不能直接重走 Google OAuth',
|
|
);
|
|
|
|
const routeErrorPage = {
|
|
locator: selector => {
|
|
assert.strictEqual(selector, 'body');
|
|
return { innerText: async () => 'Oops, an error occurred! Route Error (400 Invalid content type: text/html; charset=UTF-8)' };
|
|
},
|
|
};
|
|
assert.strictEqual(await _private.detectChatGPTRouteError(routeErrorPage), true);
|
|
|
|
const authRouteMissing = {
|
|
count: async () => 0,
|
|
nth() { return this; },
|
|
isVisible: async () => false,
|
|
};
|
|
const authErrorPage = {
|
|
url: () => 'https://chatgpt.com/auth/error?error=redacted',
|
|
locator: selector => selector === 'body'
|
|
? { innerText: async () => '' }
|
|
: authRouteMissing,
|
|
waitForTimeout: async () => {},
|
|
};
|
|
const authErrorResult = await _private.waitForChatGPTResult(authErrorPage, { email }, 23, () => {});
|
|
assert.strictEqual(authErrorResult.code, 'CHATGPT_AUTH_ROUTE_ERROR');
|
|
assert.strictEqual(authErrorResult.restartBrowser, true);
|
|
|
|
const callbackState = { url: 'https://chatgpt.com/auth/callback?code=redacted' };
|
|
const callbackPage = {
|
|
url: () => callbackState.url,
|
|
locator: selector => selector === 'body'
|
|
? { innerText: async () => '' }
|
|
: authRouteMissing,
|
|
waitForTimeout: async () => { callbackState.url = 'https://chatgpt.com/'; },
|
|
evaluate: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
text: JSON.stringify({ user: { email }, expires: '2099-01-01T00:00:00.000Z' }),
|
|
}),
|
|
};
|
|
const callbackResult = await _private.waitForChatGPTResult(callbackPage, { email }, 23, () => {});
|
|
assert.strictEqual(callbackResult.ok, true, '授权回调路由应等待落到真正首页后再读取 Session');
|
|
|
|
const captchaPage = makeTryAnotherWayPage();
|
|
const captchaResult = await _private.handleGoogleOAuth(
|
|
captchaPage.page,
|
|
{ email },
|
|
() => {},
|
|
{ captchaWaitMs: 1 },
|
|
);
|
|
assert.strictEqual(captchaResult.ok, false, '明确的人机验证页等待超时后应失败');
|
|
assert.strictEqual(captchaResult.code, 'CAPTCHA');
|
|
assert.strictEqual(captchaPage.state.clicks, 0, '人机验证页不能点击 Try another way');
|
|
|
|
const clearedCaptchaState = {
|
|
url: 'https://accounts.google.com/v3/signin/challenge/recaptcha',
|
|
waits: 0,
|
|
};
|
|
const clearedCaptchaMissing = { count: async () => 0, nth() { return this; } };
|
|
const clearedCaptchaPage = {
|
|
url: () => clearedCaptchaState.url,
|
|
locator: selector => selector === 'body'
|
|
? { innerText: async () => "Verify it's you Confirm you're not a robot" }
|
|
: clearedCaptchaMissing,
|
|
waitForTimeout: async () => {
|
|
clearedCaptchaState.waits++;
|
|
if (clearedCaptchaState.waits >= 2) {
|
|
clearedCaptchaState.url = 'https://auth.openai.com/callback';
|
|
}
|
|
},
|
|
};
|
|
let captchaDetected = 0;
|
|
let captchaCleared = 0;
|
|
const clearedCaptchaResult = await _private.handleGoogleOAuth(
|
|
clearedCaptchaPage,
|
|
{ email },
|
|
() => {},
|
|
{
|
|
captchaWaitMs: 5000,
|
|
onCaptchaChallenge: () => { captchaDetected++; },
|
|
onCaptchaCleared: () => { captchaCleared++; },
|
|
},
|
|
);
|
|
assert.strictEqual(clearedCaptchaResult.ok, true,
|
|
'用户手动完成人机验证后应自动继续 OAuth 流程');
|
|
assert.strictEqual(captchaDetected, 1);
|
|
assert.strictEqual(captchaCleared, 1);
|
|
|
|
const staleChooserState = {
|
|
url: 'https://accounts.google.com/v3/signin/challenge/recaptcha?continue=redacted',
|
|
waits: 0,
|
|
accountClicks: 0,
|
|
};
|
|
const staleChooserMissing = { count: async () => 0, nth() { return this; } };
|
|
const staleChooserAccount = {
|
|
count: async () => 1,
|
|
nth() { return this; },
|
|
isVisible: async () => true,
|
|
getAttribute: async name => name === 'data-identifier' ? email : '',
|
|
innerText: async () => email,
|
|
click: async () => { staleChooserState.accountClicks++; },
|
|
};
|
|
const staleChooserPage = {
|
|
url: () => staleChooserState.url,
|
|
locator: selector => {
|
|
if (selector === 'body') return { innerText: async () => `Choose an account ${email} Use another account` };
|
|
if (selector === '[data-identifier]') return staleChooserAccount;
|
|
return staleChooserMissing;
|
|
},
|
|
waitForTimeout: async () => {
|
|
staleChooserState.waits++;
|
|
if (staleChooserState.waits >= 2) staleChooserState.url = 'https://auth.openai.com/callback';
|
|
},
|
|
};
|
|
const staleChooserLogs = [];
|
|
const staleChooserResult = await _private.handleGoogleOAuth(
|
|
staleChooserPage,
|
|
{ email },
|
|
message => staleChooserLogs.push(message),
|
|
);
|
|
assert.strictEqual(staleChooserResult.ok, true,
|
|
'recaptcha 路由仍显示旧账号选择页时应等待真实页面,不得误判 CAPTCHA');
|
|
assert.strictEqual(staleChooserState.accountClicks, 0,
|
|
'网址已进入验证路由后不得重复点击旧账号卡片');
|
|
assert(staleChooserLogs.some(message => message.includes('验证路由正在加载')));
|
|
|
|
const accountRecovery = makeAccountRecoveryPasswordPage();
|
|
const recoveryResult = await _private.handleGoogleOAuth(
|
|
accountRecovery.page,
|
|
{ email, password: 'test-password' },
|
|
() => {},
|
|
);
|
|
assert.strictEqual(recoveryResult.ok, true, 'Account recovery 密码页应完成密码提交');
|
|
assert.strictEqual(accountRecovery.state.password, 'test-password');
|
|
assert.strictEqual(accountRecovery.state.nextClicks, 1);
|
|
assert.strictEqual(accountRecovery.state.alternateClicks, 0, '存在密码框时不能优先点击 Try another way');
|
|
|
|
const alternateWay = makeTryAnotherWayPage({ captcha: false });
|
|
const alternateLogs = [];
|
|
const alternateResult = await _private.handleGoogleOAuth(
|
|
alternateWay.page,
|
|
{ email },
|
|
message => alternateLogs.push(message),
|
|
);
|
|
assert.strictEqual(alternateResult.ok, true, '非人机验证页仍应支持 Try another way');
|
|
assert.strictEqual(alternateWay.state.clicks, 1, '非人机验证页应点击 Try another way');
|
|
assert(alternateLogs.some(message => message.includes('Try another way')));
|
|
|
|
const successful = makeGoogleIdentifierPage({ transitionOnClick: true });
|
|
const successResult = await _private.fillGoogleIdentifier(
|
|
successful.page,
|
|
email,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(successResult.submitted, true, '点击 Next 并离开邮箱页后应判定为提交成功');
|
|
assert.strictEqual(successful.state.value, email, '提交前必须完整写入邮箱');
|
|
|
|
const stuck = makeGoogleIdentifierPage({ transitionOnClick: false });
|
|
const stuckResult = await _private.fillGoogleIdentifier(
|
|
stuck.page,
|
|
email,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(stuckResult.submitted, false, '点击后仍停留在邮箱页时不能误判为成功');
|
|
assert.match(stuckResult.msg, /仍停留/);
|
|
|
|
const ageSuccess = makeAgePage({ transitionOnClick: true });
|
|
const ageSuccessResult = await _private.fillAgeOnboarding(
|
|
ageSuccess.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(ageSuccessResult.ok, true, '年龄提交后离开页面才应判定成功');
|
|
assert.strictEqual(ageSuccess.state.age, '23');
|
|
|
|
const continueAge = makeAgePage({ transitionOnClick: true, submitLabel: 'Continue' });
|
|
const continueAgeResult = await _private.fillAgeOnboarding(
|
|
continueAge.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(continueAgeResult.ok, true, '年龄页按钮为 Continue 时也应提交成功');
|
|
|
|
const retriedAge = makeAgePage({ transitionOnAttempt: 3 });
|
|
const retriedAgeResult = await _private.fillAgeOnboarding(
|
|
retriedAge.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(retriedAgeResult.ok, true, '年龄页首次点击无效时应自动重试');
|
|
assert.strictEqual(retriedAge.state.clicks, 3);
|
|
|
|
const birthday = makeBirthdayPage();
|
|
const birthdayResult = await _private.fillAgeOnboarding(
|
|
birthday.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(birthdayResult.ok, true, '新版 Birthday 页面应提交成功');
|
|
assert.strictEqual(birthday.state.birthday, _private.birthdayForAge(23).local);
|
|
assert.strictEqual(_private.birthdayForAge(23).year, '2000', '生日页应固定使用 2000 年');
|
|
|
|
const segmentedBirthday = makeSegmentedBirthdayPage();
|
|
const segmentedBirthdayResult = await _private.fillAgeOnboarding(
|
|
segmentedBirthday.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
const expectedBirthday = _private.birthdayForAge(23);
|
|
assert.strictEqual(segmentedBirthdayResult.ok, true, 'React Aria 分段生日控件应提交成功');
|
|
assert.deepStrictEqual(segmentedBirthday.state.values, [
|
|
'',
|
|
'',
|
|
expectedBirthday.year,
|
|
]);
|
|
|
|
const genericBirthday = makeGenericBirthdayYearPage();
|
|
const genericBirthdayResult = await _private.fillAgeOnboarding(
|
|
genericBirthday.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(genericBirthdayResult.ok, true, '普通 React Aria 年份 div 应能提交');
|
|
assert.strictEqual(genericBirthday.state.year, '2000', '应把可见年份 2026 替换成 2000');
|
|
|
|
const allSet = makeAllSetPage();
|
|
const allSetResult = await _private.completeAllSetPage(
|
|
allSet.page,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(allSetResult.ok, true, "You're all set 页面应点击 Continue");
|
|
assert.strictEqual(allSet.state.clicks, 1);
|
|
|
|
const directAllSet = makeAllSetPage();
|
|
const directProbeState = { opened: 0, closed: 0, url: '' };
|
|
directAllSet.page.context = () => ({
|
|
newPage: async () => {
|
|
directProbeState.opened++;
|
|
return {
|
|
goto: async url => { directProbeState.url = url; },
|
|
locator: selector => {
|
|
assert.strictEqual(selector, 'body');
|
|
return {
|
|
innerText: async () => JSON.stringify({
|
|
user: { email: 'target@example.com' },
|
|
expires: '2099-01-01T00:00:00.000Z',
|
|
}),
|
|
};
|
|
},
|
|
close: async () => { directProbeState.closed++; },
|
|
};
|
|
},
|
|
});
|
|
const directAllSetResult = await _private.waitForChatGPTResult(
|
|
directAllSet.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
);
|
|
assert.strictEqual(directAllSetResult.ok, true, "You're all set 页面应直接提取 Session");
|
|
assert.strictEqual(directAllSetResult.status, '已注册');
|
|
assert.strictEqual(directAllSet.state.clicks, 0, '直接提取成功后不应再依赖 Continue 跳转');
|
|
assert.strictEqual(directProbeState.opened, 1);
|
|
assert.strictEqual(directProbeState.closed, 1, 'Session 临时页必须及时关闭');
|
|
assert.strictEqual(directProbeState.url, 'https://chatgpt.com/api/auth/session');
|
|
|
|
const throttledAllSet = makeAllSetPage();
|
|
throttledAllSet.page.context = () => ({
|
|
newPage: async () => ({
|
|
goto: async () => ({
|
|
status: () => 429,
|
|
headers: async () => ({ 'content-type': 'application/json' }),
|
|
}),
|
|
locator: () => ({ innerText: async () => '{}' }),
|
|
close: async () => {},
|
|
}),
|
|
});
|
|
const throttledAllSetResult = await _private.waitForChatGPTResult(
|
|
throttledAllSet.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
);
|
|
assert.strictEqual(throttledAllSetResult.code, 'CHATGPT_SESSION_HTTP_429');
|
|
assert.strictEqual(throttledAllSet.state.clicks, 0, 'Session 接口明确限流时不应覆盖为 Continue 或通用超时错误');
|
|
|
|
const retryProbeState = { attempts: 0, closed: 0, timeouts: [], retryDelays: [] };
|
|
const retryProbePage = {
|
|
context: () => ({
|
|
newPage: async () => ({
|
|
goto: async (_url, options) => {
|
|
retryProbeState.attempts++;
|
|
retryProbeState.timeouts.push(options.timeout);
|
|
if (retryProbeState.attempts < 5) throw new Error('temporary network error');
|
|
},
|
|
locator: () => ({
|
|
innerText: async () => JSON.stringify({
|
|
user: { email: 'target@example.com' },
|
|
expires: '2099-01-01T00:00:00.000Z',
|
|
}),
|
|
}),
|
|
close: async () => { retryProbeState.closed++; },
|
|
}),
|
|
}),
|
|
waitForTimeout: async ms => { retryProbeState.retryDelays.push(ms); },
|
|
};
|
|
const fifthAttemptSession = await _private.readChatGPTSessionInProbePage(
|
|
retryProbePage,
|
|
() => {},
|
|
);
|
|
assert.strictEqual(_private.isAuthenticatedSession(fifthAttemptSession), true, '前4次失败后第5次成功仍应提取 Session');
|
|
assert.strictEqual(retryProbeState.attempts, 5, 'Session 最多应尝试5次');
|
|
assert.deepStrictEqual(retryProbeState.timeouts, [10000, 10000, 10000, 10000, 10000], '每次读取超时应为10秒');
|
|
assert.strictEqual(retryProbeState.retryDelays.length, 4, '5次请求之间应等待4次');
|
|
assert(retryProbeState.retryDelays.every(ms => ms >= 9000), '快速返回空Session时,两次请求起点至少应间隔约10秒');
|
|
assert.strictEqual(retryProbeState.closed, 1);
|
|
|
|
const navigationState = { url: 'https://chatgpt.com/' };
|
|
const navigationPage = {
|
|
url: () => navigationState.url,
|
|
goto: async url => { navigationState.url = url; },
|
|
locator: selector => {
|
|
assert.strictEqual(selector, 'body');
|
|
return { innerText: async () => JSON.stringify({ user: { email: 'target@example.com' } }) };
|
|
},
|
|
};
|
|
const navigationSession = await _private.readChatGPTSessionByNavigation(navigationPage, () => {});
|
|
assert.strictEqual(_private.isAuthenticatedSession(navigationSession), true);
|
|
assert.strictEqual(navigationState.url, 'https://chatgpt.com/', '读取 Session 后应返回 ChatGPT 首页');
|
|
|
|
const guestState = { authenticated: false, url: 'https://chatgpt.com/', loginStarts: 0 };
|
|
const emptyLocator = {
|
|
count: async () => 0,
|
|
nth() { return this; },
|
|
isVisible: async () => false,
|
|
};
|
|
const guestPage = {
|
|
url: () => guestState.url,
|
|
evaluate: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
text: JSON.stringify(guestState.authenticated
|
|
? { user: { email: 'target@example.com' }, expires: '2099-01-01T00:00:00.000Z' }
|
|
: {}),
|
|
}),
|
|
locator: selector => {
|
|
if (selector === 'body') {
|
|
return {
|
|
innerText: async () => guestState.authenticated
|
|
? 'Where should we begin?'
|
|
: 'Where should we begin? Sign up for free Get responses tailored to you Log in to get answers based on saved chats',
|
|
};
|
|
}
|
|
return emptyLocator;
|
|
},
|
|
goto: async url => {
|
|
assert.match(url, /login_with/);
|
|
guestState.loginStarts++;
|
|
guestState.authenticated = true;
|
|
guestState.url = 'https://chatgpt.com/';
|
|
},
|
|
waitForTimeout: async () => {},
|
|
};
|
|
assert.strictEqual(await _private.isSignedOutChatGPTHome(guestPage), true, '游客首页正文也必须被识别');
|
|
const guestRecovery = await _private.waitForChatGPTResult(
|
|
guestPage,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
);
|
|
assert.strictEqual(guestState.loginStarts, 1, '游客首页必须重新发起 Google 登录');
|
|
assert.strictEqual(guestRecovery.ok, true);
|
|
assert.strictEqual(guestRecovery.session.user.email, 'target@example.com');
|
|
|
|
const ageStuck = makeAgePage({ transitionOnClick: false });
|
|
const ageStuckResult = await _private.fillAgeOnboarding(
|
|
ageStuck.page,
|
|
{ email: 'target@example.com' },
|
|
23,
|
|
() => {},
|
|
{ transitionTimeout: 5 },
|
|
);
|
|
assert.strictEqual(ageStuckResult.ok, false, '年龄页未离开时不能误判为成功');
|
|
assert.strictEqual(ageStuckResult.retryable, true);
|
|
assert.strictEqual(ageStuckResult.code, 'CHATGPT_AGE_SUBMIT_STUCK');
|
|
|
|
const realDateNow = Date.now;
|
|
let virtualNow = 0;
|
|
try {
|
|
Date.now = () => virtualNow;
|
|
const deadlineAge = makeAgePage({ transitionOnClick: false });
|
|
deadlineAge.page.waitForTimeout = async ms => {
|
|
virtualNow += Math.max(1, Number(ms) || 0) * 10;
|
|
};
|
|
const deadlineAgeResult = await _private.waitForChatGPTResult(
|
|
deadlineAge.page,
|
|
{ email },
|
|
23,
|
|
() => {},
|
|
);
|
|
assert.strictEqual(
|
|
deadlineAgeResult.code,
|
|
'CHATGPT_AGE_SUBMIT_STUCK',
|
|
'年龄阶段自身耗时超过总截止时间时应保留明确阶段错误,不能折叠为 Session 超时',
|
|
);
|
|
} finally {
|
|
Date.now = realDateNow;
|
|
}
|
|
|
|
console.log('chatgpt_service.test.js: PASS');
|
|
})().catch(error => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|
|
|
|
assert.ok(Array.isArray(RECIPES.chatgpt_register));
|
|
assert.strictEqual(RECIPES.chatgpt_register.length, 1);
|
|
assert.strictEqual(typeof RECIPES.chatgpt_register[0], 'function');
|
|
assert.strictEqual(ATOM_STEPS.chatgpt_register.step, RECIPES.chatgpt_register[0]);
|
|
assert.strictEqual(getFailureLabel('CHATGPT_SESSION_TIMEOUT'), 'ChatGPT失败');
|
|
assert.strictEqual(getFailureLabel('CHATGPT_AGE_INPUT_FAILED'), 'ChatGPT失败');
|
|
assert.strictEqual(getFailureLabel('CHATGPT_AUTH_ROUTE_ERROR'), 'ChatGPT失败');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
|
|
const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
|
|
assert.match(html, /ChatGPT登录\/注册/);
|
|
assert.match(html, /ChatGPT 注册年龄/);
|
|
assert.match(html, /dashboardStats\.tierCounts\.level3\['ChatGPT成功'\]/);
|
|
assert.match(html, /dashboardStats\.tierCounts\.level3\['ChatGPT失败'\]/);
|
|
assert.match(appSource, /runTask\('chatgpt_register'/);
|
|
assert.match(appSource, /chatgpt_status/);
|