feat: 更新 OAuth 流程中的日志信息,增强密码缺失时的处理逻辑,添加相关测试
This commit is contained in:
+3
-5
@@ -6600,11 +6600,10 @@ async function runPreStep6CookieCleanup() {
|
|||||||
|
|
||||||
async function refreshOAuthUrlBeforeStep6(state) {
|
async function refreshOAuthUrlBeforeStep6(state) {
|
||||||
if (state?.contributionModeExpected && !state?.contributionMode) {
|
if (state?.contributionModeExpected && !state?.contributionMode) {
|
||||||
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA 面板。请重新进入贡献模式后再点击自动。');
|
throw new Error('步骤 7:当前自动流程预期使用贡献模式,但运行态 contributionMode 已丢失,已阻止回退到普通 CPA / SUB2API 链路。请重新进入贡献模式后再点击自动。');
|
||||||
}
|
}
|
||||||
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
|
if (state?.contributionMode && contributionOAuthManager?.startContributionFlow) {
|
||||||
await addLog('步骤 7:contributionMode=true,正在通过公开贡献接口申请 OAuth 链接...', 'info');
|
await addLog('步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...', 'info');
|
||||||
await addLog('步骤 7:贡献模式正在申请贡献登录地址...');
|
|
||||||
const contributionState = await contributionOAuthManager.startContributionFlow({
|
const contributionState = await contributionOAuthManager.startContributionFlow({
|
||||||
nickname: state.email,
|
nickname: state.email,
|
||||||
openAuthTab: false,
|
openAuthTab: false,
|
||||||
@@ -6617,8 +6616,7 @@ async function refreshOAuthUrlBeforeStep6(state) {
|
|||||||
await handleStepData(1, { oauthUrl });
|
await handleStepData(1, { oauthUrl });
|
||||||
return oauthUrl;
|
return oauthUrl;
|
||||||
}
|
}
|
||||||
await addLog(`步骤 7:正在刷新登录用的 ${getPanelModeLabel(state)} OAuth 链接...`);
|
await addLog(`步骤 7:contributionMode=false,走普通 CPA / SUB2API 链路(当前面板:${getPanelModeLabel(state)}),正在刷新 OAuth 登录地址...`, 'info');
|
||||||
await addLog(`步骤 7:contributionMode=${Boolean(state?.contributionMode)},当前将回退到 ${getPanelModeLabel(state)} 面板刷新 OAuth。`, 'warn');
|
|
||||||
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
|
console.log(LOG_PREFIX, '[refreshOAuthUrlBeforeStep6] requesting fresh OAuth directly from panel');
|
||||||
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
|
const refreshResult = await requestOAuthUrlFromPanel(state, { logLabel: '步骤 7' });
|
||||||
await handleStepData(1, refreshResult);
|
await handleStepData(1, refreshResult);
|
||||||
|
|||||||
+10
-2
@@ -1913,10 +1913,18 @@ async function step6SwitchToOneTimeCodeLogin(snapshot) {
|
|||||||
|
|
||||||
async function step6LoginFromPasswordPage(payload, snapshot) {
|
async function step6LoginFromPasswordPage(payload, snapshot) {
|
||||||
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
|
||||||
|
const hasPassword = Boolean(String(payload?.password || '').trim());
|
||||||
|
|
||||||
if (currentSnapshot.passwordInput) {
|
if (currentSnapshot.passwordInput) {
|
||||||
if (!payload.password) {
|
if (!hasPassword) {
|
||||||
throw new Error('登录时缺少密码,步骤 7 无法继续。');
|
if (currentSnapshot.switchTrigger) {
|
||||||
|
log('步骤 7:当前未提供密码,改走一次性验证码登录。', 'warn');
|
||||||
|
return step6SwitchToOneTimeCodeLogin(currentSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createStep6RecoverableResult('missing_password_and_one_time_code_trigger', currentSnapshot, {
|
||||||
|
message: '登录时未提供密码,且当前页面没有可用的一次性验证码登录入口。',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log('步骤 7:已进入密码页,准备填写密码...');
|
log('步骤 7:已进入密码页,准备填写密码...');
|
||||||
|
|||||||
@@ -458,8 +458,7 @@ return { refreshOAuthUrlBeforeStep6 };
|
|||||||
|
|
||||||
assert.equal(oauthUrl, 'https://auth.example.com/oauth?state=oauth-state-001');
|
assert.equal(oauthUrl, 'https://auth.example.com/oauth?state=oauth-state-001');
|
||||||
assert.deepStrictEqual(calls, [
|
assert.deepStrictEqual(calls, [
|
||||||
{ type: 'log', message: '步骤 7:contributionMode=true,正在通过公开贡献接口申请 OAuth 链接...' },
|
{ type: 'log', message: '步骤 7:contributionMode=true,走公开贡献接口,正在申请 OAuth 登录地址...' },
|
||||||
{ type: 'log', message: '步骤 7:贡献模式正在申请贡献登录地址...' },
|
|
||||||
{
|
{
|
||||||
type: 'contribution',
|
type: 'contribution',
|
||||||
options: {
|
options: {
|
||||||
@@ -488,6 +487,63 @@ return { refreshOAuthUrlBeforeStep6 };
|
|||||||
delete globalThis.LOG_PREFIX;
|
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: '步骤 7:contributionMode=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 () => {
|
test('executeStep10 blocks silent fallback when contributionModeExpected=true but contributionMode=false', async () => {
|
||||||
const bundle = extractFunction(backgroundSource, 'executeStep10');
|
const bundle = extractFunction(backgroundSource, 'executeStep10');
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user