stream-wire-normalize.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. /// <reference types="vite/client" />
  2. import { describe, expect, it } from 'vitest';
  3. import { formValuesToWirePayload } from '@/lib/xray/inbound-form-adapter';
  4. import { formValuesToWirePayload as outboundToWire } from '@/lib/xray/outbound-form-adapter';
  5. import {
  6. normalizeSockoptForWire,
  7. normalizeStreamSettingsForWire,
  8. normalizeXhttpForWire,
  9. validateRealityClientVer,
  10. validateRealityMaxClientVer,
  11. validateRealityTarget,
  12. } from '@/lib/xray/stream-wire-normalize';
  13. import { InboundFormSchema } from '@/schemas/forms/inbound-form';
  14. import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
  15. import type { InboundFormValues } from '@/schemas/forms/inbound-form';
  16. import { XHttpXmuxSchema, XMUX_FRESH_DEFAULTS } from '@/schemas/protocols/stream/xhttp';
  17. describe('validateRealityTarget', () => {
  18. it('accepts host:port and bare port', () => {
  19. expect(validateRealityTarget('play.google.com:443')).toBeUndefined();
  20. expect(validateRealityTarget('443')).toBeUndefined();
  21. });
  22. it('rejects host without port', () => {
  23. expect(validateRealityTarget('play.google.com')).toBe(
  24. 'pages.inbounds.form.realityTargetNeedsPort',
  25. );
  26. expect(validateRealityTarget('')).toBe('pages.inbounds.form.realityTargetRequired');
  27. });
  28. });
  29. describe('validateRealityClientVer', () => {
  30. it('accepts empty (not set) and core-style versions', () => {
  31. expect(validateRealityClientVer('')).toBeUndefined();
  32. expect(validateRealityClientVer('26.3.27')).toBeUndefined();
  33. expect(validateRealityClientVer('1.0.0')).toBeUndefined();
  34. expect(validateRealityClientVer('26')).toBeUndefined();
  35. expect(validateRealityClientVer('26.3')).toBeUndefined();
  36. expect(validateRealityClientVer('0.0.255')).toBeUndefined();
  37. });
  38. it('rejects untrimmed values because the save path ships them verbatim', () => {
  39. expect(validateRealityClientVer('26.3.27 ')).toBe('pages.inbounds.form.clientVerInvalid');
  40. expect(validateRealityClientVer(' 26.3.27')).toBe('pages.inbounds.form.clientVerInvalid');
  41. expect(validateRealityClientVer(' ')).toBe('pages.inbounds.form.clientVerInvalid');
  42. });
  43. it('rejects what the core parser rejects', () => {
  44. expect(validateRealityClientVer('26.3.27.1')).toBe('pages.inbounds.form.clientVerInvalid');
  45. expect(validateRealityClientVer('26.3.256')).toBe('pages.inbounds.form.clientVerInvalid');
  46. expect(validateRealityClientVer('v26.3.27')).toBe('pages.inbounds.form.clientVerInvalid');
  47. expect(validateRealityClientVer('26..27')).toBe('pages.inbounds.form.clientVerInvalid');
  48. expect(validateRealityClientVer('26.3.')).toBe('pages.inbounds.form.clientVerInvalid');
  49. expect(validateRealityClientVer('-1.0.0')).toBe('pages.inbounds.form.clientVerInvalid');
  50. });
  51. });
  52. describe('validateRealityMaxClientVer', () => {
  53. it('accepts an empty max, an empty min, and a valid range', () => {
  54. expect(validateRealityMaxClientVer('', '26.3.27')).toBeUndefined();
  55. expect(validateRealityMaxClientVer('27.0.0', '')).toBeUndefined();
  56. expect(validateRealityMaxClientVer('26.3.27', '26.3.27')).toBeUndefined();
  57. expect(validateRealityMaxClientVer('27.1.2', '26.3.27')).toBeUndefined();
  58. });
  59. it('rejects a max below the min, the stale-placeholder trap included', () => {
  60. expect(validateRealityMaxClientVer('25.9.11', '26.3.27')).toBe(
  61. 'pages.inbounds.form.maxClientVerBelowMin',
  62. );
  63. expect(validateRealityMaxClientVer('26.3.26', '26.3.27')).toBe(
  64. 'pages.inbounds.form.maxClientVerBelowMin',
  65. );
  66. });
  67. it('pads short versions like the core does before comparing', () => {
  68. expect(validateRealityMaxClientVer('26', '26.0.0')).toBeUndefined();
  69. expect(validateRealityMaxClientVer('26', '26.3')).toBe(
  70. 'pages.inbounds.form.maxClientVerBelowMin',
  71. );
  72. });
  73. it('reports format errors before range errors and skips a malformed min', () => {
  74. expect(validateRealityMaxClientVer('25.9', 'not-a-version')).toBeUndefined();
  75. expect(validateRealityMaxClientVer('nope', '26.3.27')).toBe(
  76. 'pages.inbounds.form.clientVerInvalid',
  77. );
  78. });
  79. });
  80. describe('normalizeXhttpForWire stream-one', () => {
  81. it('drops packet-up and stream-up-only fields on inbound', () => {
  82. const out = normalizeXhttpForWire(
  83. {
  84. path: '/app',
  85. host: 'play.google.com',
  86. mode: 'stream-one',
  87. xPaddingBytes: '100-1000',
  88. scMaxEachPostBytes: '1000000',
  89. scMinPostsIntervalMs: '30',
  90. scMaxBufferedPosts: 30,
  91. scStreamUpServerSecs: '20-80',
  92. enableXmux: false,
  93. headers: {},
  94. },
  95. 'inbound',
  96. );
  97. expect(out).toMatchObject({
  98. path: '/app',
  99. host: 'play.google.com',
  100. mode: 'stream-one',
  101. xPaddingBytes: '100-1000',
  102. });
  103. expect(out).not.toHaveProperty('scMaxEachPostBytes');
  104. expect(out).not.toHaveProperty('scMinPostsIntervalMs');
  105. expect(out).not.toHaveProperty('scMaxBufferedPosts');
  106. expect(out).not.toHaveProperty('scStreamUpServerSecs');
  107. expect(out).not.toHaveProperty('enableXmux');
  108. expect(out).not.toHaveProperty('headers');
  109. });
  110. it('preserves non-default scMinPostsIntervalMs on inbound for subscriptions', () => {
  111. const out = normalizeXhttpForWire(
  112. {
  113. path: '/app',
  114. mode: 'packet-up',
  115. scMinPostsIntervalMs: '50-150',
  116. enableXmux: false,
  117. },
  118. 'inbound',
  119. );
  120. expect(out.scMinPostsIntervalMs).toBe('50-150');
  121. });
  122. it('strips empty scMinPostsIntervalMs on inbound', () => {
  123. const out = normalizeXhttpForWire(
  124. {
  125. path: '/app',
  126. mode: 'packet-up',
  127. scMinPostsIntervalMs: '',
  128. enableXmux: false,
  129. },
  130. 'inbound',
  131. );
  132. expect(out).not.toHaveProperty('scMinPostsIntervalMs');
  133. });
  134. it('keeps xmux on outbound stream-one', () => {
  135. const out = normalizeXhttpForWire(
  136. {
  137. path: '/app',
  138. mode: 'stream-one',
  139. xPaddingBytes: '100-1000',
  140. xmux: { maxConcurrency: '16-32' },
  141. scMaxEachPostBytes: '1000000',
  142. },
  143. 'outbound',
  144. );
  145. expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
  146. expect(out).not.toHaveProperty('scMaxEachPostBytes');
  147. });
  148. it('keeps inbound xmux when enableXmux is on (stored for subscription extra; stripped from xray config on Go side)', () => {
  149. const out = normalizeXhttpForWire(
  150. {
  151. path: '/app',
  152. mode: 'auto',
  153. enableXmux: true,
  154. xmux: { maxConcurrency: '16-32' },
  155. },
  156. 'inbound',
  157. );
  158. expect(out).not.toHaveProperty('enableXmux');
  159. expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
  160. });
  161. it('drops inbound xmux when enableXmux is off', () => {
  162. const out = normalizeXhttpForWire(
  163. {
  164. path: '/app',
  165. mode: 'auto',
  166. enableXmux: false,
  167. xmux: { maxConcurrency: '16-32' },
  168. },
  169. 'inbound',
  170. );
  171. expect(out).not.toHaveProperty('enableXmux');
  172. expect(out).not.toHaveProperty('xmux');
  173. });
  174. // xray-core rejects a config with both maxConnections and maxConcurrency.
  175. it('drops maxConcurrency when maxConnections is set (xray-core exclusivity)', () => {
  176. const out = normalizeXhttpForWire(
  177. {
  178. path: '/app',
  179. mode: 'auto',
  180. enableXmux: true,
  181. xmux: { maxConcurrency: '16-32', maxConnections: 4, hKeepAlivePeriod: 30 },
  182. },
  183. 'inbound',
  184. );
  185. const xmux = out.xmux as Record<string, unknown>;
  186. expect(xmux).not.toHaveProperty('maxConcurrency');
  187. expect(xmux.maxConnections).toBe(4);
  188. expect(xmux.hKeepAlivePeriod).toBe(30);
  189. });
  190. it('keeps maxConcurrency when maxConnections is 0/unset', () => {
  191. const out = normalizeXhttpForWire(
  192. {
  193. path: '/app',
  194. mode: 'stream-one',
  195. xmux: { maxConcurrency: '16-32', maxConnections: 0 },
  196. },
  197. 'outbound',
  198. );
  199. const xmux = out.xmux as Record<string, unknown>;
  200. expect(xmux.maxConcurrency).toBe('16-32');
  201. expect(xmux.maxConnections).toBe(0);
  202. });
  203. it('applies xmux exclusivity on the outbound side too', () => {
  204. const out = normalizeXhttpForWire(
  205. {
  206. path: '/app',
  207. mode: 'stream-one',
  208. xmux: { maxConcurrency: '16-32', maxConnections: '8' },
  209. },
  210. 'outbound',
  211. );
  212. const xmux = out.xmux as Record<string, unknown>;
  213. expect(xmux).not.toHaveProperty('maxConcurrency');
  214. expect(xmux.maxConnections).toBe('8');
  215. });
  216. it('bare schema defaults keep only one exclusive xmux strategy non-zero', () => {
  217. expect(XHttpXmuxSchema.parse({}).maxConnections).toBe(0);
  218. expect(XHttpXmuxSchema.parse({}).maxConcurrency).toBe('16-32');
  219. });
  220. it('XMUX_FRESH_DEFAULTS seeds the core maxConnections fallback without a competing maxConcurrency', () => {
  221. expect(XMUX_FRESH_DEFAULTS.maxConnections).toBe(3);
  222. expect(XMUX_FRESH_DEFAULTS.maxConcurrency).toBe('');
  223. const out = normalizeXhttpForWire(
  224. {
  225. path: '/app',
  226. mode: 'stream-one',
  227. enableXmux: true,
  228. xmux: XMUX_FRESH_DEFAULTS,
  229. },
  230. 'outbound',
  231. );
  232. const xmux = out.xmux as Record<string, unknown>;
  233. expect(xmux.maxConnections).toBe(3);
  234. expect(xmux.maxConcurrency).toBe('');
  235. });
  236. });
  237. describe('normalizeSockoptForWire', () => {
  238. it('omits doc-example defaults that throttle throughput', () => {
  239. const out = normalizeSockoptForWire({
  240. tcpWindowClamp: 0,
  241. tcpMaxSeg: 0,
  242. tcpUserTimeout: 0,
  243. tcpFastOpen: true,
  244. tcpcongestion: 'bbr',
  245. domainStrategy: 'AsIs',
  246. tproxy: 'off',
  247. mark: 0,
  248. });
  249. expect(out).toEqual({
  250. tcpFastOpen: true,
  251. tcpcongestion: 'bbr',
  252. });
  253. });
  254. it('preserves happyEyeballs on freedom-style outbound', () => {
  255. const out = normalizeSockoptForWire({
  256. domainStrategy: 'UseIP',
  257. happyEyeballs: {
  258. tryDelayMs: 150,
  259. prioritizeIPv6: true,
  260. interleave: 1,
  261. maxConcurrentTry: 4,
  262. },
  263. });
  264. expect(out?.happyEyeballs).toMatchObject({
  265. tryDelayMs: 150,
  266. prioritizeIPv6: true,
  267. });
  268. expect(out?.domainStrategy).toBe('UseIP');
  269. });
  270. it('keeps a freshly toggled happyEyeballs (schema defaults) across the wire round trip', () => {
  271. const out = normalizeSockoptForWire({
  272. happyEyeballs: HappyEyeballsSchema.parse({}),
  273. });
  274. expect(out?.happyEyeballs).toEqual({ tryDelayMs: 250 });
  275. });
  276. it('keeps an explicit tryDelayMs of 0 so it cannot rehydrate as the 250 default', () => {
  277. const out = normalizeSockoptForWire({
  278. happyEyeballs: { tryDelayMs: 0, prioritizeIPv6: true, interleave: 1, maxConcurrentTry: 4 },
  279. });
  280. expect(out?.happyEyeballs).toEqual({ tryDelayMs: 0, prioritizeIPv6: true });
  281. });
  282. });
  283. describe('normalizeStreamSettingsForWire reality', () => {
  284. it('preserves the nested client settings on inbound (share links read publicKey from there)', () => {
  285. const out = normalizeStreamSettingsForWire(
  286. {
  287. network: 'xhttp',
  288. security: 'reality',
  289. realitySettings: {
  290. target: 'play.google.com:443',
  291. privateKey: 'priv',
  292. serverNames: ['play.google.com'],
  293. shortIds: ['abcd'],
  294. settings: {
  295. publicKey: 'pub',
  296. fingerprint: 'chrome',
  297. spiderX: '/',
  298. },
  299. },
  300. },
  301. { side: 'inbound' },
  302. );
  303. const reality = out.realitySettings as Record<string, unknown>;
  304. expect(reality.target).toBe('play.google.com:443');
  305. expect(reality.privateKey).toBe('priv');
  306. const settings = reality.settings as Record<string, unknown>;
  307. expect(settings.publicKey).toBe('pub');
  308. expect(settings.spiderX).toBe('/');
  309. });
  310. it('passes client realitySettings through unchanged on outbound', () => {
  311. const out = normalizeStreamSettingsForWire(
  312. {
  313. network: 'xhttp',
  314. security: 'reality',
  315. realitySettings: {
  316. publicKey: 'pub',
  317. fingerprint: 'chrome',
  318. serverName: 'play.google.com',
  319. shortId: 'abcd',
  320. spiderX: '/x',
  321. },
  322. },
  323. { side: 'outbound' },
  324. );
  325. const reality = out.realitySettings as Record<string, unknown>;
  326. expect(reality.publicKey).toBe('pub');
  327. expect(reality.serverName).toBe('play.google.com');
  328. expect(reality.spiderX).toBe('/x');
  329. });
  330. });
  331. describe('normalizeStreamSettingsForWire tls', () => {
  332. it('drops empty uTLS fingerprints from inbound and outbound TLS shapes', () => {
  333. const out = normalizeStreamSettingsForWire(
  334. {
  335. network: 'hysteria',
  336. security: 'tls',
  337. tlsSettings: {
  338. fingerprint: '',
  339. settings: {
  340. fingerprint: '',
  341. echConfigList: '',
  342. },
  343. },
  344. },
  345. { side: 'inbound' },
  346. );
  347. const tls = out.tlsSettings as Record<string, unknown>;
  348. const settings = tls.settings as Record<string, unknown>;
  349. expect(tls).not.toHaveProperty('fingerprint');
  350. expect(settings).not.toHaveProperty('fingerprint');
  351. expect(settings.echConfigList).toBe('');
  352. });
  353. });
  354. describe('inbound formValuesToWirePayload integration', () => {
  355. it('emits lean stream-one xhttp + sockopt on save', () => {
  356. const values = {
  357. remark: 't',
  358. enable: true,
  359. port: 443,
  360. listen: '0.0.0.0',
  361. tag: 'in-443',
  362. expiryTime: 0,
  363. sniffing: { enabled: false },
  364. up: 0,
  365. down: 0,
  366. total: 0,
  367. trafficReset: 'never',
  368. lastTrafficResetTime: 0,
  369. nodeId: null,
  370. protocol: 'vless',
  371. settings: {
  372. clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }],
  373. decryption: 'none',
  374. },
  375. streamSettings: {
  376. network: 'xhttp',
  377. security: 'reality',
  378. realitySettings: {
  379. target: 'play.google.com:443',
  380. privateKey: 'priv',
  381. serverNames: ['play.google.com'],
  382. shortIds: ['44003d86dc1e'],
  383. settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
  384. },
  385. xhttpSettings: {
  386. path: '/app',
  387. host: 'play.google.com',
  388. mode: 'stream-one',
  389. xPaddingBytes: '100-1000',
  390. scMaxEachPostBytes: '1000000',
  391. scMinPostsIntervalMs: '30',
  392. enableXmux: false,
  393. },
  394. sockopt: {
  395. tcpWindowClamp: 0,
  396. tcpMaxSeg: 0,
  397. tcpUserTimeout: 0,
  398. tcpFastOpen: true,
  399. tcpcongestion: 'bbr',
  400. },
  401. },
  402. } as InboundFormValues;
  403. const payload = formValuesToWirePayload(values);
  404. const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
  405. const xhttp = stream.xhttpSettings as Record<string, unknown>;
  406. const sockopt = stream.sockopt as Record<string, unknown>;
  407. const reality = stream.realitySettings as Record<string, unknown>;
  408. expect(xhttp).not.toHaveProperty('scMaxEachPostBytes');
  409. expect(sockopt).not.toHaveProperty('tcpWindowClamp');
  410. expect(sockopt.tcpFastOpen).toBe(true);
  411. const realitySettings = reality.settings as Record<string, unknown>;
  412. expect(realitySettings.publicKey).toBe('pub');
  413. });
  414. it('accepts Hysteria TLS with uTLS None and omits fingerprint on save', () => {
  415. const values = {
  416. remark: 'hy2',
  417. enable: true,
  418. port: 443,
  419. listen: '',
  420. tag: 'hy2-443',
  421. expiryTime: 0,
  422. sniffing: { enabled: false },
  423. up: 0,
  424. down: 0,
  425. total: 0,
  426. trafficReset: 'never',
  427. lastTrafficResetTime: 0,
  428. nodeId: null,
  429. protocol: 'hysteria',
  430. settings: { version: 2, clients: [] },
  431. streamSettings: {
  432. network: 'hysteria',
  433. security: 'tls',
  434. hysteriaSettings: {
  435. version: 2,
  436. auth: 'auth',
  437. udpIdleTimeout: 60,
  438. },
  439. tlsSettings: {
  440. alpn: ['h3'],
  441. certificates: [{ certificateFile: '/cert/server.pem', keyFile: '/cert/server.key' }],
  442. settings: {
  443. fingerprint: '',
  444. },
  445. },
  446. },
  447. };
  448. const parsed = InboundFormSchema.safeParse(values);
  449. expect(parsed.success).toBe(true);
  450. if (!parsed.success) throw parsed.error;
  451. const payload = formValuesToWirePayload(parsed.data);
  452. const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
  453. const tls = stream.tlsSettings as Record<string, unknown>;
  454. const settings = tls.settings as Record<string, unknown>;
  455. expect(settings).not.toHaveProperty('fingerprint');
  456. });
  457. it('preserves non-default scMinPostsIntervalMs in packet-up inbound wire payload for subscriptions', () => {
  458. const values = {
  459. remark: 't',
  460. enable: true,
  461. port: 443,
  462. listen: '0.0.0.0',
  463. tag: 'in-443',
  464. expiryTime: 0,
  465. sniffing: { enabled: false },
  466. up: 0,
  467. down: 0,
  468. total: 0,
  469. trafficReset: 'never',
  470. lastTrafficResetTime: 0,
  471. nodeId: null,
  472. protocol: 'vless',
  473. settings: {
  474. clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }],
  475. decryption: 'none',
  476. },
  477. streamSettings: {
  478. network: 'xhttp',
  479. security: 'reality',
  480. realitySettings: {
  481. target: 'play.google.com:443',
  482. privateKey: 'priv',
  483. serverNames: ['play.google.com'],
  484. shortIds: ['44003d86dc1e'],
  485. settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
  486. },
  487. xhttpSettings: {
  488. path: '/app',
  489. host: 'play.google.com',
  490. mode: 'packet-up',
  491. scMinPostsIntervalMs: '50-150',
  492. },
  493. sockopt: {},
  494. },
  495. };
  496. const parsed = InboundFormSchema.safeParse(values);
  497. expect(parsed.success).toBe(true);
  498. if (!parsed.success) throw parsed.error;
  499. const payload = formValuesToWirePayload(parsed.data);
  500. const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
  501. const xhttp = stream.xhttpSettings as Record<string, unknown>;
  502. expect(xhttp.scMinPostsIntervalMs).toBe('50-150');
  503. });
  504. it('strips default scMinPostsIntervalMs=30 from inbound wire payload', () => {
  505. const values = {
  506. remark: 't',
  507. enable: true,
  508. port: 443,
  509. listen: '0.0.0.0',
  510. tag: 'in-443',
  511. expiryTime: 0,
  512. sniffing: { enabled: false },
  513. up: 0,
  514. down: 0,
  515. total: 0,
  516. trafficReset: 'never',
  517. lastTrafficResetTime: 0,
  518. nodeId: null,
  519. protocol: 'vless',
  520. settings: {
  521. clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }],
  522. decryption: 'none',
  523. },
  524. streamSettings: {
  525. network: 'xhttp',
  526. security: 'reality',
  527. realitySettings: {
  528. target: 'play.google.com:443',
  529. privateKey: 'priv',
  530. serverNames: ['play.google.com'],
  531. shortIds: ['44003d86dc1e'],
  532. settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
  533. },
  534. xhttpSettings: {
  535. path: '/app',
  536. host: 'play.google.com',
  537. mode: 'packet-up',
  538. scMinPostsIntervalMs: '30',
  539. },
  540. sockopt: {},
  541. },
  542. };
  543. const parsed = InboundFormSchema.safeParse(values);
  544. expect(parsed.success).toBe(true);
  545. if (!parsed.success) throw parsed.error;
  546. const payload = formValuesToWirePayload(parsed.data);
  547. const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
  548. const xhttp = stream.xhttpSettings as Record<string, unknown>;
  549. expect(xhttp).not.toHaveProperty('scMinPostsIntervalMs');
  550. });
  551. });
  552. describe('freedom outbound sockopt wire payload', () => {
  553. it('preserves happyEyeballs on direct freedom outbound', () => {
  554. const wire = outboundToWire({
  555. protocol: 'freedom',
  556. tag: 'direct',
  557. settings: { domainStrategy: 'UseIP' },
  558. streamSettings: {
  559. sockopt: {
  560. domainStrategy: 'UseIP',
  561. happyEyeballs: {
  562. tryDelayMs: 150,
  563. prioritizeIPv6: true,
  564. interleave: 1,
  565. maxConcurrentTry: 4,
  566. },
  567. },
  568. },
  569. } as Parameters<typeof outboundToWire>[0]);
  570. expect(wire.streamSettings).toMatchObject({
  571. sockopt: {
  572. domainStrategy: 'UseIP',
  573. happyEyeballs: {
  574. tryDelayMs: 150,
  575. prioritizeIPv6: true,
  576. },
  577. },
  578. });
  579. });
  580. });