background-icloud.test.js 9.0 KB

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