feat: 更新 OAuth 流程中的日志信息,增强密码缺失时的处理逻辑,添加相关测试

This commit is contained in:
QLHazyCoder
2026-04-20 01:05:09 +08:00
parent da7cddee8f
commit 0d0cf4b2af
4 changed files with 232 additions and 9 deletions
+3 -5
View File
@@ -6600,11 +6600,10 @@ async function runPreStep6CookieCleanup() {
async function refreshOAuthUrlBeforeStep6(state) {
if (state?.contributionModeExpected && !state?.contributionMode) {
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA 面板。请重新进入贡献模式后再点击自动。');
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 链路。请重新进入贡献模式后再点击自动。');
}
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
await addLog('步骤 7contributionMode=true正在通过公开贡献接口申请 OAuth 链接...', 'info');
await addLog('步骤 7:贡献模式正在申请贡献登录地址...');
await addLog('步骤 7contributionMode=true公开贡献接口,正在申请 OAuth 登录地址...', 'info');
const contributionState = await contributionOAuthManager.startContributionFlow({
nickname: state.email,
openAuthTab: false,
@@ -6617,8 +6616,7 @@ async function refreshOAuthUrlBeforeStep6(state) {
await handleStepData(1, { oauthUrl });
return oauthUrl;
}
await addLog(`步骤 7正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`);
await addLog(`步骤 7contributionMode=${Boolean(state?.contributionMode)},当前将回退到 ${getPanelModeLabel(state)} 面板刷新 OAuth。`, 'warn');
await addLog(`步骤 7contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
await handleStepData(1, refreshResult);
+10 -2
View File
@@ -1913,10 +1913,18 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
async function step6LoginFromPasswordPage(payload, snapshot) {
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
const hasPassword = Boolean(String(payload?.password || '').trim());
if (currentSnapshot.passwordInput) {
if (!payload.password) {
throw new Error('登录时缺少密码,步骤 7 无法继续。');
if (!hasPassword) {
if (currentSnapshot.switchTrigger) {
log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn');
return step6SwitchToOneTimeCodeLogin(currentSnapshot);
}
return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, {
message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。',
});
}
log('步骤 7:已进入密码页,准备填写密码...');
+58 -2
View File
@@ -458,8 +458,7 @@ return { refreshOAuthUrlBeforeStep6 };
assert.equal(oauthUrl, 'https://auth.example.com/oauth?state=oauth-state-001');
assert.deepStrictEqual(calls, [
{ type: 'log', message: '步骤 7contributionMode=true正在通过公开贡献接口申请 OAuth 链接...' },
{ type: 'log', message: '步骤 7:贡献模式正在申请贡献登录地址...' },
{ type: 'log', message: '步骤 7contributionMode=true公开贡献接口,正在申请 OAuth 登录地址...' },
{
type: 'contribution',
options: {
@@ -488,6 +487,63 @@ return { refreshOAuthUrlBeforeStep6 };
delete globalThis.LOG_PREFIX;
});
test('refreshOAuthUrlBeforeStep6 logs the normal CPA/SUB2API path explicitly when contributionMode=false', async () => {
const bundle = extractFunction(backgroundSource, 'refreshOAuthUrlBeforeStep6');
const calls = [];
const api = new Function(`
${bundle}
return { refreshOAuthUrlBeforeStep6 };
`)();
globalThis.addLog = async (message) => {
calls.push({ type: 'log', message });
};
globalThis.contributionOAuthManager = {
async startContributionFlow() {
calls.push({ type: 'contribution' });
return {
contributionAuthUrl: 'https://auth.example.com/oauth?state=unexpected',
};
},
};
globalThis.handleStepData = async (step, payload) => {
calls.push({ type: 'step', step, payload });
};
globalThis.getPanelModeLabel = () => 'SUB2API';
globalThis.requestOAuthUrlFromPanel = async () => {
calls.push({ type: 'panel' });
return { oauthUrl: 'https://panel.example.com/oauth' };
};
globalThis.LOG_PREFIX = '[test]';
const oauthUrl = await api.refreshOAuthUrlBeforeStep6({
contributionMode: false,
panelMode: 'sub2api',
email: 'user@example.com',
});
assert.equal(oauthUrl, 'https://panel.example.com/oauth');
assert.deepStrictEqual(calls, [
{ type: 'log', message: '步骤 7contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:SUB2API),正在刷新 OAuth 登录地址...' },
{ type: 'panel' },
{
type: 'step',
step: 1,
payload: {
oauthUrl: 'https://panel.example.com/oauth',
},
},
]);
delete globalThis.addLog;
delete globalThis.contributionOAuthManager;
delete globalThis.handleStepData;
delete globalThis.getPanelModeLabel;
delete globalThis.requestOAuthUrlFromPanel;
delete globalThis.LOG_PREFIX;
});
test('executeStep10 blocks silent fallback when contributionModeExpected=true but contributionMode=false', async () => {
const bundle = extractFunction(backgroundSource, 'executeStep10');
+161
View File
@@ -0,0 +1,161 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/signup-page.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
const bundle = extractFunction('step6LoginFromPasswordPage');
function createApi() {
return new Function(`
${bundle}
return { step6LoginFromPasswordPage };
`)();
}
function cleanupGlobals() {
delete globalThis.normalizeStep6Snapshot;
delete globalThis.inspectLoginAuthState;
delete globalThis.log;
delete globalThis.step6SwitchToOneTimeCodeLogin;
delete globalThis.createStep6RecoverableResult;
delete globalThis.fillInput;
delete globalThis.humanPause;
delete globalThis.sleep;
delete globalThis.triggerLoginSubmitAction;
delete globalThis.waitForStep6PasswordSubmitTransition;
}
test('step6LoginFromPasswordPage switches to one-time-code login when password is missing but switch trigger exists', async () => {
const api = createApi();
const logs = [];
const snapshot = {
state: 'password_page',
passwordInput: { id: 'password' },
switchTrigger: { id: 'otp' },
};
globalThis.normalizeStep6Snapshot = (value) => value;
globalThis.inspectLoginAuthState = () => snapshot;
globalThis.log = (message, level = 'info') => {
logs.push({ message, level });
};
globalThis.step6SwitchToOneTimeCodeLogin = async (value) => {
assert.strictEqual(value, snapshot);
return { step6Outcome: 'success', via: 'switch_to_one_time_code_login' };
};
globalThis.createStep6RecoverableResult = () => {
throw new Error('should not create recoverable result when switch trigger exists');
};
globalThis.fillInput = () => {
throw new Error('should not fill password when password is missing');
};
globalThis.humanPause = async () => {};
globalThis.sleep = async () => {};
globalThis.triggerLoginSubmitAction = async () => {};
globalThis.waitForStep6PasswordSubmitTransition = async () => {
throw new Error('should not submit password when password is missing');
};
try {
const result = await api.step6LoginFromPasswordPage({ email: 'user@example.com', password: '' }, snapshot);
assert.deepStrictEqual(result, { step6Outcome: 'success', via: 'switch_to_one_time_code_login' });
assert.deepStrictEqual(logs, [
{ message: '步骤 7:当前未提供密码,改走一次性验证码登录。', level: 'warn' },
]);
} finally {
cleanupGlobals();
}
});
test('step6LoginFromPasswordPage returns a recoverable result when password is missing and no one-time-code trigger exists', async () => {
const api = createApi();
const snapshot = {
state: 'password_page',
passwordInput: { id: 'password' },
switchTrigger: null,
};
globalThis.normalizeStep6Snapshot = (value) => value;
globalThis.inspectLoginAuthState = () => snapshot;
globalThis.log = () => {};
globalThis.step6SwitchToOneTimeCodeLogin = async () => {
throw new Error('should not switch without a one-time-code trigger');
};
globalThis.createStep6RecoverableResult = (reason, stateSnapshot, details) => ({
step6Outcome: 'recoverable',
reason,
stateSnapshot,
...details,
});
globalThis.fillInput = () => {
throw new Error('should not fill password when password is missing');
};
globalThis.humanPause = async () => {};
globalThis.sleep = async () => {};
globalThis.triggerLoginSubmitAction = async () => {};
globalThis.waitForStep6PasswordSubmitTransition = async () => {
throw new Error('should not submit password when password is missing');
};
try {
const result = await api.step6LoginFromPasswordPage({ email: 'user@example.com', password: '' }, snapshot);
assert.deepStrictEqual(result, {
step6Outcome: 'recoverable',
reason: 'missing_password_and_one_time_code_trigger',
stateSnapshot: snapshot,
message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。',
});
} finally {
cleanupGlobals();
}
});