const assert = require('assert'); const fs = require('fs'); const path = require('path'); const screenshotPath = require.resolve('../src/utils/screenshot'); const screenshotModule = require(screenshotPath); const originalSaveErrorScreenshot = screenshotModule.saveErrorScreenshot; screenshotModule.saveErrorScreenshot = async () => null; function loadStepWithResult(stepModule, serviceModule, exportName, result) { const stepPath = require.resolve(stepModule); const servicePath = require.resolve(serviceModule); const service = require(servicePath); const original = service[exportName]; service[exportName] = typeof result === 'function' ? result : async () => result; delete require.cache[stepPath]; const step = require(stepPath); service[exportName] = original; return step; } function createContext(flags = {}, accountExtras = {}) { const state = { password_changed: 0, fa_changed: 0, ...flags }; const statuses = []; const messages = []; const dbWrites = []; const account = { email: 'status@example.com', ...accountExtras }; return { ctx: { account, page: { url: () => 'https://myaccount.google.com/' }, log: () => {}, results: {}, shouldStop: false, keepBrowserOpen: false, dbWrite: async (name, args) => { dbWrites.push({ name, args }); if (name === 'getSetting') return args[1]; if (name === 'getAccount') return { email: account.email, ...state }; if (name === 'updateAccountPassword') state.password_changed = 1; if (name === 'updateAccountFlag') state[args[1]] = args[2]; return true; }, updateStatus: async (_email, status) => statuses.push(status), updateMessage: async (_email, message) => messages.push(message), }, statuses, messages, dbWrites, }; } (async () => { try { let change2faStep = loadStepWithResult( '../src/pipeline/steps/change_2fa', '../src/services/change_2fa_service', 'change2faForAccount', [true, '2FA密钥更改成功', 'NEW2FAKEY'], ); let run = createContext({ password_changed: 1 }); await change2faStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '未验证已改密和2FA', '已改密账号改2FA成功后应等待ChatGPT验证'); run = createContext({ password_changed: 0 }); await change2faStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '2FA修改成功', '未改密账号改2FA成功后应显示2FA修改成功'); run = createContext({ password_changed: 1, chatgpt_status: '失败' }); await change2faStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '未注册成功已改密和2FA', 'ChatGPT已失败的账号完成2FA后应进入新失败状态'); assert.strictEqual(run.messages.length, 0, '后完成2FA时不得覆盖原ChatGPT失败处理结论'); assert.strictEqual( run.dbWrites.some(write => write.name === 'updateAccountStatusReason' && write.args[1] == null), false, '后完成2FA时不得清除原ChatGPT失败原因', ); let change2faCalls = 0; change2faStep = loadStepWithResult( '../src/pipeline/steps/change_2fa', '../src/services/change_2fa_service', 'change2faForAccount', async () => { change2faCalls++; return [true, '2FA密钥更改成功', 'NEW2FAKEY']; }, ); run = createContext({ password_changed: 1, fa_changed: 1, secret_key: 'EXISTING2FAKEY' }); run.ctx.taskType = 'security_chatgpt'; run.ctx.params = { skipCompletedSecuritySteps: true }; run.ctx.results.changePassword = { success: true, skipped: true }; await change2faStep(run.ctx); assert.strictEqual(change2faCalls, 0, '组合任务选择跳过时,已完成的2FA不得再次修改'); assert.strictEqual(run.ctx.results.change2fa.skipped, true); assert.strictEqual(run.statuses.at(-1), '未验证已改密和2FA'); assert.match(run.messages.at(-1), /密码和2FA均已修改/); run = createContext({ password_changed: 1, fa_changed: 1 }); run.ctx.taskType = 'security_chatgpt'; run.ctx.params = { skipCompletedSecuritySteps: false }; await change2faStep(run.ctx); assert.strictEqual(change2faCalls, 1, '组合任务选择覆盖时必须继续修改2FA'); run = createContext({ password_changed: 1, fa_changed: 1 }); run.ctx.taskType = 'change_2fa'; run.ctx.params = { skipCompletedSecuritySteps: true }; await change2faStep(run.ctx); assert.strictEqual(change2faCalls, 2, '单独更改2FA不得受组合任务跳过选项影响'); change2faStep = loadStepWithResult( '../src/pipeline/steps/change_2fa', '../src/services/change_2fa_service', 'change2faForAccount', [false, '无法提取新的2FA密钥', null], ); run = createContext({ password_changed: 1 }); await change2faStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '2FA修改失败', '进入2FA步骤后修改失败应显示2FA修改失败'); const changePasswordStep = loadStepWithResult( '../src/pipeline/steps/change_password', '../src/services/change_password_service', 'changePasswordForAccount', [true, '密码更改成功', 'NewPassword123'], ); run = createContext({ fa_changed: 1 }); await changePasswordStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '未验证已改密和2FA', '已改2FA账号改密成功后应等待ChatGPT验证'); run = createContext({ fa_changed: 0 }); await changePasswordStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '改密成功', '未改2FA账号改密成功后应保持改密成功'); run = createContext({ fa_changed: 1, chatgpt_status: '失败' }); await changePasswordStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '未注册成功已改密和2FA', 'ChatGPT已失败的账号完成改密后应进入新失败状态'); assert.strictEqual(run.messages.length, 0, '后完成改密时不得覆盖原ChatGPT失败处理结论'); assert.strictEqual( run.dbWrites.some(write => write.name === 'updateAccountStatusReason' && write.args[1] == null), false, '后完成改密时不得清除原ChatGPT失败原因', ); let changePasswordCalls = 0; const guardedChangePasswordStep = loadStepWithResult( '../src/pipeline/steps/change_password', '../src/services/change_password_service', 'changePasswordForAccount', async () => { changePasswordCalls++; return [true, '密码更改成功', 'NewPassword123']; }, ); run = createContext({ password_changed: 1, fa_changed: 0, password: 'ExistingPassword' }); run.ctx.taskType = 'security_chatgpt'; run.ctx.params = { skipCompletedSecuritySteps: true }; await guardedChangePasswordStep(run.ctx); assert.strictEqual(changePasswordCalls, 0, '组合任务选择跳过时,已完成的密码不得再次修改'); assert.strictEqual(run.ctx.results.changePassword.skipped, true); assert.strictEqual(run.statuses.at(-1), '改密成功'); run = createContext({ password_changed: 1, fa_changed: 0 }); run.ctx.taskType = 'security_chatgpt'; run.ctx.params = { skipCompletedSecuritySteps: false }; await guardedChangePasswordStep(run.ctx); assert.strictEqual(changePasswordCalls, 1, '组合任务选择覆盖时必须继续修改密码'); run = createContext({ password_changed: 1, fa_changed: 0 }); run.ctx.taskType = 'change_password'; run.ctx.params = { skipCompletedSecuritySteps: true }; await guardedChangePasswordStep(run.ctx); assert.strictEqual(changePasswordCalls, 2, '单独改密码不得受组合任务跳过选项影响'); run = createContext({ fa_changed: 1 }, { _forcedPasswordChangeHandled: true }); await changePasswordStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '未验证已改密和2FA', '登录阶段强制改密完成后也应等待ChatGPT验证'); const chatgptRegisterStep = loadStepWithResult( '../src/pipeline/steps/chatgpt_register', '../src/services/chatgpt_service', 'loginOrRegisterChatGPT', { ok: true, created: false, status: '已登录', session: { user: { email: 'status@example.com' }, token: 'test-session' }, }, ); run = createContext({ password_changed: 1, fa_changed: 1 }); await chatgptRegisterStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '已验证改密和2FA', '两项安全修改完成后ChatGPT成功才应进入已验证状态'); run = createContext({ password_changed: 1, fa_changed: 0 }); await chatgptRegisterStep(run.ctx); assert.strictEqual(run.statuses.at(-1), 'ChatGPT成功', '安全修改未全部完成时ChatGPT成功不应误标为已验证两项修改'); const failedChatgptRegisterStep = loadStepWithResult( '../src/pipeline/steps/chatgpt_register', '../src/services/chatgpt_service', 'loginOrRegisterChatGPT', { ok: false, code: 'CHATGPT_AUTH_ROUTE_ERROR', msg: 'ChatGPT登录失败' }, ); run = createContext({ password_changed: 1, fa_changed: 1 }); await failedChatgptRegisterStep(run.ctx); assert.strictEqual(run.statuses.at(-1), '未注册成功已改密和2FA', '安全修改均完成后ChatGPT失败应进入新失败状态'); assert.strictEqual(run.ctx.shouldStop, true); run = createContext({ password_changed: 1, fa_changed: 0 }); await failedChatgptRegisterStep(run.ctx); assert.strictEqual(run.statuses.at(-1), 'ChatGPT失败', '安全修改未全部完成时仍应保持普通ChatGPT失败'); const root = path.join(__dirname, '..'); const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8'); const appSource = fs.readFileSync(path.join(root, 'public', 'app.js'), 'utf8'); assert.ok( (html.match(/value="未注册成功已改密和2FA"/g) || []).length >= 2, '原地下拉和批量状态下拉都应包含新状态', ); assert.match(appSource, /_LOGIN_FAIL_STATUSES:[\s\S]{0,160}未注册成功已改密和2FA/, '新状态必须归入登录失败分类'); assert.match(appSource, /startsWith\('未注册成功已改密和2FA'\)[\s\S]{0,80}status-ChatGPT失败/, '新状态应使用失败颜色'); console.log('security_completion_status.test.js: PASS'); } finally { screenshotModule.saveErrorScreenshot = originalSaveErrorScreenshot; } })().catch(error => { console.error(error); process.exitCode = 1; });