background-icloud.test.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('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. const bundle = [
  49. extractFunction('normalizeEmailGenerator'),
  50. extractFunction('getEmailGeneratorLabel'),
  51. extractFunction('normalizeVerificationResendCount'),
  52. extractFunction('normalizePersistentSettingValue'),
  53. extractFunction('finalizeIcloudAliasAfterSuccessfulFlow'),
  54. ].join('\n');
  55. function createApi(overrides = {}) {
  56. return new Function('overrides', `
  57. const HOTMAIL_PROVIDER = 'hotmail-api';
  58. const HOTMAIL_SERVICE_MODE_LOCAL = 'local';
  59. const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
  60. const DEFAULT_LOCAL_CPA_STEP9_MODE = 'submit';
  61. const DEFAULT_HOTMAIL_REMOTE_BASE_URL = '';
  62. const DEFAULT_HOTMAIL_LOCAL_BASE_URL = 'http://127.0.0.1:17373';
  63. const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
  64. const DEFAULT_ADD_PHONE_PAUSE_MINUTES = 10;
  65. const AUTO_RUN_ADD_PHONE_PAUSE_MIN_MINUTES = 1;
  66. const AUTO_RUN_ADD_PHONE_PAUSE_MAX_MINUTES = 1440;
  67. const VERIFICATION_RESEND_COUNT_MIN = 0;
  68. const VERIFICATION_RESEND_COUNT_MAX = 20;
  69. const PERSISTED_SETTING_DEFAULTS = {
  70. mailProvider: '163',
  71. autoStepDelaySeconds: null,
  72. };
  73. const calls = {
  74. setUsed: [],
  75. logs: [],
  76. deletes: [],
  77. listCalls: 0,
  78. };
  79. function normalizeIcloudHost(value) {
  80. const normalized = String(value || '').trim().toLowerCase();
  81. return ['icloud.com', 'icloud.com.cn'].includes(normalized) ? normalized : '';
  82. }
  83. function normalizePanelMode(value = '') {
  84. return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa';
  85. }
  86. function normalizeMailProvider(value = '') {
  87. return String(value || '').trim().toLowerCase() || '163';
  88. }
  89. function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
  90. return Math.max(0, Math.floor(Number(value) || 0));
  91. }
  92. function normalizeAddPhonePauseMinutes(value, fallback = DEFAULT_ADD_PHONE_PAUSE_MINUTES) {
  93. const numeric = Number(value);
  94. return Number.isFinite(numeric)
  95. ? Math.min(AUTO_RUN_ADD_PHONE_PAUSE_MAX_MINUTES, Math.max(AUTO_RUN_ADD_PHONE_PAUSE_MIN_MINUTES, Math.floor(numeric)))
  96. : fallback;
  97. }
  98. function normalizeAutoRunDelayMinutes(value) {
  99. return Math.max(1, Math.floor(Number(value) || 30));
  100. }
  101. function normalizeAutoStepDelaySeconds(value, fallback = null) {
  102. const numeric = Number(value);
  103. return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback;
  104. }
  105. function normalizeHotmailServiceMode() {
  106. return HOTMAIL_SERVICE_MODE_LOCAL;
  107. }
  108. function normalizeHotmailRemoteBaseUrl(value = '') {
  109. return String(value || '').trim() || DEFAULT_HOTMAIL_REMOTE_BASE_URL;
  110. }
  111. function normalizeHotmailLocalBaseUrl(value = '') {
  112. return String(value || '').trim() || DEFAULT_HOTMAIL_LOCAL_BASE_URL;
  113. }
  114. function normalizeCloudflareDomain(value = '') {
  115. return String(value || '').trim().toLowerCase();
  116. }
  117. function normalizeCloudflareDomains(values = []) {
  118. return Array.isArray(values) ? values : [];
  119. }
  120. function normalizeCloudflareTempEmailAddress(value = '') {
  121. return String(value || '').trim().toLowerCase();
  122. }
  123. function normalizeCloudflareTempEmailReceiveMailbox(value = '') {
  124. const normalized = normalizeCloudflareTempEmailAddress(value);
  125. return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(normalized) ? normalized : '';
  126. }
  127. function normalizeHotmailAccounts(values = []) {
  128. return Array.isArray(values) ? values : [];
  129. }
  130. function getManualAliasUsageMap(state) {
  131. return { ...(state?.manualAliasUsage || {}) };
  132. }
  133. function getPreservedAliasMap(state) {
  134. return { ...(state?.preservedAliases || {}) };
  135. }
  136. function isAliasPreserved(state, email) {
  137. return Boolean(getPreservedAliasMap(state)[String(email || '').trim().toLowerCase()]);
  138. }
  139. async function setIcloudAliasUsedState(payload, options = {}) {
  140. calls.setUsed.push({ payload, options });
  141. }
  142. async function addLog(message, level = 'info') {
  143. calls.logs.push({ message, level });
  144. }
  145. async function deleteIcloudAlias(alias) {
  146. calls.deletes.push(alias);
  147. }
  148. async function listIcloudAliases() {
  149. calls.listCalls += 1;
  150. return overrides.listIcloudAliases ? overrides.listIcloudAliases() : [];
  151. }
  152. function findIcloudAliasByEmail(aliases, email) {
  153. return (aliases || []).find((alias) => String(alias.email || '').toLowerCase() === String(email || '').toLowerCase()) || null;
  154. }
  155. function getErrorMessage(error) {
  156. return String(typeof error === 'string' ? error : error?.message || '');
  157. }
  158. ${bundle}
  159. return {
  160. calls,
  161. normalizeEmailGenerator,
  162. getEmailGeneratorLabel,
  163. normalizePersistentSettingValue,
  164. finalizeIcloudAliasAfterSuccessfulFlow,
  165. };
  166. `)(overrides);
  167. }
  168. test('normalizeEmailGenerator and label support icloud', () => {
  169. const api = createApi();
  170. assert.equal(api.normalizeEmailGenerator('icloud'), 'icloud');
  171. assert.equal(api.getEmailGeneratorLabel('icloud'), 'iCloud 隐私邮箱');
  172. });
  173. test('normalizePersistentSettingValue handles icloud settings', () => {
  174. const api = createApi();
  175. assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com');
  176. assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto');
  177. assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true);
  178. assert.equal(api.normalizePersistentSettingValue('autoRunAddPhonePauseMinutes', '60'), 60);
  179. assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '6'), 6);
  180. assert.equal(api.normalizePersistentSettingValue('verificationResendCount', 99), 20);
  181. assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailReceiveMailbox', ' Forward@Example.com '), 'forward@example.com');
  182. });
  183. test('finalizeIcloudAliasAfterSuccessfulFlow marks icloud aliases as used without deleting when auto-delete is off', async () => {
  184. const api = createApi();
  185. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  186. email: 'alias@icloud.com',
  187. emailGenerator: 'icloud',
  188. autoDeleteUsedIcloudAlias: false,
  189. manualAliasUsage: {},
  190. preservedAliases: {},
  191. });
  192. assert.deepEqual(result, { handled: true, deleted: false });
  193. assert.equal(api.calls.setUsed.length, 1);
  194. assert.equal(api.calls.listCalls, 0);
  195. assert.equal(api.calls.deletes.length, 0);
  196. });
  197. test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting preserved aliases', async () => {
  198. const api = createApi();
  199. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  200. email: 'alias@icloud.com',
  201. emailGenerator: 'icloud',
  202. autoDeleteUsedIcloudAlias: true,
  203. manualAliasUsage: {},
  204. preservedAliases: { 'alias@icloud.com': true },
  205. });
  206. assert.deepEqual(result, { handled: true, deleted: false });
  207. assert.equal(api.calls.setUsed.length, 1);
  208. assert.equal(api.calls.listCalls, 0);
  209. assert.equal(api.calls.deletes.length, 0);
  210. });
  211. test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting aliases that are preserved in the latest alias list', async () => {
  212. const api = createApi({
  213. listIcloudAliases() {
  214. return [
  215. { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: true },
  216. ];
  217. },
  218. });
  219. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  220. email: 'alias@icloud.com',
  221. emailGenerator: 'icloud',
  222. autoDeleteUsedIcloudAlias: true,
  223. manualAliasUsage: {},
  224. preservedAliases: {},
  225. });
  226. assert.deepEqual(result, { handled: true, deleted: false });
  227. assert.equal(api.calls.setUsed.length, 1);
  228. assert.equal(api.calls.listCalls, 1);
  229. assert.equal(api.calls.deletes.length, 0);
  230. });
  231. test('finalizeIcloudAliasAfterSuccessfulFlow deletes alias when auto-delete is enabled and alias exists', async () => {
  232. const api = createApi({
  233. listIcloudAliases() {
  234. return [
  235. { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false },
  236. ];
  237. },
  238. });
  239. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  240. email: 'alias@icloud.com',
  241. emailGenerator: 'icloud',
  242. autoDeleteUsedIcloudAlias: true,
  243. manualAliasUsage: {},
  244. preservedAliases: {},
  245. });
  246. assert.deepEqual(result, { handled: true, deleted: true });
  247. assert.equal(api.calls.setUsed.length, 1);
  248. assert.equal(api.calls.listCalls, 1);
  249. assert.deepEqual(api.calls.deletes, [
  250. { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false },
  251. ]);
  252. });
  253. test('finalizeIcloudAliasAfterSuccessfulFlow ignores non-icloud flows', async () => {
  254. const api = createApi();
  255. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  256. email: 'plain@example.com',
  257. emailGenerator: 'duck',
  258. autoDeleteUsedIcloudAlias: true,
  259. manualAliasUsage: {},
  260. preservedAliases: {},
  261. });
  262. assert.deepEqual(result, { handled: false, deleted: false });
  263. assert.equal(api.calls.setUsed.length, 0);
  264. });