background-icloud.test.js 8.7 KB

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