981 lines
30 KiB
JavaScript
981 lines
30 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
|
|
const source = fs.readFileSync('background.js', 'utf8');
|
|
const messageRouterSource = fs.readFileSync('background/message-router.js', 'utf8');
|
|
const messageRouterApi = new Function(
|
|
'self',
|
|
`${messageRouterSource}; return self.MultiPageBackgroundMessageRouter;`
|
|
)({});
|
|
|
|
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);
|
|
}
|
|
|
|
function createLuckmailPlatformVerifyRouter({
|
|
state,
|
|
stepKeyByStep = { 10: 'platform-verify' },
|
|
} = {}) {
|
|
let clearedOptions = null;
|
|
let usedMarker = null;
|
|
const logs = [];
|
|
const router = messageRouterApi.createMessageRouter({
|
|
addLog: async (message, level) => {
|
|
logs.push({ message, level });
|
|
},
|
|
buildLocalhostCleanupPrefix: () => '',
|
|
clearLuckmailRuntimeState: async (options) => {
|
|
clearedOptions = options;
|
|
},
|
|
closeLocalhostCallbackTabs: async () => {},
|
|
closeTabsByUrlPrefix: async () => {},
|
|
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
|
|
finalizePhoneActivationAfterSuccessfulFlow: async () => {},
|
|
getCurrentLuckmailPurchase: (latestState) => latestState.currentLuckmailPurchase,
|
|
getState: async () => state,
|
|
getStepDefinitionForState: (step) => ({ id: step, key: stepKeyByStep[step] || '' }),
|
|
isHotmailProvider: () => false,
|
|
isLocalhostOAuthCallbackUrl: () => true,
|
|
isLuckmailProvider: (latestState) => latestState.mailProvider === 'luckmail-api',
|
|
patchHotmailAccount: async () => {},
|
|
setLuckmailPurchaseUsedState: async (purchaseId, used) => {
|
|
usedMarker = { purchaseId, used };
|
|
},
|
|
});
|
|
return {
|
|
router,
|
|
snapshot() {
|
|
return { clearedOptions, usedMarker, logs };
|
|
},
|
|
};
|
|
}
|
|
|
|
test('ensureLuckmailPurchaseForFlow buys openai mailbox and defaults email type to ms_graph', async () => {
|
|
const bundle = [
|
|
extractFunction('getLuckmailSessionConfig'),
|
|
extractFunction('getCurrentLuckmailPurchase'),
|
|
extractFunction('ensureLuckmailPurchaseForFlow'),
|
|
].join('\n');
|
|
|
|
const factory = new Function('initialState', `
|
|
let currentState = { ...initialState };
|
|
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
|
|
const purchaseCalls = [];
|
|
const activateCalls = [];
|
|
|
|
function normalizeLuckmailBaseUrl(value) {
|
|
return String(value || '').trim() || 'https://mails.luckyous.com';
|
|
}
|
|
function normalizeLuckmailEmailType(value) {
|
|
return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
|
|
? String(value || '').trim()
|
|
: 'ms_graph';
|
|
}
|
|
function normalizeLuckmailPurchase(value) {
|
|
return value;
|
|
}
|
|
function normalizeLuckmailPurchases(value) {
|
|
return value.purchases || [];
|
|
}
|
|
async function getState() {
|
|
return currentState;
|
|
}
|
|
function createLuckmailClient() {
|
|
return {
|
|
user: {
|
|
async purchaseEmails(projectCode, quantity, options) {
|
|
purchaseCalls.push({ projectCode, quantity, options });
|
|
return {
|
|
purchases: [{ id: 15, email_address: 'demo@outlook.com', token: 'tok-1' }],
|
|
};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
async function findReusableLuckmailPurchaseForFlow() {
|
|
return null;
|
|
}
|
|
async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
|
|
activateCalls.push({ state, purchase, options });
|
|
currentState.currentLuckmailPurchase = purchase;
|
|
currentState.email = purchase.email_address;
|
|
return purchase;
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
ensureLuckmailPurchaseForFlow,
|
|
snapshot() {
|
|
return { currentState, purchaseCalls, activateCalls };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory({
|
|
luckmailApiKey: 'sk-test',
|
|
luckmailBaseUrl: '',
|
|
luckmailEmailType: '',
|
|
luckmailDomain: '',
|
|
currentLuckmailPurchase: null,
|
|
email: null,
|
|
});
|
|
|
|
const purchase = await api.ensureLuckmailPurchaseForFlow();
|
|
const snapshot = api.snapshot();
|
|
|
|
assert.equal(purchase.email_address, 'demo@outlook.com');
|
|
assert.deepStrictEqual(snapshot.purchaseCalls, [{
|
|
projectCode: 'openai',
|
|
quantity: 1,
|
|
options: {
|
|
emailType: 'ms_graph',
|
|
domain: undefined,
|
|
},
|
|
}]);
|
|
assert.equal(snapshot.activateCalls[0].options.initializeCursor, false);
|
|
assert.equal(snapshot.currentState.email, 'demo@outlook.com');
|
|
});
|
|
|
|
test('ensureLuckmailPurchaseForFlow reuses reusable openai mailbox before buying a new one', async () => {
|
|
const bundle = [
|
|
extractFunction('getLuckmailSessionConfig'),
|
|
extractFunction('getCurrentLuckmailPurchase'),
|
|
extractFunction('ensureLuckmailPurchaseForFlow'),
|
|
].join('\n');
|
|
|
|
const factory = new Function('initialState', `
|
|
let currentState = { ...initialState };
|
|
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
|
|
const purchaseCalls = [];
|
|
const activateCalls = [];
|
|
|
|
function normalizeLuckmailBaseUrl(value) {
|
|
return String(value || '').trim() || 'https://mails.luckyous.com';
|
|
}
|
|
function normalizeLuckmailEmailType(value) {
|
|
return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
|
|
? String(value || '').trim()
|
|
: 'ms_graph';
|
|
}
|
|
function normalizeLuckmailPurchase(value) {
|
|
return value;
|
|
}
|
|
function normalizeLuckmailPurchases(value) {
|
|
return value.purchases || [];
|
|
}
|
|
async function getState() {
|
|
return currentState;
|
|
}
|
|
function createLuckmailClient() {
|
|
return {
|
|
user: {
|
|
async purchaseEmails(projectCode, quantity, options) {
|
|
purchaseCalls.push({ projectCode, quantity, options });
|
|
return { purchases: [] };
|
|
},
|
|
},
|
|
};
|
|
}
|
|
async function findReusableLuckmailPurchaseForFlow() {
|
|
return {
|
|
id: 99,
|
|
email_address: 'reuse@outlook.com',
|
|
token: 'tok-reuse',
|
|
};
|
|
}
|
|
async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
|
|
activateCalls.push({ state, purchase, options });
|
|
currentState.currentLuckmailPurchase = purchase;
|
|
currentState.email = purchase.email_address;
|
|
return purchase;
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
ensureLuckmailPurchaseForFlow,
|
|
snapshot() {
|
|
return { currentState, purchaseCalls, activateCalls };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory({
|
|
luckmailApiKey: 'sk-test',
|
|
luckmailBaseUrl: 'https://mails.luckyous.com',
|
|
luckmailEmailType: 'ms_imap',
|
|
luckmailDomain: 'outlook.com',
|
|
currentLuckmailPurchase: null,
|
|
email: null,
|
|
});
|
|
|
|
const purchase = await api.ensureLuckmailPurchaseForFlow();
|
|
const snapshot = api.snapshot();
|
|
|
|
assert.equal(purchase.id, 99);
|
|
assert.deepStrictEqual(snapshot.purchaseCalls, []);
|
|
assert.equal(snapshot.activateCalls[0].options.initializeCursor, true);
|
|
assert.match(snapshot.activateCalls[0].options.logMessage, /已复用 openai 邮箱/);
|
|
});
|
|
|
|
test('activateLuckmailPurchaseForFlow builds baseline cursor from existing mails when reusing mailbox', async () => {
|
|
const bundle = extractFunction('activateLuckmailPurchaseForFlow');
|
|
|
|
const factory = new Function(`
|
|
let currentPurchase = null;
|
|
let currentCursor = null;
|
|
let currentEmail = null;
|
|
const buildCalls = [];
|
|
|
|
function normalizeLuckmailPurchase(value) {
|
|
return value;
|
|
}
|
|
async function setLuckmailPurchaseState(value) {
|
|
currentPurchase = value;
|
|
}
|
|
async function setLuckmailMailCursorState(value) {
|
|
currentCursor = value;
|
|
}
|
|
async function setEmailState(value) {
|
|
currentEmail = value;
|
|
}
|
|
async function addLog() {}
|
|
function buildLuckmailBaselineCursor(mails) {
|
|
buildCalls.push(mails);
|
|
return { messageId: 'mail-new', receivedAt: '2026-04-14 13:32:05' };
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
activateLuckmailPurchaseForFlow,
|
|
snapshot() {
|
|
return { currentPurchase, currentCursor, currentEmail, buildCalls };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory();
|
|
const client = {
|
|
user: {
|
|
async getTokenMails() {
|
|
return {
|
|
mails: [
|
|
{ message_id: 'mail-old', received_at: '2026-04-14 13:31:15' },
|
|
{ message_id: 'mail-new', received_at: '2026-04-14 13:32:05' },
|
|
],
|
|
};
|
|
},
|
|
},
|
|
};
|
|
|
|
await api.activateLuckmailPurchaseForFlow({}, client, {
|
|
id: 5,
|
|
email_address: 'reuse@outlook.com',
|
|
token: 'tok-reuse',
|
|
}, {
|
|
initializeCursor: true,
|
|
logMessage: 'reuse',
|
|
});
|
|
|
|
const snapshot = api.snapshot();
|
|
assert.equal(snapshot.currentPurchase.id, 5);
|
|
assert.deepStrictEqual(snapshot.currentCursor, {
|
|
messageId: 'mail-new',
|
|
receivedAt: '2026-04-14 13:32:05',
|
|
});
|
|
assert.equal(snapshot.currentEmail, 'reuse@outlook.com');
|
|
assert.equal(snapshot.buildCalls.length, 1);
|
|
});
|
|
|
|
test('pollLuckmailVerificationCode snapshots existing mails before accepting new LuckMail code', async () => {
|
|
const bundle = extractFunction('pollLuckmailVerificationCode');
|
|
|
|
const factory = new Function(`
|
|
let currentState = {
|
|
currentLuckmailPurchase: {
|
|
id: 7,
|
|
email_address: 'luck@example.com',
|
|
token: 'tok-luck',
|
|
},
|
|
currentLuckmailMailCursor: null,
|
|
};
|
|
const logs = [];
|
|
const cursorWrites = [];
|
|
let tokenCodeCalls = 0;
|
|
|
|
function getCurrentLuckmailPurchase(state) {
|
|
return state.currentLuckmailPurchase;
|
|
}
|
|
function createLuckmailClient() {
|
|
return {
|
|
user: {
|
|
async getTokenMails() {
|
|
if (tokenCodeCalls === 0) {
|
|
return {
|
|
mails: [
|
|
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
|
|
],
|
|
};
|
|
}
|
|
return {
|
|
mails: [
|
|
{ message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
|
|
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
|
|
],
|
|
};
|
|
},
|
|
async getTokenCode() {
|
|
tokenCodeCalls += 1;
|
|
return tokenCodeCalls === 1
|
|
? {
|
|
has_new_mail: true,
|
|
verification_code: '111111',
|
|
mail: { message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
|
|
}
|
|
: {
|
|
has_new_mail: true,
|
|
verification_code: '222222',
|
|
mail: { message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
|
|
};
|
|
},
|
|
async getTokenMailDetail(_token, messageId) {
|
|
return { message_id: messageId, received_at: '2026-04-14 13:32:05', verification_code: '222222' };
|
|
},
|
|
},
|
|
};
|
|
}
|
|
async function getState() {
|
|
return currentState;
|
|
}
|
|
async function setLuckmailMailCursorState(cursor) {
|
|
currentState = { ...currentState, currentLuckmailMailCursor: cursor };
|
|
cursorWrites.push(cursor);
|
|
}
|
|
function normalizeLuckmailMailCursor(cursor) {
|
|
return {
|
|
messageId: cursor?.messageId || cursor?.message_id || '',
|
|
receivedAt: cursor?.receivedAt || cursor?.received_at || '',
|
|
};
|
|
}
|
|
function normalizeLuckmailTimestamp(value) {
|
|
return Date.parse(String(value || '').replace(' ', 'T') + 'Z') || 0;
|
|
}
|
|
function buildLuckmailMailCursor(mail) {
|
|
return { messageId: mail.message_id || '', receivedAt: mail.received_at || '' };
|
|
}
|
|
function buildLuckmailBaselineCursor(mails) {
|
|
const latest = mails[0] || null;
|
|
return latest ? buildLuckmailMailCursor(latest) : null;
|
|
}
|
|
function isLuckmailMailNewerThanCursor(mail, cursor) {
|
|
if (!cursor?.messageId && !cursor?.receivedAt) return true;
|
|
if (mail.message_id === cursor.messageId) return false;
|
|
return normalizeLuckmailTimestamp(mail.received_at) > normalizeLuckmailTimestamp(cursor.receivedAt);
|
|
}
|
|
function pickLuckmailVerificationMail(mails, filters) {
|
|
const excludeCodes = new Set(filters.excludeCodes || []);
|
|
for (const mail of mails || []) {
|
|
if (!mail.verification_code || excludeCodes.has(mail.verification_code)) continue;
|
|
return { mail, code: mail.verification_code };
|
|
}
|
|
return null;
|
|
}
|
|
function normalizeLuckmailTokenCode(result) {
|
|
return result;
|
|
}
|
|
async function resolveLuckmailVerificationMail(client, token, filters, tokenCodeResult) {
|
|
if (tokenCodeResult?.mail) {
|
|
const inline = pickLuckmailVerificationMail([tokenCodeResult.mail], filters);
|
|
if (inline) return inline;
|
|
}
|
|
const mailList = await client.user.getTokenMails(token);
|
|
return pickLuckmailVerificationMail(mailList.mails, filters);
|
|
}
|
|
async function addLog(message, level) {
|
|
logs.push({ message, level });
|
|
}
|
|
function throwIfStopped() {}
|
|
function isStopError() {
|
|
return false;
|
|
}
|
|
async function sleepWithStop() {}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
pollLuckmailVerificationCode,
|
|
snapshot() {
|
|
return { currentState, cursorWrites, logs, tokenCodeCalls };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory();
|
|
const result = await api.pollLuckmailVerificationCode(4, await api.snapshot().currentState, {
|
|
maxAttempts: 2,
|
|
intervalMs: 1000,
|
|
senderFilters: ['openai'],
|
|
subjectFilters: ['code'],
|
|
excludeCodes: [],
|
|
});
|
|
|
|
assert.equal(result.code, '222222');
|
|
const snapshot = api.snapshot();
|
|
assert.deepStrictEqual(snapshot.cursorWrites[0], {
|
|
messageId: 'old-mail',
|
|
receivedAt: '2026-04-14 13:31:15',
|
|
});
|
|
assert.deepStrictEqual(snapshot.cursorWrites.at(-1), {
|
|
messageId: 'new-mail',
|
|
receivedAt: '2026-04-14 13:32:05',
|
|
});
|
|
assert.equal(snapshot.logs.some((entry) => /已保存当前邮箱旧邮件快照/.test(entry.message)), true);
|
|
assert.equal(snapshot.tokenCodeCalls, 2);
|
|
});
|
|
|
|
test('buildPersistentSettingsPayload keeps LuckMail config fields for storage.local persistence', () => {
|
|
const bundle = [
|
|
extractFunction('normalizePersistentSettingValue'),
|
|
extractFunction('buildPersistentSettingsPayload'),
|
|
].join('\n');
|
|
|
|
const factory = new Function(`
|
|
const DEFAULT_LUCKMAIL_BASE_URL = 'https://mails.luckyous.com';
|
|
const DEFAULT_LUCKMAIL_EMAIL_TYPE = 'ms_graph';
|
|
const PERSISTED_SETTING_DEFAULTS = {
|
|
luckmailApiKey: '',
|
|
luckmailBaseUrl: DEFAULT_LUCKMAIL_BASE_URL,
|
|
luckmailEmailType: DEFAULT_LUCKMAIL_EMAIL_TYPE,
|
|
luckmailDomain: '',
|
|
luckmailUsedPurchases: {},
|
|
luckmailPreserveTagId: 0,
|
|
luckmailPreserveTagName: '保留',
|
|
};
|
|
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
|
|
function normalizeLuckmailBaseUrl(value) {
|
|
const normalized = String(value || '').trim() || DEFAULT_LUCKMAIL_BASE_URL;
|
|
return normalized.replace(/\\/$/, '');
|
|
}
|
|
function normalizeLuckmailEmailType(value) {
|
|
return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
|
|
? String(value || '').trim()
|
|
: DEFAULT_LUCKMAIL_EMAIL_TYPE;
|
|
}
|
|
function normalizeLuckmailPurchaseId(value) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
|
|
}
|
|
function normalizeLuckmailUsedPurchases(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
return Object.entries(value).reduce((result, [key, used]) => {
|
|
const normalizedKey = normalizeLuckmailPurchaseId(key);
|
|
if (normalizedKey) result[normalizedKey] = Boolean(used);
|
|
return result;
|
|
}, {});
|
|
}
|
|
function resolveLegacyAutoStepDelaySeconds() {
|
|
return undefined;
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
buildPersistentSettingsPayload,
|
|
};
|
|
`);
|
|
|
|
const api = factory();
|
|
const payload = api.buildPersistentSettingsPayload({
|
|
luckmailApiKey: 'sk-live-demo',
|
|
luckmailBaseUrl: 'https://demo.example.com/',
|
|
luckmailEmailType: 'ms_imap',
|
|
luckmailDomain: ' outlook.com ',
|
|
});
|
|
|
|
assert.deepStrictEqual(payload, {
|
|
luckmailApiKey: 'sk-live-demo',
|
|
luckmailBaseUrl: 'https://demo.example.com',
|
|
luckmailEmailType: 'ms_imap',
|
|
luckmailDomain: 'outlook.com',
|
|
});
|
|
|
|
const statePayload = api.buildPersistentSettingsPayload({
|
|
luckmailUsedPurchases: { 88: true, 99: false, bad: true },
|
|
luckmailPreserveTagId: '9',
|
|
luckmailPreserveTagName: ' 保留邮箱 ',
|
|
});
|
|
assert.deepStrictEqual(statePayload, {
|
|
luckmailUsedPurchases: { 88: true, 99: false },
|
|
luckmailPreserveTagId: 9,
|
|
luckmailPreserveTagName: '保留邮箱',
|
|
});
|
|
});
|
|
|
|
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
|
|
const bundle = extractFunction('listLuckmailPurchasesByProject');
|
|
|
|
const factory = new Function(`
|
|
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
|
|
function normalizeLuckmailProjectName(value) {
|
|
return String(value || '').trim().toLowerCase();
|
|
}
|
|
function isLuckmailPurchaseForProject(purchase, projectCode) {
|
|
return normalizeLuckmailProjectName(purchase.project_name || purchase.project) === normalizeLuckmailProjectName(projectCode);
|
|
}
|
|
async function getAllLuckmailPurchases() {
|
|
return [
|
|
{ id: 1, project_name: 'OpenAi' },
|
|
{ id: 2, project_name: 'other' },
|
|
{ id: 3, project: 'openai' },
|
|
];
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return { listLuckmailPurchasesByProject };
|
|
`);
|
|
|
|
const api = factory();
|
|
const result = await api.listLuckmailPurchasesByProject({}, { projectCode: 'openai' });
|
|
assert.deepStrictEqual(result.map((item) => item.id), [1, 3]);
|
|
});
|
|
|
|
test('disableUsedLuckmailPurchases only disables locally used and non-preserved openai mailboxes', async () => {
|
|
const bundle = extractFunction('disableUsedLuckmailPurchases');
|
|
|
|
const factory = new Function(`
|
|
let clearedOptions = null;
|
|
const disabledCalls = [];
|
|
const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
|
|
|
|
function normalizeLuckmailPurchaseId(value) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
|
|
}
|
|
async function ensureManualInteractionAllowed() {
|
|
return {
|
|
luckmailUsedPurchases: { 1: true, 2: true, 3: true },
|
|
luckmailPreserveTagId: 9,
|
|
luckmailPreserveTagName: '保留',
|
|
mailProvider: 'luckmail-api',
|
|
};
|
|
}
|
|
function getLuckmailUsedPurchases(state) {
|
|
return state.luckmailUsedPurchases;
|
|
}
|
|
function getLuckmailPreserveTagInfo(state) {
|
|
return {
|
|
id: state.luckmailPreserveTagId,
|
|
name: state.luckmailPreserveTagName,
|
|
};
|
|
}
|
|
function isLuckmailPurchasePreserved(purchase, options) {
|
|
return purchase.tag_id === options.preserveTagId || purchase.tag_name === options.preserveTagName;
|
|
}
|
|
function createLuckmailClient() {
|
|
return {
|
|
user: {
|
|
async batchSetPurchaseDisabled(ids, disabled) {
|
|
disabledCalls.push({ ids, disabled });
|
|
},
|
|
},
|
|
};
|
|
}
|
|
async function listLuckmailPurchasesByProject() {
|
|
return [
|
|
{ id: 1, email_address: 'used-1@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
|
|
{ id: 2, email_address: 'preserved@outlook.com', user_disabled: 0, tag_id: 9, tag_name: '保留' },
|
|
{ id: 3, email_address: 'already-disabled@outlook.com', user_disabled: 1, tag_id: 0, tag_name: '' },
|
|
{ id: 4, email_address: 'unused@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
|
|
];
|
|
}
|
|
async function getState() {
|
|
return {
|
|
currentLuckmailPurchase: { id: 1 },
|
|
mailProvider: 'luckmail-api',
|
|
};
|
|
}
|
|
function getCurrentLuckmailPurchase(state) {
|
|
return state.currentLuckmailPurchase;
|
|
}
|
|
function isLuckmailProvider(state) {
|
|
return state.mailProvider === 'luckmail-api';
|
|
}
|
|
async function clearLuckmailRuntimeState(options) {
|
|
clearedOptions = options;
|
|
}
|
|
async function addLog() {}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
disableUsedLuckmailPurchases,
|
|
snapshot() {
|
|
return { disabledCalls, clearedOptions };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory();
|
|
const result = await api.disableUsedLuckmailPurchases();
|
|
const snapshot = api.snapshot();
|
|
|
|
assert.deepStrictEqual(result.disabledIds, [1]);
|
|
assert.deepStrictEqual(snapshot.disabledCalls, [{ ids: [1], disabled: 1 }]);
|
|
assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
|
|
});
|
|
|
|
test('resetState preserves LuckMail session config, used map, and preserve tag cache while clearing runtime purchase state', async () => {
|
|
const bundle = [
|
|
extractFunction('buildAccountContributionState'),
|
|
extractFunction('resetState'),
|
|
].join('\n');
|
|
|
|
const factory = new Function([
|
|
'let cleared = false;',
|
|
'let storedPayload = null;',
|
|
"const LOG_PREFIX = '[test]';",
|
|
"const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';",
|
|
'const DEFAULT_STATE = {',
|
|
" luckmailApiKey: '',",
|
|
" luckmailBaseUrl: 'https://mails.luckyous.com',",
|
|
" luckmailEmailType: 'ms_graph',",
|
|
" luckmailDomain: '',",
|
|
" yydsMailApiKey: '',",
|
|
" yydsMailBaseUrl: 'https://maliapi.215.im/v1',",
|
|
" panelMode: 'cpa',",
|
|
' luckmailUsedPurchases: {},',
|
|
' luckmailPreserveTagId: 0,',
|
|
" luckmailPreserveTagName: '保留',",
|
|
" currentLuckmailPurchase: { token: 'stale' },",
|
|
" currentLuckmailMailCursor: { messageId: 'stale' },",
|
|
" currentYydsMailInbox: { address: 'stale@example.com', token: 'stale-token' },",
|
|
' currentPhoneActivation: null,',
|
|
' reusablePhoneActivation: null,',
|
|
' email: null,',
|
|
'};',
|
|
'const CONTRIBUTION_RUNTIME_DEFAULTS = {',
|
|
' accountContributionEnabled: false,',
|
|
" contributionSessionId: '',",
|
|
" contributionAuthUrl: '',",
|
|
" contributionAuthState: '',",
|
|
" contributionCallbackUrl: '',",
|
|
" contributionStatus: '',",
|
|
" contributionStatusMessage: '',",
|
|
' contributionLastPollAt: 0,',
|
|
" contributionCallbackStatus: 'idle',",
|
|
" contributionCallbackMessage: '',",
|
|
' contributionAuthOpenedAt: 0,',
|
|
' contributionAuthTabId: 0,',
|
|
'};',
|
|
'const CONTRIBUTION_RUNTIME_KEYS = Object.keys(CONTRIBUTION_RUNTIME_DEFAULTS);',
|
|
'function normalizeLuckmailBaseUrl(value) {',
|
|
" const normalized = String(value || '').trim() || 'https://mails.luckyous.com';",
|
|
" return normalized.replace(/\\/$/, '');",
|
|
'}',
|
|
'function normalizeLuckmailEmailType(value) {',
|
|
" return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())",
|
|
" ? String(value || '').trim()",
|
|
" : 'ms_graph';",
|
|
'}',
|
|
'function normalizeLuckmailUsedPurchases(value) {',
|
|
' return value || {};',
|
|
'}',
|
|
'function normalizeYydsMailApiKey(value) {',
|
|
" return String(value || '').trim();",
|
|
'}',
|
|
'function normalizeYydsMailBaseUrl(value) {',
|
|
" return (String(value || '').trim() || 'https://maliapi.215.im/v1').replace(/\\/$/, '');",
|
|
'}',
|
|
'async function getPersistedSettings() {',
|
|
" return { mailProvider: '163' };",
|
|
'}',
|
|
'async function getPersistedAliasState() {',
|
|
' return {};',
|
|
'}',
|
|
'function buildStatePatchWithRuntimeState(_currentState, updates) {',
|
|
' return updates;',
|
|
'}',
|
|
'const chrome = {',
|
|
' storage: {',
|
|
' session: {',
|
|
' async get() {',
|
|
' return {',
|
|
" seenCodes: ['seen-1'],",
|
|
" seenInbucketMailIds: ['mail-1'],",
|
|
" accounts: [{ email: 'saved@example.com' }],",
|
|
" tabRegistry: { foo: { tabId: 1 } },",
|
|
" sourceLastUrls: { foo: 'https://example.com' },",
|
|
" reusablePhoneActivation: { activationId: 'rx-001', phoneNumber: '66951112222', provider: 'hero-sms', serviceCode: 'dr', countryId: 52, successfulUses: 1, maxUses: 3 },",
|
|
" luckmailApiKey: 'sk-session',",
|
|
" luckmailBaseUrl: 'https://demo.example.com/',",
|
|
" luckmailEmailType: 'ms_imap',",
|
|
" luckmailDomain: 'outlook.com',",
|
|
" luckmailUsedPurchases: { 88: true },",
|
|
' luckmailPreserveTagId: 9,',
|
|
" luckmailPreserveTagName: '保留',",
|
|
" yydsMailApiKey: 'AC-session',",
|
|
" yydsMailBaseUrl: 'https://maliapi.215.im/v1/',",
|
|
' };',
|
|
' },',
|
|
' async clear() {',
|
|
' cleared = true;',
|
|
' },',
|
|
' async set(payload) {',
|
|
' storedPayload = payload;',
|
|
' },',
|
|
' },',
|
|
' },',
|
|
'};',
|
|
bundle,
|
|
'return {',
|
|
' resetState,',
|
|
' snapshot() {',
|
|
' return { cleared, storedPayload };',
|
|
' },',
|
|
'};',
|
|
].join('\n'));
|
|
|
|
const api = factory();
|
|
await api.resetState();
|
|
const snapshot = api.snapshot();
|
|
|
|
assert.equal(snapshot.cleared, true);
|
|
assert.equal(snapshot.storedPayload.luckmailApiKey, 'sk-session');
|
|
assert.equal(snapshot.storedPayload.luckmailBaseUrl, 'https://demo.example.com');
|
|
assert.equal(snapshot.storedPayload.luckmailEmailType, 'ms_imap');
|
|
assert.equal(snapshot.storedPayload.luckmailDomain, 'outlook.com');
|
|
assert.deepStrictEqual(snapshot.storedPayload.luckmailUsedPurchases, { 88: true });
|
|
assert.equal(snapshot.storedPayload.luckmailPreserveTagId, 9);
|
|
assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
|
|
assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
|
|
assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
|
|
assert.equal(snapshot.storedPayload.yydsMailApiKey, 'AC-session');
|
|
assert.equal(snapshot.storedPayload.yydsMailBaseUrl, 'https://maliapi.215.im/v1');
|
|
assert.equal(snapshot.storedPayload.currentYydsMailInbox, null);
|
|
assert.deepStrictEqual(snapshot.storedPayload.reusablePhoneActivation, {
|
|
activationId: 'rx-001',
|
|
phoneNumber: '66951112222',
|
|
provider: 'hero-sms',
|
|
serviceCode: 'dr',
|
|
countryId: 52,
|
|
successfulUses: 1,
|
|
maxUses: 3,
|
|
});
|
|
assert.equal(snapshot.storedPayload.currentPhoneActivation, null);
|
|
});
|
|
|
|
test('message router platform verify marks current LuckMail purchase as used and clears runtime state', async () => {
|
|
const { router, snapshot } = createLuckmailPlatformVerifyRouter({
|
|
state: {
|
|
mailProvider: 'luckmail-api',
|
|
currentHotmailAccountId: null,
|
|
currentLuckmailPurchase: {
|
|
id: 123,
|
|
email_address: 'demo@outlook.com',
|
|
},
|
|
email: 'demo@outlook.com',
|
|
},
|
|
});
|
|
|
|
await router.handleStepData(10, {
|
|
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
|
});
|
|
|
|
const result = snapshot();
|
|
assert.deepStrictEqual(result.usedMarker, { purchaseId: 123, used: true });
|
|
assert.deepStrictEqual(result.clearedOptions, { clearEmail: true });
|
|
assert.equal(result.logs.at(-1).message, '当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。');
|
|
});
|
|
|
|
test('message router marks current LuckMail purchase as used on Plus platform verify step 13', async () => {
|
|
const { router, snapshot } = createLuckmailPlatformVerifyRouter({
|
|
state: {
|
|
plusModeEnabled: true,
|
|
mailProvider: 'luckmail-api',
|
|
currentHotmailAccountId: null,
|
|
currentLuckmailPurchase: {
|
|
id: 456,
|
|
email_address: 'plus@outlook.com',
|
|
},
|
|
email: 'plus@outlook.com',
|
|
},
|
|
stepKeyByStep: {
|
|
10: 'oauth-login',
|
|
13: 'platform-verify',
|
|
},
|
|
});
|
|
|
|
await router.handleStepData(10, {});
|
|
assert.equal(snapshot().usedMarker, null);
|
|
|
|
await router.handleStepData(13, {
|
|
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
|
});
|
|
|
|
const result = snapshot();
|
|
assert.deepStrictEqual(result.usedMarker, { purchaseId: 456, used: true });
|
|
assert.equal(result.logs.some((entry) => /已在本地标记为已用/.test(entry.message)), true);
|
|
});
|
|
|
|
test('setLuckmailPurchaseUsedState persists used map to storage.local so reload keeps it non-reusable', async () => {
|
|
const bundle = [
|
|
extractFunction('getLuckmailUsedPurchases'),
|
|
extractFunction('setLuckmailUsedPurchasesState'),
|
|
extractFunction('setLuckmailPurchaseUsedState'),
|
|
].join('\n');
|
|
|
|
const factory = new Function(`
|
|
let sessionState = {
|
|
luckmailUsedPurchases: { 7: true },
|
|
};
|
|
let persistentUpdates = [];
|
|
let sessionUpdates = [];
|
|
let broadcasts = [];
|
|
function normalizeLuckmailPurchaseId(value) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
|
|
}
|
|
function normalizeLuckmailUsedPurchases(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
return Object.entries(value).reduce((result, [key, used]) => {
|
|
const normalizedKey = normalizeLuckmailPurchaseId(key);
|
|
if (normalizedKey) result[normalizedKey] = Boolean(used);
|
|
return result;
|
|
}, {});
|
|
}
|
|
async function getState() {
|
|
return { ...sessionState };
|
|
}
|
|
async function setPersistentSettings(updates) {
|
|
persistentUpdates.push(updates);
|
|
}
|
|
async function setState(updates) {
|
|
sessionUpdates.push(updates);
|
|
sessionState = { ...sessionState, ...updates };
|
|
}
|
|
function broadcastDataUpdate(updates) {
|
|
broadcasts.push(updates);
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
setLuckmailPurchaseUsedState,
|
|
snapshot() {
|
|
return { sessionState, persistentUpdates, sessionUpdates, broadcasts };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory();
|
|
const result = await api.setLuckmailPurchaseUsedState(88, true);
|
|
const snapshot = api.snapshot();
|
|
|
|
assert.deepStrictEqual(result, { purchaseId: 88, used: true });
|
|
assert.deepStrictEqual(snapshot.sessionState.luckmailUsedPurchases, { 7: true, 88: true });
|
|
assert.deepStrictEqual(snapshot.persistentUpdates, [
|
|
{ luckmailUsedPurchases: { 7: true, 88: true } },
|
|
]);
|
|
assert.deepStrictEqual(snapshot.sessionUpdates, [
|
|
{ luckmailUsedPurchases: { 7: true, 88: true } },
|
|
]);
|
|
assert.deepStrictEqual(snapshot.broadcasts, [
|
|
{ luckmailUsedPurchases: { 7: true, 88: true } },
|
|
]);
|
|
});
|
|
|
|
test('setLuckmailPreserveTagInfo persists tag cache to storage.local', async () => {
|
|
const bundle = extractFunction('setLuckmailPreserveTagInfo');
|
|
|
|
const factory = new Function(`
|
|
let persistentUpdates = [];
|
|
let sessionUpdates = [];
|
|
let broadcasts = [];
|
|
const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';
|
|
function normalizeLuckmailTags(tags) {
|
|
return (Array.isArray(tags) ? tags : []).map((tag) => ({
|
|
id: Number(tag?.id) || 0,
|
|
name: String(tag?.name || '').trim(),
|
|
})).filter((tag) => tag.id > 0 || tag.name);
|
|
}
|
|
async function setPersistentSettings(updates) {
|
|
persistentUpdates.push(updates);
|
|
}
|
|
async function setState(updates) {
|
|
sessionUpdates.push(updates);
|
|
}
|
|
function broadcastDataUpdate(updates) {
|
|
broadcasts.push(updates);
|
|
}
|
|
|
|
${bundle}
|
|
|
|
return {
|
|
setLuckmailPreserveTagInfo,
|
|
snapshot() {
|
|
return { persistentUpdates, sessionUpdates, broadcasts };
|
|
},
|
|
};
|
|
`);
|
|
|
|
const api = factory();
|
|
const result = await api.setLuckmailPreserveTagInfo({ id: '12', name: ' 保留邮箱 ' });
|
|
const expected = {
|
|
luckmailPreserveTagId: 12,
|
|
luckmailPreserveTagName: '保留邮箱',
|
|
};
|
|
|
|
assert.deepStrictEqual(result, expected);
|
|
assert.deepStrictEqual(api.snapshot().persistentUpdates, [expected]);
|
|
assert.deepStrictEqual(api.snapshot().sessionUpdates, [expected]);
|
|
assert.deepStrictEqual(api.snapshot().broadcasts, [expected]);
|
|
});
|