hotmail-utils.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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 returns relaxed match when strict filters miss but recent code exists', () => {
  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.message.id, 'login-mail');
  288. assert.equal(result.match.code, '555666');
  289. assert.equal(result.usedRelaxedFilters, true);
  290. assert.equal(result.usedTimeFallback, false);
  291. });
  292. test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp when latest code mail is just outside flow window', () => {
  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, true);
  311. assert.equal(result.usedTimeFallback, true);
  312. });
  313. test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
  314. const url = new URL(buildHotmailMailApiLatestUrl({
  315. clientId: 'client-123',
  316. email: 'user@hotmail.com',
  317. refreshToken: 'refresh-token-xyz',
  318. mailbox: 'Junk',
  319. }));
  320. assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
  321. assert.equal(url.searchParams.get('client_id'), 'client-123');
  322. assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
  323. assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
  324. assert.equal(url.searchParams.get('mailbox'), 'Junk');
  325. assert.equal(url.searchParams.get('response_type'), 'json');
  326. });
  327. test('normalizeHotmailMailApiMessages maps third-party payload fields into verification message shape', () => {
  328. const messages = normalizeHotmailMailApiMessages([
  329. {
  330. id: 'mail-1',
  331. from: 'noreply@openai.com',
  332. subject: 'ChatGPT verification code',
  333. text: 'Use 135790 to continue',
  334. date: '2026-04-10T10:02:00.000Z',
  335. },
  336. {
  337. message_id: 'mail-2',
  338. sender_email: 'alerts@example.com',
  339. title: 'Ignored',
  340. body: 'No code here',
  341. received_at: '2026-04-10T10:03:00.000Z',
  342. },
  343. ]);
  344. assert.deepEqual(messages, [
  345. {
  346. id: 'mail-1',
  347. subject: 'ChatGPT verification code',
  348. from: { emailAddress: { address: 'noreply@openai.com' } },
  349. bodyPreview: 'Use 135790 to continue',
  350. receivedDateTime: '2026-04-10T10:02:00.000Z',
  351. },
  352. {
  353. id: 'mail-2',
  354. subject: 'Ignored',
  355. from: { emailAddress: { address: 'alerts@example.com' } },
  356. bodyPreview: 'No code here',
  357. receivedDateTime: '2026-04-10T10:03:00.000Z',
  358. },
  359. ]);
  360. });
  361. test('getHotmailVerificationPollConfig gives Hotmail a slower initial wait and longer polling window', () => {
  362. assert.deepEqual(getHotmailVerificationPollConfig(4), {
  363. initialDelayMs: 5000,
  364. maxAttempts: 12,
  365. intervalMs: 5000,
  366. requestFreshCodeFirst: false,
  367. ignorePersistedLastCode: true,
  368. });
  369. assert.deepEqual(getHotmailVerificationPollConfig(7), {
  370. initialDelayMs: 5000,
  371. maxAttempts: 12,
  372. intervalMs: 5000,
  373. requestFreshCodeFirst: false,
  374. ignorePersistedLastCode: true,
  375. });
  376. });
  377. test('getHotmailVerificationRequestTimestamp prefers actual request timestamps with a safety buffer', () => {
  378. const signupRequestedAt = Date.UTC(2026, 3, 10, 12, 0, 30);
  379. const loginRequestedAt = Date.UTC(2026, 3, 10, 12, 5, 45);
  380. assert.equal(
  381. getHotmailVerificationRequestTimestamp(4, {
  382. signupVerificationRequestedAt: signupRequestedAt,
  383. flowStartTime: signupRequestedAt - 60_000,
  384. }),
  385. signupRequestedAt - 15_000
  386. );
  387. assert.equal(
  388. getHotmailVerificationRequestTimestamp(7, {
  389. loginVerificationRequestedAt: loginRequestedAt,
  390. lastEmailTimestamp: loginRequestedAt - 120_000,
  391. flowStartTime: loginRequestedAt - 300_000,
  392. }),
  393. loginRequestedAt - 15_000
  394. );
  395. });
  396. test('getHotmailMailApiRequestConfig defines a finite timeout for mailbox polling', () => {
  397. assert.deepEqual(getHotmailMailApiRequestConfig(), {
  398. timeoutMs: 15000,
  399. });
  400. });
  401. test('parseHotmailImportText parses account lines in email----password----clientId----token format', () => {
  402. const parsed = parseHotmailImportText(`
  403. 账号----密码----ID----Token
  404. JohnRodriguez5425@hotmail.com----nb4ta1OK----9e5f94bc-e8a4-4e73-b8be-63364c29d753----refresh-token-1
  405. alice@hotmail.com----pass-2----client-2----refresh-token-2
  406. `.trim());
  407. assert.deepEqual(parsed, [
  408. {
  409. email: 'JohnRodriguez5425@hotmail.com',
  410. password: 'nb4ta1OK',
  411. clientId: '9e5f94bc-e8a4-4e73-b8be-63364c29d753',
  412. refreshToken: 'refresh-token-1',
  413. },
  414. {
  415. email: 'alice@hotmail.com',
  416. password: 'pass-2',
  417. clientId: 'client-2',
  418. refreshToken: 'refresh-token-2',
  419. },
  420. ]);
  421. });