background-icloud.test.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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 normalizeHotmailAccounts(values = []) {
  108. return Array.isArray(values) ? values : [];
  109. }
  110. function getManualAliasUsageMap(state) {
  111. return { ...(state?.manualAliasUsage || {}) };
  112. }
  113. function getPreservedAliasMap(state) {
  114. return { ...(state?.preservedAliases || {}) };
  115. }
  116. function isAliasPreserved(state, email) {
  117. return Boolean(getPreservedAliasMap(state)[String(email || '').trim().toLowerCase()]);
  118. }
  119. async function setIcloudAliasUsedState(payload, options = {}) {
  120. calls.setUsed.push({ payload, options });
  121. }
  122. async function addLog(message, level = 'info') {
  123. calls.logs.push({ message, level });
  124. }
  125. async function deleteIcloudAlias(alias) {
  126. calls.deletes.push(alias);
  127. }
  128. async function listIcloudAliases() {
  129. calls.listCalls += 1;
  130. return overrides.listIcloudAliases ? overrides.listIcloudAliases() : [];
  131. }
  132. function findIcloudAliasByEmail(aliases, email) {
  133. return (aliases || []).find((alias) => String(alias.email || '').toLowerCase() === String(email || '').toLowerCase()) || null;
  134. }
  135. function getErrorMessage(error) {
  136. return String(typeof error === 'string' ? error : error?.message || '');
  137. }
  138. ${bundle}
  139. return {
  140. calls,
  141. normalizeEmailGenerator,
  142. getEmailGeneratorLabel,
  143. normalizePersistentSettingValue,
  144. finalizeIcloudAliasAfterSuccessfulFlow,
  145. };
  146. `)(overrides);
  147. }
  148. test('normalizeEmailGenerator and label support icloud', () => {
  149. const api = createApi();
  150. assert.equal(api.normalizeEmailGenerator('icloud'), 'icloud');
  151. assert.equal(api.getEmailGeneratorLabel('icloud'), 'iCloud 隐私邮箱');
  152. });
  153. test('normalizePersistentSettingValue handles icloud settings', () => {
  154. const api = createApi();
  155. assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'icloud.com'), 'icloud.com');
  156. assert.equal(api.normalizePersistentSettingValue('icloudHostPreference', 'bad-host'), 'auto');
  157. assert.equal(api.normalizePersistentSettingValue('autoDeleteUsedIcloudAlias', 1), true);
  158. });
  159. test('finalizeIcloudAliasAfterSuccessfulFlow marks icloud aliases as used without deleting when auto-delete is off', async () => {
  160. const api = createApi();
  161. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  162. email: 'alias@icloud.com',
  163. emailGenerator: 'icloud',
  164. autoDeleteUsedIcloudAlias: false,
  165. manualAliasUsage: {},
  166. preservedAliases: {},
  167. });
  168. assert.deepEqual(result, { handled: true, deleted: false });
  169. assert.equal(api.calls.setUsed.length, 1);
  170. assert.equal(api.calls.listCalls, 0);
  171. assert.equal(api.calls.deletes.length, 0);
  172. });
  173. test('finalizeIcloudAliasAfterSuccessfulFlow skips deleting preserved aliases', async () => {
  174. const api = createApi();
  175. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  176. email: 'alias@icloud.com',
  177. emailGenerator: 'icloud',
  178. autoDeleteUsedIcloudAlias: true,
  179. manualAliasUsage: {},
  180. preservedAliases: { 'alias@icloud.com': true },
  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 aliases that are preserved in the latest alias list', async () => {
  188. const api = createApi({
  189. listIcloudAliases() {
  190. return [
  191. { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: true },
  192. ];
  193. },
  194. });
  195. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  196. email: 'alias@icloud.com',
  197. emailGenerator: 'icloud',
  198. autoDeleteUsedIcloudAlias: true,
  199. manualAliasUsage: {},
  200. preservedAliases: {},
  201. });
  202. assert.deepEqual(result, { handled: true, deleted: false });
  203. assert.equal(api.calls.setUsed.length, 1);
  204. assert.equal(api.calls.listCalls, 1);
  205. assert.equal(api.calls.deletes.length, 0);
  206. });
  207. test('finalizeIcloudAliasAfterSuccessfulFlow deletes alias when auto-delete is enabled and alias exists', async () => {
  208. const api = createApi({
  209. listIcloudAliases() {
  210. return [
  211. { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false },
  212. ];
  213. },
  214. });
  215. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  216. email: 'alias@icloud.com',
  217. emailGenerator: 'icloud',
  218. autoDeleteUsedIcloudAlias: true,
  219. manualAliasUsage: {},
  220. preservedAliases: {},
  221. });
  222. assert.deepEqual(result, { handled: true, deleted: true });
  223. assert.equal(api.calls.setUsed.length, 1);
  224. assert.equal(api.calls.listCalls, 1);
  225. assert.deepEqual(api.calls.deletes, [
  226. { email: 'alias@icloud.com', anonymousId: 'anon-1', preserved: false },
  227. ]);
  228. });
  229. test('finalizeIcloudAliasAfterSuccessfulFlow ignores non-icloud flows', async () => {
  230. const api = createApi();
  231. const result = await api.finalizeIcloudAliasAfterSuccessfulFlow({
  232. email: 'plain@example.com',
  233. emailGenerator: 'duck',
  234. autoDeleteUsedIcloudAlias: true,
  235. manualAliasUsage: {},
  236. preservedAliases: {},
  237. });
  238. assert.deepEqual(result, { handled: false, deleted: false });
  239. assert.equal(api.calls.setUsed.length, 0);
  240. });