background-luckmail.test.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const source = fs.readFileSync('background.js', 'utf8');
  5. function extractFunction(name) {
  6. const markers = [`async function ${name}(`, `function ${name}(`];
  7. const start = markers
  8. .map((marker) => source.indexOf(marker))
  9. .find((index) => index >= 0);
  10. if (start < 0) {
  11. throw new Error(`missing function ${name}`);
  12. }
  13. let parenDepth = 0;
  14. let signatureEnded = false;
  15. let braceStart = -1;
  16. for (let i = start; i < source.length; i += 1) {
  17. const ch = source[i];
  18. if (ch === '(') {
  19. parenDepth += 1;
  20. } else if (ch === ')') {
  21. parenDepth -= 1;
  22. if (parenDepth === 0) {
  23. signatureEnded = true;
  24. }
  25. } else if (ch === '{' && signatureEnded) {
  26. braceStart = i;
  27. break;
  28. }
  29. }
  30. if (braceStart < 0) {
  31. throw new Error(`missing body for function ${name}`);
  32. }
  33. let depth = 0;
  34. let end = braceStart;
  35. for (; end < source.length; end += 1) {
  36. const ch = source[end];
  37. if (ch === '{') depth += 1;
  38. if (ch === '}') {
  39. depth -= 1;
  40. if (depth === 0) {
  41. end += 1;
  42. break;
  43. }
  44. }
  45. }
  46. return source.slice(start, end);
  47. }
  48. test('ensureLuckmailPurchaseForFlow buys openai mailbox and defaults email type to ms_graph', async () => {
  49. const bundle = [
  50. extractFunction('getLuckmailSessionConfig'),
  51. extractFunction('getCurrentLuckmailPurchase'),
  52. extractFunction('ensureLuckmailPurchaseForFlow'),
  53. ].join('\n');
  54. const factory = new Function('initialState', `
  55. let currentState = { ...initialState };
  56. const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
  57. const purchaseCalls = [];
  58. const activateCalls = [];
  59. function normalizeLuckmailBaseUrl(value) {
  60. return String(value || '').trim() || 'https://mails.luckyous.com';
  61. }
  62. function normalizeLuckmailEmailType(value) {
  63. return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
  64. ? String(value || '').trim()
  65. : 'ms_graph';
  66. }
  67. function normalizeLuckmailPurchase(value) {
  68. return value;
  69. }
  70. function normalizeLuckmailPurchases(value) {
  71. return value.purchases || [];
  72. }
  73. async function getState() {
  74. return currentState;
  75. }
  76. function createLuckmailClient() {
  77. return {
  78. user: {
  79. async purchaseEmails(projectCode, quantity, options) {
  80. purchaseCalls.push({ projectCode, quantity, options });
  81. return {
  82. purchases: [{ id: 15, email_address: 'demo@outlook.com', token: 'tok-1' }],
  83. };
  84. },
  85. },
  86. };
  87. }
  88. async function findReusableLuckmailPurchaseForFlow() {
  89. return null;
  90. }
  91. async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
  92. activateCalls.push({ state, purchase, options });
  93. currentState.currentLuckmailPurchase = purchase;
  94. currentState.email = purchase.email_address;
  95. return purchase;
  96. }
  97. ${bundle}
  98. return {
  99. ensureLuckmailPurchaseForFlow,
  100. snapshot() {
  101. return { currentState, purchaseCalls, activateCalls };
  102. },
  103. };
  104. `);
  105. const api = factory({
  106. luckmailApiKey: 'sk-test',
  107. luckmailBaseUrl: '',
  108. luckmailEmailType: '',
  109. luckmailDomain: '',
  110. currentLuckmailPurchase: null,
  111. email: null,
  112. });
  113. const purchase = await api.ensureLuckmailPurchaseForFlow();
  114. const snapshot = api.snapshot();
  115. assert.equal(purchase.email_address, 'demo@outlook.com');
  116. assert.deepStrictEqual(snapshot.purchaseCalls, [{
  117. projectCode: 'openai',
  118. quantity: 1,
  119. options: {
  120. emailType: 'ms_graph',
  121. domain: undefined,
  122. },
  123. }]);
  124. assert.equal(snapshot.activateCalls[0].options.initializeCursor, false);
  125. assert.equal(snapshot.currentState.email, 'demo@outlook.com');
  126. });
  127. test('ensureLuckmailPurchaseForFlow reuses reusable openai mailbox before buying a new one', async () => {
  128. const bundle = [
  129. extractFunction('getLuckmailSessionConfig'),
  130. extractFunction('getCurrentLuckmailPurchase'),
  131. extractFunction('ensureLuckmailPurchaseForFlow'),
  132. ].join('\n');
  133. const factory = new Function('initialState', `
  134. let currentState = { ...initialState };
  135. const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
  136. const purchaseCalls = [];
  137. const activateCalls = [];
  138. function normalizeLuckmailBaseUrl(value) {
  139. return String(value || '').trim() || 'https://mails.luckyous.com';
  140. }
  141. function normalizeLuckmailEmailType(value) {
  142. return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())
  143. ? String(value || '').trim()
  144. : 'ms_graph';
  145. }
  146. function normalizeLuckmailPurchase(value) {
  147. return value;
  148. }
  149. function normalizeLuckmailPurchases(value) {
  150. return value.purchases || [];
  151. }
  152. async function getState() {
  153. return currentState;
  154. }
  155. function createLuckmailClient() {
  156. return {
  157. user: {
  158. async purchaseEmails(projectCode, quantity, options) {
  159. purchaseCalls.push({ projectCode, quantity, options });
  160. return { purchases: [] };
  161. },
  162. },
  163. };
  164. }
  165. async function findReusableLuckmailPurchaseForFlow() {
  166. return {
  167. id: 99,
  168. email_address: 'reuse@outlook.com',
  169. token: 'tok-reuse',
  170. };
  171. }
  172. async function activateLuckmailPurchaseForFlow(state, client, purchase, options) {
  173. activateCalls.push({ state, purchase, options });
  174. currentState.currentLuckmailPurchase = purchase;
  175. currentState.email = purchase.email_address;
  176. return purchase;
  177. }
  178. ${bundle}
  179. return {
  180. ensureLuckmailPurchaseForFlow,
  181. snapshot() {
  182. return { currentState, purchaseCalls, activateCalls };
  183. },
  184. };
  185. `);
  186. const api = factory({
  187. luckmailApiKey: 'sk-test',
  188. luckmailBaseUrl: 'https://mails.luckyous.com',
  189. luckmailEmailType: 'ms_imap',
  190. luckmailDomain: 'outlook.com',
  191. currentLuckmailPurchase: null,
  192. email: null,
  193. });
  194. const purchase = await api.ensureLuckmailPurchaseForFlow();
  195. const snapshot = api.snapshot();
  196. assert.equal(purchase.id, 99);
  197. assert.deepStrictEqual(snapshot.purchaseCalls, []);
  198. assert.equal(snapshot.activateCalls[0].options.initializeCursor, true);
  199. assert.match(snapshot.activateCalls[0].options.logMessage, /已复用 openai 邮箱/);
  200. });
  201. test('activateLuckmailPurchaseForFlow builds baseline cursor from existing mails when reusing mailbox', async () => {
  202. const bundle = extractFunction('activateLuckmailPurchaseForFlow');
  203. const factory = new Function(`
  204. let currentPurchase = null;
  205. let currentCursor = null;
  206. let currentEmail = null;
  207. const buildCalls = [];
  208. function normalizeLuckmailPurchase(value) {
  209. return value;
  210. }
  211. async function setLuckmailPurchaseState(value) {
  212. currentPurchase = value;
  213. }
  214. async function setLuckmailMailCursorState(value) {
  215. currentCursor = value;
  216. }
  217. async function setEmailState(value) {
  218. currentEmail = value;
  219. }
  220. async function addLog() {}
  221. function buildLuckmailBaselineCursor(mails) {
  222. buildCalls.push(mails);
  223. return { messageId: 'mail-new', receivedAt: '2026-04-14 13:32:05' };
  224. }
  225. ${bundle}
  226. return {
  227. activateLuckmailPurchaseForFlow,
  228. snapshot() {
  229. return { currentPurchase, currentCursor, currentEmail, buildCalls };
  230. },
  231. };
  232. `);
  233. const api = factory();
  234. const client = {
  235. user: {
  236. async getTokenMails() {
  237. return {
  238. mails: [
  239. { message_id: 'mail-old', received_at: '2026-04-14 13:31:15' },
  240. { message_id: 'mail-new', received_at: '2026-04-14 13:32:05' },
  241. ],
  242. };
  243. },
  244. },
  245. };
  246. await api.activateLuckmailPurchaseForFlow({}, client, {
  247. id: 5,
  248. email_address: 'reuse@outlook.com',
  249. token: 'tok-reuse',
  250. }, {
  251. initializeCursor: true,
  252. logMessage: 'reuse',
  253. });
  254. const snapshot = api.snapshot();
  255. assert.equal(snapshot.currentPurchase.id, 5);
  256. assert.deepStrictEqual(snapshot.currentCursor, {
  257. messageId: 'mail-new',
  258. receivedAt: '2026-04-14 13:32:05',
  259. });
  260. assert.equal(snapshot.currentEmail, 'reuse@outlook.com');
  261. assert.equal(snapshot.buildCalls.length, 1);
  262. });
  263. test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
  264. const bundle = extractFunction('listLuckmailPurchasesByProject');
  265. const factory = new Function(`
  266. const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
  267. function normalizeLuckmailProjectName(value) {
  268. return String(value || '').trim().toLowerCase();
  269. }
  270. function isLuckmailPurchaseForProject(purchase, projectCode) {
  271. return normalizeLuckmailProjectName(purchase.project_name || purchase.project) === normalizeLuckmailProjectName(projectCode);
  272. }
  273. async function getAllLuckmailPurchases() {
  274. return [
  275. { id: 1, project_name: 'OpenAi' },
  276. { id: 2, project_name: 'other' },
  277. { id: 3, project: 'openai' },
  278. ];
  279. }
  280. ${bundle}
  281. return { listLuckmailPurchasesByProject };
  282. `);
  283. const api = factory();
  284. const result = await api.listLuckmailPurchasesByProject({}, { projectCode: 'openai' });
  285. assert.deepStrictEqual(result.map((item) => item.id), [1, 3]);
  286. });
  287. test('disableUsedLuckmailPurchases only disables locally used and non-preserved openai mailboxes', async () => {
  288. const bundle = extractFunction('disableUsedLuckmailPurchases');
  289. const factory = new Function(`
  290. let clearedOptions = null;
  291. const disabledCalls = [];
  292. const DEFAULT_LUCKMAIL_PROJECT_CODE = 'openai';
  293. function normalizeLuckmailPurchaseId(value) {
  294. const numeric = Number(value);
  295. return Number.isFinite(numeric) && numeric > 0 ? String(Math.floor(numeric)) : '';
  296. }
  297. async function ensureManualInteractionAllowed() {
  298. return {
  299. luckmailUsedPurchases: { 1: true, 2: true, 3: true },
  300. luckmailPreserveTagId: 9,
  301. luckmailPreserveTagName: '保留',
  302. mailProvider: 'luckmail-api',
  303. };
  304. }
  305. function getLuckmailUsedPurchases(state) {
  306. return state.luckmailUsedPurchases;
  307. }
  308. function getLuckmailPreserveTagInfo(state) {
  309. return {
  310. id: state.luckmailPreserveTagId,
  311. name: state.luckmailPreserveTagName,
  312. };
  313. }
  314. function isLuckmailPurchasePreserved(purchase, options) {
  315. return purchase.tag_id === options.preserveTagId || purchase.tag_name === options.preserveTagName;
  316. }
  317. function createLuckmailClient() {
  318. return {
  319. user: {
  320. async batchSetPurchaseDisabled(ids, disabled) {
  321. disabledCalls.push({ ids, disabled });
  322. },
  323. },
  324. };
  325. }
  326. async function listLuckmailPurchasesByProject() {
  327. return [
  328. { id: 1, email_address: 'used-1@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
  329. { id: 2, email_address: 'preserved@outlook.com', user_disabled: 0, tag_id: 9, tag_name: '保留' },
  330. { id: 3, email_address: 'already-disabled@outlook.com', user_disabled: 1, tag_id: 0, tag_name: '' },
  331. { id: 4, email_address: 'unused@outlook.com', user_disabled: 0, tag_id: 0, tag_name: '' },
  332. ];
  333. }
  334. async function getState() {
  335. return {
  336. currentLuckmailPurchase: { id: 1 },
  337. mailProvider: 'luckmail-api',
  338. };
  339. }
  340. function getCurrentLuckmailPurchase(state) {
  341. return state.currentLuckmailPurchase;
  342. }
  343. function isLuckmailProvider(state) {
  344. return state.mailProvider === 'luckmail-api';
  345. }
  346. async function clearLuckmailRuntimeState(options) {
  347. clearedOptions = options;
  348. }
  349. async function addLog() {}
  350. ${bundle}
  351. return {
  352. disableUsedLuckmailPurchases,
  353. snapshot() {
  354. return { disabledCalls, clearedOptions };
  355. },
  356. };
  357. `);
  358. const api = factory();
  359. const result = await api.disableUsedLuckmailPurchases();
  360. const snapshot = api.snapshot();
  361. assert.deepStrictEqual(result.disabledIds, [1]);
  362. assert.deepStrictEqual(snapshot.disabledCalls, [{ ids: [1], disabled: 1 }]);
  363. assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
  364. });
  365. test('resetState preserves LuckMail session config, used map, and preserve tag cache while clearing runtime purchase state', async () => {
  366. const bundle = extractFunction('resetState');
  367. const factory = new Function([
  368. 'let cleared = false;',
  369. 'let storedPayload = null;',
  370. "const LOG_PREFIX = '[test]';",
  371. "const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = '保留';",
  372. 'const DEFAULT_STATE = {',
  373. " luckmailApiKey: '',",
  374. " luckmailBaseUrl: 'https://mails.luckyous.com',",
  375. " luckmailEmailType: 'ms_graph',",
  376. " luckmailDomain: '',",
  377. ' luckmailUsedPurchases: {},',
  378. ' luckmailPreserveTagId: 0,',
  379. " luckmailPreserveTagName: '保留',",
  380. " currentLuckmailPurchase: { token: 'stale' },",
  381. " currentLuckmailMailCursor: { messageId: 'stale' },",
  382. ' email: null,',
  383. '};',
  384. 'function normalizeLuckmailBaseUrl(value) {',
  385. " const normalized = String(value || '').trim() || 'https://mails.luckyous.com';",
  386. " return normalized.replace(/\\/$/, '');",
  387. '}',
  388. 'function normalizeLuckmailEmailType(value) {',
  389. " return ['self_built', 'ms_imap', 'ms_graph', 'google_variant'].includes(String(value || '').trim())",
  390. " ? String(value || '').trim()",
  391. " : 'ms_graph';",
  392. '}',
  393. 'function normalizeLuckmailUsedPurchases(value) {',
  394. ' return value || {};',
  395. '}',
  396. 'async function getPersistedSettings() {',
  397. " return { mailProvider: '163' };",
  398. '}',
  399. 'async function getPersistedAliasState() {',
  400. ' return {};',
  401. '}',
  402. 'const chrome = {',
  403. ' storage: {',
  404. ' session: {',
  405. ' async get() {',
  406. ' return {',
  407. " seenCodes: ['seen-1'],",
  408. " seenInbucketMailIds: ['mail-1'],",
  409. " accounts: [{ email: 'saved@example.com' }],",
  410. " tabRegistry: { foo: { tabId: 1 } },",
  411. " sourceLastUrls: { foo: 'https://example.com' },",
  412. " luckmailApiKey: 'sk-session',",
  413. " luckmailBaseUrl: 'https://demo.example.com/',",
  414. " luckmailEmailType: 'ms_imap',",
  415. " luckmailDomain: 'outlook.com',",
  416. " luckmailUsedPurchases: { 88: true },",
  417. ' luckmailPreserveTagId: 9,',
  418. " luckmailPreserveTagName: '保留',",
  419. ' };',
  420. ' },',
  421. ' async clear() {',
  422. ' cleared = true;',
  423. ' },',
  424. ' async set(payload) {',
  425. ' storedPayload = payload;',
  426. ' },',
  427. ' },',
  428. ' },',
  429. '};',
  430. bundle,
  431. 'return {',
  432. ' resetState,',
  433. ' snapshot() {',
  434. ' return { cleared, storedPayload };',
  435. ' },',
  436. '};',
  437. ].join('\n'));
  438. const api = factory();
  439. await api.resetState();
  440. const snapshot = api.snapshot();
  441. assert.equal(snapshot.cleared, true);
  442. assert.equal(snapshot.storedPayload.luckmailApiKey, 'sk-session');
  443. assert.equal(snapshot.storedPayload.luckmailBaseUrl, 'https://demo.example.com');
  444. assert.equal(snapshot.storedPayload.luckmailEmailType, 'ms_imap');
  445. assert.equal(snapshot.storedPayload.luckmailDomain, 'outlook.com');
  446. assert.deepStrictEqual(snapshot.storedPayload.luckmailUsedPurchases, { 88: true });
  447. assert.equal(snapshot.storedPayload.luckmailPreserveTagId, 9);
  448. assert.equal(snapshot.storedPayload.luckmailPreserveTagName, '保留');
  449. assert.equal(snapshot.storedPayload.currentLuckmailPurchase, null);
  450. assert.equal(snapshot.storedPayload.currentLuckmailMailCursor, null);
  451. });
  452. test('handleStepData step 10 marks current LuckMail purchase as used and clears runtime state', async () => {
  453. const bundle = extractFunction('handleStepData');
  454. const factory = new Function(`
  455. let clearedOptions = null;
  456. let usedMarker = null;
  457. const logs = [];
  458. async function closeLocalhostCallbackTabs() {}
  459. async function getState() {
  460. return {
  461. mailProvider: 'luckmail-api',
  462. currentHotmailAccountId: null,
  463. currentLuckmailPurchase: {
  464. id: 123,
  465. email_address: 'demo@outlook.com',
  466. },
  467. email: 'demo@outlook.com',
  468. };
  469. }
  470. function getCurrentLuckmailPurchase(state) {
  471. return state.currentLuckmailPurchase;
  472. }
  473. function isHotmailProvider() {
  474. return false;
  475. }
  476. async function patchHotmailAccount() {}
  477. function isLuckmailProvider(state) {
  478. return state.mailProvider === 'luckmail-api';
  479. }
  480. async function setLuckmailPurchaseUsedState(purchaseId, used) {
  481. usedMarker = { purchaseId, used };
  482. }
  483. async function clearLuckmailRuntimeState(options) {
  484. clearedOptions = options;
  485. }
  486. async function addLog(message, level) {
  487. logs.push({ message, level });
  488. }
  489. function buildLocalhostCleanupPrefix() {
  490. return '';
  491. }
  492. async function closeTabsByUrlPrefix() {}
  493. function shouldUseCustomRegistrationEmail() {
  494. return false;
  495. }
  496. async function setEmailStateSilently() {}
  497. async function setState() {}
  498. function broadcastDataUpdate() {}
  499. function isLocalhostOAuthCallbackUrl() {
  500. return true;
  501. }
  502. async function finalizeIcloudAliasAfterSuccessfulFlow() {}
  503. ${bundle}
  504. return {
  505. handleStepData,
  506. snapshot() {
  507. return { clearedOptions, usedMarker, logs };
  508. },
  509. };
  510. `);
  511. const api = factory();
  512. await api.handleStepData(10, {
  513. localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
  514. });
  515. const snapshot = api.snapshot();
  516. assert.deepStrictEqual(snapshot.usedMarker, { purchaseId: 123, used: true });
  517. assert.deepStrictEqual(snapshot.clearedOptions, { clearEmail: true });
  518. assert.equal(snapshot.logs.at(-1).message, '当前 LuckMail 邮箱运行态已清空,下轮将优先复用未用邮箱或重新购买邮箱。');
  519. });