feat: add account run history module and logging functionality
- Implemented `background/account-run-history.js` to persist account run results (success, failure, stop) in `chrome.storage.local` and log to a text file when the local Hotmail helper is enabled. - Enhanced `background/auto-run-controller.js` and `background/message-router.js` to utilize the new account run record functionality. - Updated steps in `background/steps/fetch-login-code.js`, `background/steps/fetch-signup-code.js`, and `background/steps/oauth-login.js` to handle retries and logging appropriately. - Introduced tests for account run history and step retry limits to ensure functionality and reliability. - Modified documentation to reflect new features and changes in behavior for specific providers (e.g., 2925).
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports account run history module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/account-run-history\.js/);
|
||||
});
|
||||
|
||||
test('account run history module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createAccountRunHistoryHelpers, 'function');
|
||||
});
|
||||
|
||||
test('account run history helper normalizes records and persists without helper upload when local helper is disabled', async () => {
|
||||
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
|
||||
|
||||
let storedHistory = [{ email: 'old@example.com', password: 'old-pass', status: 'success', recordedAt: '2026-04-17T00:00:00.000Z' }];
|
||||
let fetchCalled = false;
|
||||
global.fetch = async () => {
|
||||
fetchCalled = true;
|
||||
throw new Error('should not call fetch');
|
||||
};
|
||||
|
||||
const helpers = api.createAccountRunHistoryHelpers({
|
||||
ACCOUNT_RUN_HISTORY_STORAGE_KEY: 'accountRunHistory',
|
||||
addLog: async () => {},
|
||||
buildHotmailLocalEndpoint: (baseUrl, path) => `${baseUrl}${path}`,
|
||||
chrome: {
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ accountRunHistory: storedHistory }),
|
||||
set: async (payload) => {
|
||||
storedHistory = payload.accountRunHistory;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getState: async () => ({
|
||||
email: ' latest@example.com ',
|
||||
password: ' secret ',
|
||||
hotmailServiceMode: 'remote',
|
||||
hotmailLocalBaseUrl: '',
|
||||
}),
|
||||
HOTMAIL_SERVICE_MODE_LOCAL: 'local',
|
||||
normalizeHotmailLocalBaseUrl: (value) => String(value || '').trim(),
|
||||
});
|
||||
|
||||
const record = helpers.buildAccountRunHistoryRecord(
|
||||
{ email: ' latest@example.com ', password: ' secret ' },
|
||||
' FAILED ',
|
||||
' reason '
|
||||
);
|
||||
assert.deepStrictEqual(record, {
|
||||
email: 'latest@example.com',
|
||||
password: 'secret',
|
||||
status: 'failed',
|
||||
recordedAt: record.recordedAt,
|
||||
reason: 'reason',
|
||||
});
|
||||
|
||||
const appended = await helpers.appendAccountRunRecord('failed', null, 'boom');
|
||||
assert.equal(appended.email, 'latest@example.com');
|
||||
assert.equal(appended.status, 'failed');
|
||||
assert.equal(storedHistory.length, 2);
|
||||
assert.equal(storedHistory[1].reason, 'boom');
|
||||
assert.equal(fetchCalled, false);
|
||||
assert.equal(helpers.shouldAppendAccountRunTextFile({ hotmailServiceMode: 'remote', hotmailLocalBaseUrl: 'http://127.0.0.1:17373' }), false);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep6;`)(globalScope);
|
||||
|
||||
test('step 6 retries up to configured limit and then fails', async () => {
|
||||
const events = {
|
||||
cleanupCalls: 0,
|
||||
refreshCalls: 0,
|
||||
sendCalls: 0,
|
||||
completed: 0,
|
||||
};
|
||||
|
||||
const executor = api.createStep6Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {
|
||||
events.completed += 1;
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => {
|
||||
events.refreshCalls += 1;
|
||||
return `https://oauth.example/${events.refreshCalls}`;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
runPreStep6CookieCleanup: async () => {
|
||||
events.cleanupCalls += 1;
|
||||
},
|
||||
sendToContentScriptResilient: async () => {
|
||||
events.sendCalls += 1;
|
||||
return {
|
||||
step6Outcome: 'recoverable',
|
||||
state: 'email_page',
|
||||
message: '当前仍停留在邮箱页',
|
||||
};
|
||||
},
|
||||
shouldSkipLoginVerificationForCpaCallback: () => false,
|
||||
skipLoginVerificationStepsForCpaCallback: async () => {},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep6({ email: 'user@example.com', password: 'secret' }),
|
||||
/已重试 2 次,仍未成功/
|
||||
);
|
||||
|
||||
assert.equal(events.cleanupCalls, 1);
|
||||
assert.equal(events.refreshCalls, 3);
|
||||
assert.equal(events.sendCalls, 3);
|
||||
assert.equal(events.completed, 0);
|
||||
});
|
||||
|
||||
test('step 6 can skip pre-login cleanup during step 7 recovery replay', async () => {
|
||||
const events = {
|
||||
cleanupCalls: 0,
|
||||
completedPayloads: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep6Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
events.completedPayloads.push({ step, payload });
|
||||
},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
isStep6RecoverableResult: () => false,
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
runPreStep6CookieCleanup: async () => {
|
||||
events.cleanupCalls += 1;
|
||||
},
|
||||
sendToContentScriptResilient: async () => ({
|
||||
step6Outcome: 'success',
|
||||
loginVerificationRequestedAt: 123,
|
||||
}),
|
||||
shouldSkipLoginVerificationForCpaCallback: () => false,
|
||||
skipLoginVerificationStepsForCpaCallback: async () => {},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep6(
|
||||
{ email: 'user@example.com', password: 'secret' },
|
||||
{ skipPreLoginCleanup: true }
|
||||
);
|
||||
|
||||
assert.equal(events.cleanupCalls, 0);
|
||||
assert.deepStrictEqual(events.completedPayloads, [
|
||||
{
|
||||
step: 6,
|
||||
payload: { loginVerificationRequestedAt: 123 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/steps/fetch-login-code.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
test('step 7 refreshes CPA oauth via step 6 replay before submitting verification code', async () => {
|
||||
const calls = {
|
||||
ensureReady: 0,
|
||||
executeStep6: [],
|
||||
sleep: [],
|
||||
resolveOptions: null,
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep7VerificationPageReady: async () => {
|
||||
calls.ensureReady += 1;
|
||||
return { state: 'verification_page' };
|
||||
},
|
||||
executeStep6: async (_state, options = {}) => {
|
||||
calls.executeStep6.push(options);
|
||||
},
|
||||
getMailConfig: () => ({
|
||||
provider: 'qq',
|
||||
label: 'QQ 邮箱',
|
||||
source: 'mail-qq',
|
||||
url: 'https://mail.qq.com',
|
||||
navigateOnReuse: false,
|
||||
}),
|
||||
getPanelMode: () => 'cpa',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
isVerificationMailPollingError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||
calls.resolveOptions = options;
|
||||
await options.beforeSubmit({ code: '654321' });
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
shouldSkipLoginVerificationForCpaCallback: () => false,
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
sleepWithStop: async (ms) => {
|
||||
calls.sleep.push(ms);
|
||||
},
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(typeof calls.resolveOptions.beforeSubmit, 'function');
|
||||
assert.equal(calls.ensureReady, 2);
|
||||
assert.deepStrictEqual(calls.executeStep6, [{ skipPreLoginCleanup: true }]);
|
||||
assert.deepStrictEqual(calls.sleep, [1200]);
|
||||
assert.equal(calls.resolveOptions.resendIntervalMs, 25000);
|
||||
});
|
||||
|
||||
test('step 7 disables resend interval for 2925 mailbox polling', async () => {
|
||||
let capturedOptions = null;
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep7VerificationPageReady: async () => ({ state: 'verification_page' }),
|
||||
executeStep6: async () => {},
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
label: '2925 邮箱',
|
||||
source: 'mail-2925',
|
||||
url: 'https://2925.com',
|
||||
navigateOnReuse: false,
|
||||
}),
|
||||
getPanelMode: () => 'sub2api',
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
isVerificationMailPollingError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||
capturedOptions = options;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
shouldSkipLoginVerificationForCpaCallback: () => false,
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
sleepWithStop: async () => {},
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
|
||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||
assert.equal(capturedOptions.beforeSubmit, undefined);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/verification-flow.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope);
|
||||
|
||||
test('verification flow extends 2925 polling window', () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: { tabs: { update: async () => {} } },
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToMailContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
const step4Payload = helpers.getVerificationPollPayload(4, { email: 'user@example.com', mailProvider: '2925' });
|
||||
const step7Payload = helpers.getVerificationPollPayload(7, { email: 'user@example.com', mailProvider: '2925' });
|
||||
|
||||
assert.equal(step4Payload.maxAttempts, 15);
|
||||
assert.equal(step4Payload.intervalMs, 15000);
|
||||
assert.equal(step7Payload.maxAttempts, 15);
|
||||
assert.equal(step7Payload.intervalMs, 15000);
|
||||
});
|
||||
|
||||
test('verification flow runs beforeSubmit hook before filling the code', async () => {
|
||||
const events = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async (_step, payload) => {
|
||||
events.push(['complete', payload.code]);
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async (_source, message) => {
|
||||
if (message.type === 'FILL_CODE') {
|
||||
events.push(['submit', message.payload.code]);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
sendToMailContentScriptResilient: async () => ({
|
||||
code: '654321',
|
||||
emailTimestamp: 123,
|
||||
}),
|
||||
setState: async (payload) => {
|
||||
events.push(['state', payload.lastLoginCode || payload.lastSignupCode]);
|
||||
},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
await helpers.resolveVerificationStep(
|
||||
7,
|
||||
{ email: 'user@example.com', lastLoginCode: null },
|
||||
{ provider: 'qq', label: 'QQ 邮箱' },
|
||||
{
|
||||
beforeSubmit: async (result) => {
|
||||
events.push(['beforeSubmit', result.code]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(events, [
|
||||
['beforeSubmit', '654321'],
|
||||
['submit', '654321'],
|
||||
['state', '654321'],
|
||||
['complete', '654321'],
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user