234 lines
10 KiB
JavaScript
234 lines
10 KiB
JavaScript
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;
|
|
});
|