hotmail-utils.test.js 14 KB

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