hotmail-utils.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const {
  4. buildHotmailGraphMessagesUrl,
  5. extractVerificationCodeFromMessage,
  6. filterHotmailAccountsByUsage,
  7. extractVerificationCode,
  8. getLatestHotmailMessage,
  9. getHotmailBulkActionLabel,
  10. getHotmailListToggleLabel,
  11. getHotmailGraphRequestConfig,
  12. getHotmailVerificationPollConfig,
  13. getHotmailVerificationRequestTimestamp,
  14. normalizeHotmailMailboxId,
  15. normalizeHotmailMailApiMessages,
  16. parseHotmailImportText,
  17. pickHotmailAccountForRun,
  18. pickVerificationMessage,
  19. pickVerificationMessageWithFallback,
  20. pickVerificationMessageWithTimeFallback,
  21. shouldClearHotmailCurrentSelection,
  22. upsertHotmailAccountInList,
  23. } = require('../hotmail-utils.js');
  24. test('pickHotmailAccountForRun prefers authorized account with oldest lastUsedAt', () => {
  25. const now = Date.UTC(2026, 3, 10, 10, 0, 0);
  26. const accounts = [
  27. {
  28. id: 'recent',
  29. email: 'recent@hotmail.com',
  30. status: 'authorized',
  31. refreshToken: 'rt-recent',
  32. lastUsedAt: now - 1_000,
  33. },
  34. {
  35. id: 'oldest',
  36. email: 'oldest@hotmail.com',
  37. status: 'authorized',
  38. refreshToken: 'rt-oldest',
  39. lastUsedAt: now - 50_000,
  40. },
  41. {
  42. id: 'pending',
  43. email: 'pending@hotmail.com',
  44. status: 'pending',
  45. refreshToken: '',
  46. lastUsedAt: 0,
  47. },
  48. ];
  49. const selected = pickHotmailAccountForRun(accounts, { now });
  50. assert.equal(selected.id, 'oldest');
  51. });
  52. test('pickHotmailAccountForRun skips used accounts but ignores legacy enabled flag', () => {
  53. const accounts = [
  54. {
  55. id: 'disabled',
  56. email: 'disabled@hotmail.com',
  57. status: 'authorized',
  58. refreshToken: 'rt-disabled',
  59. enabled: false,
  60. used: false,
  61. lastUsedAt: 1,
  62. },
  63. {
  64. id: 'used',
  65. email: 'used@hotmail.com',
  66. status: 'authorized',
  67. refreshToken: 'rt-used',
  68. enabled: true,
  69. used: true,
  70. lastUsedAt: 0,
  71. },
  72. {
  73. id: 'available',
  74. email: 'available@hotmail.com',
  75. status: 'authorized',
  76. refreshToken: 'rt-available',
  77. enabled: true,
  78. used: false,
  79. lastUsedAt: 2,
  80. },
  81. ];
  82. const selected = pickHotmailAccountForRun(accounts, {});
  83. assert.equal(selected.id, 'disabled');
  84. });
  85. test('pickHotmailAccountForRun returns null for used-only pools', () => {
  86. assert.equal(
  87. pickHotmailAccountForRun([{
  88. id: 'used-only',
  89. email: 'used-only@hotmail.com',
  90. status: 'authorized',
  91. refreshToken: 'rt-used-only',
  92. used: true,
  93. lastUsedAt: 0,
  94. }], {}),
  95. null
  96. );
  97. });
  98. test('pickHotmailAccountForRun falls back to never-used authorized account first', () => {
  99. const accounts = [
  100. {
  101. id: 'used',
  102. email: 'used@hotmail.com',
  103. status: 'authorized',
  104. refreshToken: 'rt-used',
  105. lastUsedAt: Date.UTC(2026, 3, 10, 9, 0, 0),
  106. },
  107. {
  108. id: 'fresh',
  109. email: 'fresh@hotmail.com',
  110. status: 'authorized',
  111. refreshToken: 'rt-fresh',
  112. lastUsedAt: 0,
  113. },
  114. ];
  115. const selected = pickHotmailAccountForRun(accounts, { now: Date.UTC(2026, 3, 10, 10, 0, 0) });
  116. assert.equal(selected.id, 'fresh');
  117. });
  118. test('upsertHotmailAccountInList replaces matching account state by id', () => {
  119. const accounts = [
  120. {
  121. id: 'active',
  122. email: 'active@hotmail.com',
  123. status: 'authorized',
  124. used: false,
  125. },
  126. {
  127. id: 'other',
  128. email: 'other@hotmail.com',
  129. status: 'authorized',
  130. used: false,
  131. },
  132. ];
  133. const nextAccounts = upsertHotmailAccountInList(accounts, {
  134. id: 'active',
  135. email: 'active@hotmail.com',
  136. status: 'authorized',
  137. used: true,
  138. });
  139. assert.deepEqual(nextAccounts, [
  140. {
  141. id: 'active',
  142. email: 'active@hotmail.com',
  143. status: 'authorized',
  144. used: true,
  145. },
  146. {
  147. id: 'other',
  148. email: 'other@hotmail.com',
  149. status: 'authorized',
  150. used: false,
  151. },
  152. ]);
  153. });
  154. test('shouldClearHotmailCurrentSelection returns true only when account becomes used', () => {
  155. assert.equal(shouldClearHotmailCurrentSelection({
  156. id: 'used',
  157. used: true,
  158. }), true);
  159. assert.equal(shouldClearHotmailCurrentSelection({
  160. id: 'available',
  161. used: false,
  162. }), false);
  163. });
  164. test('extractVerificationCode returns first six-digit code from multilingual mail text', () => {
  165. assert.equal(extractVerificationCode('你的 ChatGPT 验证码为 370794,请勿泄露。'), '370794');
  166. assert.equal(extractVerificationCode('Your verification code is 654321.'), '654321');
  167. assert.equal(extractVerificationCode('No code here'), null);
  168. });
  169. test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
  170. assert.equal(
  171. extractVerificationCodeFromMessage({
  172. subject: '你的 ChatGPT 代码为 192742',
  173. bodyPreview: 'OpenAI 验证邮件',
  174. from: { emailAddress: { address: 'noreply@openai.com' } },
  175. }),
  176. '192742'
  177. );
  178. assert.equal(
  179. extractVerificationCodeFromMessage({
  180. subject: 'OpenAI security message',
  181. bodyPreview: 'Your verification code is 654321.',
  182. from: { emailAddress: { address: 'noreply@openai.com' } },
  183. }),
  184. '654321'
  185. );
  186. });
  187. test('getHotmailListToggleLabel reflects expanded state and account count', () => {
  188. assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
  189. assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7)');
  190. assert.equal(getHotmailListToggleLabel(true, 7), '收起列表(7)');
  191. });
  192. test('filterHotmailAccountsByUsage can pick only used accounts or return all accounts', () => {
  193. const accounts = [
  194. { id: 'used-1', email: 'used-1@hotmail.com', used: true },
  195. { id: 'fresh-1', email: 'fresh-1@hotmail.com', used: false },
  196. { id: 'used-2', email: 'used-2@hotmail.com', used: true },
  197. ];
  198. assert.deepEqual(
  199. filterHotmailAccountsByUsage(accounts, 'used').map((account) => account.id),
  200. ['used-1', 'used-2']
  201. );
  202. assert.deepEqual(
  203. filterHotmailAccountsByUsage(accounts, 'all').map((account) => account.id),
  204. ['used-1', 'fresh-1', 'used-2']
  205. );
  206. });
  207. test('getHotmailBulkActionLabel reflects action type and count', () => {
  208. assert.equal(getHotmailBulkActionLabel('used', 0), '清空已用');
  209. assert.equal(getHotmailBulkActionLabel('used', 3), '清空已用(3)');
  210. assert.equal(getHotmailBulkActionLabel('all', 5), '全部删除(5)');
  211. });
  212. test('getLatestHotmailMessage picks the newest received mail', () => {
  213. const latest = getLatestHotmailMessage([
  214. {
  215. id: 'older',
  216. subject: 'older',
  217. receivedDateTime: '2026-04-11T00:01:00.000Z',
  218. },
  219. {
  220. id: 'newest',
  221. subject: 'newest',
  222. receivedDateTime: '2026-04-11T00:05:00.000Z',
  223. },
  224. {
  225. id: 'middle',
  226. subject: 'middle',
  227. receivedDateTime: '2026-04-11T00:03:00.000Z',
  228. },
  229. ]);
  230. assert.equal(latest.id, 'newest');
  231. });
  232. test('pickVerificationMessage filters by time, sender, subject, and excluded codes', () => {
  233. const messages = [
  234. {
  235. id: 'old-mail',
  236. subject: 'Your code is 111111',
  237. from: { emailAddress: { address: 'noreply@openai.com' } },
  238. bodyPreview: '111111',
  239. receivedDateTime: '2026-04-10T09:00:00.000Z',
  240. },
  241. {
  242. id: 'wrong-sender',
  243. subject: 'Your code is 222222',
  244. from: { emailAddress: { address: 'noreply@example.com' } },
  245. bodyPreview: '222222',
  246. receivedDateTime: '2026-04-10T10:01:00.000Z',
  247. },
  248. {
  249. id: 'good-mail',
  250. subject: 'ChatGPT verification code 333333',
  251. from: { emailAddress: { address: 'noreply@openai.com' } },
  252. bodyPreview: 'Use 333333 to continue',
  253. receivedDateTime: '2026-04-10T10:02:00.000Z',
  254. },
  255. {
  256. id: 'excluded-mail',
  257. subject: 'ChatGPT verification code 444444',
  258. from: { emailAddress: { address: 'noreply@openai.com' } },
  259. bodyPreview: 'Use 444444 to continue',
  260. receivedDateTime: '2026-04-10T10:03:00.000Z',
  261. },
  262. ];
  263. const match = pickVerificationMessage(messages, {
  264. afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 0),
  265. senderFilters: ['openai', 'noreply'],
  266. subjectFilters: ['verification', 'code', 'chatgpt'],
  267. excludeCodes: ['444444'],
  268. });
  269. assert.equal(match.message.id, 'good-mail');
  270. assert.equal(match.code, '333333');
  271. });
  272. test('pickVerificationMessageWithFallback no longer matches arbitrary recent mails when filters miss', () => {
  273. const messages = [
  274. {
  275. id: 'login-mail',
  276. subject: 'Use this security code to continue 555666',
  277. from: { emailAddress: { address: 'account-security@openai.com' } },
  278. bodyPreview: 'Your one-time security code is 555666',
  279. receivedDateTime: '2026-04-10T10:05:00.000Z',
  280. },
  281. ];
  282. const result = pickVerificationMessageWithFallback(messages, {
  283. afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 0),
  284. senderFilters: ['noreply'],
  285. subjectFilters: ['verification'],
  286. excludeCodes: [],
  287. });
  288. assert.equal(result.match, null);
  289. assert.equal(result.usedRelaxedFilters, false);
  290. assert.equal(result.usedTimeFallback, false);
  291. });
  292. test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while keeping sender and subject filters', () => {
  293. const messages = [
  294. {
  295. id: 'slightly-old-mail',
  296. subject: '你的 ChatGPT 代码为 141735',
  297. from: { emailAddress: { address: 'unknown' } },
  298. bodyPreview: 'OpenAI logo ...',
  299. receivedDateTime: '2026-04-10T10:00:02.000Z',
  300. },
  301. ];
  302. const result = pickVerificationMessageWithTimeFallback(messages, {
  303. afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 10),
  304. senderFilters: ['openai', 'noreply'],
  305. subjectFilters: ['verify', 'verification', 'code'],
  306. excludeCodes: [],
  307. });
  308. assert.equal(result.match.message.id, 'slightly-old-mail');
  309. assert.equal(result.match.code, '141735');
  310. assert.equal(result.usedRelaxedFilters, false);
  311. assert.equal(result.usedTimeFallback, true);
  312. });
  313. test('buildHotmailGraphMessagesUrl targets the official Microsoft Graph mailbox endpoint', () => {
  314. const url = new URL(buildHotmailGraphMessagesUrl({
  315. mailbox: 'Junk',
  316. }));
  317. assert.equal(url.origin + url.pathname, 'https://graph.microsoft.com/v1.0/me/mailFolders/junkemail/messages');
  318. assert.equal(url.searchParams.get('$top'), '10');
  319. assert.equal(url.searchParams.get('$orderby'), 'receivedDateTime desc');
  320. assert.match(url.searchParams.get('$select'), /subject/);
  321. assert.match(url.searchParams.get('$select'), /receivedDateTime/);
  322. });
  323. test('normalizeHotmailMailboxId maps supported mailbox labels to Graph folder ids', () => {
  324. assert.equal(normalizeHotmailMailboxId('INBOX'), 'inbox');
  325. assert.equal(normalizeHotmailMailboxId('Junk'), 'junkemail');
  326. assert.equal(normalizeHotmailMailboxId('junkemail'), 'junkemail');
  327. });
  328. test('normalizeHotmailMailApiMessages maps third-party payload fields into verification message shape', () => {
  329. const messages = normalizeHotmailMailApiMessages([
  330. {
  331. id: 'mail-1',
  332. from: 'noreply@openai.com',
  333. subject: 'ChatGPT verification code',
  334. text: 'Use 135790 to continue',
  335. date: '2026-04-10T10:02:00.000Z',
  336. },
  337. {
  338. message_id: 'mail-2',
  339. sender_email: 'alerts@example.com',
  340. title: 'Ignored',
  341. body: 'No code here',
  342. received_at: '2026-04-10T10:03:00.000Z',
  343. },
  344. ]);
  345. assert.deepEqual(messages, [
  346. {
  347. id: 'mail-1',
  348. subject: 'ChatGPT verification code',
  349. from: { emailAddress: { address: 'noreply@openai.com' } },
  350. bodyPreview: 'Use 135790 to continue',
  351. receivedDateTime: '2026-04-10T10:02:00.000Z',
  352. },
  353. {
  354. id: 'mail-2',
  355. subject: 'Ignored',
  356. from: { emailAddress: { address: 'alerts@example.com' } },
  357. bodyPreview: 'No code here',
  358. receivedDateTime: '2026-04-10T10:03:00.000Z',
  359. },
  360. ]);
  361. });
  362. test('getHotmailVerificationPollConfig gives Hotmail a slower initial wait and longer polling window', () => {
  363. assert.deepEqual(getHotmailVerificationPollConfig(4), {
  364. initialDelayMs: 5000,
  365. maxAttempts: 12,
  366. intervalMs: 5000,
  367. requestFreshCodeFirst: false,
  368. ignorePersistedLastCode: true,
  369. });
  370. assert.deepEqual(getHotmailVerificationPollConfig(7), {
  371. initialDelayMs: 5000,
  372. maxAttempts: 12,
  373. intervalMs: 5000,
  374. requestFreshCodeFirst: false,
  375. ignorePersistedLastCode: true,
  376. });
  377. });
  378. test('getHotmailVerificationRequestTimestamp prefers actual request timestamps with a safety buffer', () => {
  379. const signupRequestedAt = Date.UTC(2026, 3, 10, 12, 0, 30);
  380. const loginRequestedAt = Date.UTC(2026, 3, 10, 12, 5, 45);
  381. assert.equal(
  382. getHotmailVerificationRequestTimestamp(4, {
  383. signupVerificationRequestedAt: signupRequestedAt,
  384. flowStartTime: signupRequestedAt - 60_000,
  385. }),
  386. signupRequestedAt - 15_000
  387. );
  388. assert.equal(
  389. getHotmailVerificationRequestTimestamp(7, {
  390. loginVerificationRequestedAt: loginRequestedAt,
  391. lastEmailTimestamp: loginRequestedAt - 120_000,
  392. flowStartTime: loginRequestedAt - 300_000,
  393. }),
  394. loginRequestedAt - 15_000
  395. );
  396. });
  397. test('getHotmailGraphRequestConfig defines Microsoft Graph request defaults', () => {
  398. assert.deepEqual(getHotmailGraphRequestConfig(), {
  399. timeoutMs: 15000,
  400. pageSize: 10,
  401. scopes: [
  402. 'offline_access',
  403. 'https://graph.microsoft.com/Mail.Read',
  404. 'https://graph.microsoft.com/User.Read',
  405. ],
  406. tokenUrl: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token',
  407. messageFields: [
  408. 'id',
  409. 'internetMessageId',
  410. 'subject',
  411. 'from',
  412. 'bodyPreview',
  413. 'receivedDateTime',
  414. ],
  415. });
  416. });
  417. test('parseHotmailImportText parses account lines in email----password----clientId----token format', () => {
  418. const parsed = parseHotmailImportText(`
  419. 账号----密码----ID----Token
  420. JohnRodriguez5425@hotmail.com----nb4ta1OK----9e5f94bc-e8a4-4e73-b8be-63364c29d753----refresh-token-1
  421. alice@hotmail.com----pass-2----client-2----refresh-token-2
  422. `.trim());
  423. assert.deepEqual(parsed, [
  424. {
  425. email: 'JohnRodriguez5425@hotmail.com',
  426. password: 'nb4ta1OK',
  427. clientId: '9e5f94bc-e8a4-4e73-b8be-63364c29d753',
  428. refreshToken: 'refresh-token-1',
  429. },
  430. {
  431. email: 'alice@hotmail.com',
  432. password: 'pass-2',
  433. clientId: 'client-2',
  434. refreshToken: 'refresh-token-2',
  435. },
  436. ]);
  437. });