mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-22 07:02:20 +03:00
* feat(ui): validate the REALITY client version range at save time The impossible range from PR #6125 — a max below the effective minimum — could still be saved; the tooltip only helps a user who hovers it. Add save-time validation mirroring xray-core's parser (up to three dot-separated parts, each 0-255) on both fields, plus a cross-field check that a non-empty max is not below a non-empty min. Errors are field-level i18n keys following the REALITY target precedent, so the modal stays open and points at the offending field instead of storing a config that rejects every client. A malformed min is reported by its own field and skipped by the max comparison, so the user sees one precise error per field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): reject untrimmed client versions and revalidate max on min edits From review: the validators trimmed but the save path ships the value verbatim, and xray-core's part parser accepts no surrounding whitespace — so a green form could still save a config the core refuses to load. Reject any value that differs from its trimmed form. Also revalidate the max field after a min edit when max already shows an error, so correcting the min clears the stale cross-field message without waiting for the next submit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
592 lines
19 KiB
TypeScript
592 lines
19 KiB
TypeScript
/// <reference types="vite/client" />
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { formValuesToWirePayload } from '@/lib/xray/inbound-form-adapter';
|
|
import { formValuesToWirePayload as outboundToWire } from '@/lib/xray/outbound-form-adapter';
|
|
import {
|
|
normalizeSockoptForWire,
|
|
normalizeStreamSettingsForWire,
|
|
normalizeXhttpForWire,
|
|
validateRealityClientVer,
|
|
validateRealityMaxClientVer,
|
|
validateRealityTarget,
|
|
} from '@/lib/xray/stream-wire-normalize';
|
|
import { InboundFormSchema } from '@/schemas/forms/inbound-form';
|
|
import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
|
|
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
|
import { XHttpXmuxSchema, XMUX_FRESH_DEFAULTS } from '@/schemas/protocols/stream/xhttp';
|
|
|
|
describe('validateRealityTarget', () => {
|
|
it('accepts host:port and bare port', () => {
|
|
expect(validateRealityTarget('play.google.com:443')).toBeUndefined();
|
|
expect(validateRealityTarget('443')).toBeUndefined();
|
|
});
|
|
|
|
it('rejects host without port', () => {
|
|
expect(validateRealityTarget('play.google.com')).toBe('pages.inbounds.form.realityTargetNeedsPort');
|
|
expect(validateRealityTarget('')).toBe('pages.inbounds.form.realityTargetRequired');
|
|
});
|
|
});
|
|
|
|
describe('validateRealityClientVer', () => {
|
|
it('accepts empty (not set) and core-style versions', () => {
|
|
expect(validateRealityClientVer('')).toBeUndefined();
|
|
expect(validateRealityClientVer('26.3.27')).toBeUndefined();
|
|
expect(validateRealityClientVer('1.0.0')).toBeUndefined();
|
|
expect(validateRealityClientVer('26')).toBeUndefined();
|
|
expect(validateRealityClientVer('26.3')).toBeUndefined();
|
|
expect(validateRealityClientVer('0.0.255')).toBeUndefined();
|
|
});
|
|
|
|
it('rejects untrimmed values because the save path ships them verbatim', () => {
|
|
expect(validateRealityClientVer('26.3.27 ')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer(' 26.3.27')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer(' ')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
});
|
|
|
|
it('rejects what the core parser rejects', () => {
|
|
expect(validateRealityClientVer('26.3.27.1')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer('26.3.256')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer('v26.3.27')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer('26..27')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer('26.3.')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
expect(validateRealityClientVer('-1.0.0')).toBe('pages.inbounds.form.clientVerInvalid');
|
|
});
|
|
});
|
|
|
|
describe('validateRealityMaxClientVer', () => {
|
|
it('accepts an empty max, an empty min, and a valid range', () => {
|
|
expect(validateRealityMaxClientVer('', '26.3.27')).toBeUndefined();
|
|
expect(validateRealityMaxClientVer('27.0.0', '')).toBeUndefined();
|
|
expect(validateRealityMaxClientVer('26.3.27', '26.3.27')).toBeUndefined();
|
|
expect(validateRealityMaxClientVer('27.1.2', '26.3.27')).toBeUndefined();
|
|
});
|
|
|
|
it('rejects a max below the min, the stale-placeholder trap included', () => {
|
|
expect(validateRealityMaxClientVer('25.9.11', '26.3.27')).toBe(
|
|
'pages.inbounds.form.maxClientVerBelowMin',
|
|
);
|
|
expect(validateRealityMaxClientVer('26.3.26', '26.3.27')).toBe(
|
|
'pages.inbounds.form.maxClientVerBelowMin',
|
|
);
|
|
});
|
|
|
|
it('pads short versions like the core does before comparing', () => {
|
|
expect(validateRealityMaxClientVer('26', '26.0.0')).toBeUndefined();
|
|
expect(validateRealityMaxClientVer('26', '26.3')).toBe(
|
|
'pages.inbounds.form.maxClientVerBelowMin',
|
|
);
|
|
});
|
|
|
|
it('reports format errors before range errors and skips a malformed min', () => {
|
|
expect(validateRealityMaxClientVer('25.9', 'not-a-version')).toBeUndefined();
|
|
expect(validateRealityMaxClientVer('nope', '26.3.27')).toBe(
|
|
'pages.inbounds.form.clientVerInvalid',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('normalizeXhttpForWire stream-one', () => {
|
|
it('drops packet-up and stream-up-only fields on inbound', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
host: 'play.google.com',
|
|
mode: 'stream-one',
|
|
xPaddingBytes: '100-1000',
|
|
scMaxEachPostBytes: '1000000',
|
|
scMinPostsIntervalMs: '30',
|
|
scMaxBufferedPosts: 30,
|
|
scStreamUpServerSecs: '20-80',
|
|
enableXmux: false,
|
|
headers: {},
|
|
}, 'inbound');
|
|
|
|
expect(out).toMatchObject({
|
|
path: '/app',
|
|
host: 'play.google.com',
|
|
mode: 'stream-one',
|
|
xPaddingBytes: '100-1000',
|
|
});
|
|
expect(out).not.toHaveProperty('scMaxEachPostBytes');
|
|
expect(out).not.toHaveProperty('scMinPostsIntervalMs');
|
|
expect(out).not.toHaveProperty('scMaxBufferedPosts');
|
|
expect(out).not.toHaveProperty('scStreamUpServerSecs');
|
|
expect(out).not.toHaveProperty('enableXmux');
|
|
expect(out).not.toHaveProperty('headers');
|
|
});
|
|
|
|
it('preserves non-default scMinPostsIntervalMs on inbound for subscriptions', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'packet-up',
|
|
scMinPostsIntervalMs: '50-150',
|
|
enableXmux: false,
|
|
}, 'inbound');
|
|
|
|
expect(out.scMinPostsIntervalMs).toBe('50-150');
|
|
});
|
|
|
|
it('strips empty scMinPostsIntervalMs on inbound', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'packet-up',
|
|
scMinPostsIntervalMs: '',
|
|
enableXmux: false,
|
|
}, 'inbound');
|
|
|
|
expect(out).not.toHaveProperty('scMinPostsIntervalMs');
|
|
});
|
|
|
|
it('keeps xmux on outbound stream-one', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'stream-one',
|
|
xPaddingBytes: '100-1000',
|
|
xmux: { maxConcurrency: '16-32' },
|
|
scMaxEachPostBytes: '1000000',
|
|
}, 'outbound');
|
|
|
|
expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
|
|
expect(out).not.toHaveProperty('scMaxEachPostBytes');
|
|
});
|
|
|
|
it('keeps inbound xmux when enableXmux is on (stored for subscription extra; stripped from xray config on Go side)', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'auto',
|
|
enableXmux: true,
|
|
xmux: { maxConcurrency: '16-32' },
|
|
}, 'inbound');
|
|
|
|
expect(out).not.toHaveProperty('enableXmux');
|
|
expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
|
|
});
|
|
|
|
it('drops inbound xmux when enableXmux is off', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'auto',
|
|
enableXmux: false,
|
|
xmux: { maxConcurrency: '16-32' },
|
|
}, 'inbound');
|
|
|
|
expect(out).not.toHaveProperty('enableXmux');
|
|
expect(out).not.toHaveProperty('xmux');
|
|
});
|
|
|
|
// xray-core rejects a config with both maxConnections and maxConcurrency.
|
|
it('drops maxConcurrency when maxConnections is set (xray-core exclusivity)', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'auto',
|
|
enableXmux: true,
|
|
xmux: { maxConcurrency: '16-32', maxConnections: 4, hKeepAlivePeriod: 30 },
|
|
}, 'inbound');
|
|
|
|
const xmux = out.xmux as Record<string, unknown>;
|
|
expect(xmux).not.toHaveProperty('maxConcurrency');
|
|
expect(xmux.maxConnections).toBe(4);
|
|
expect(xmux.hKeepAlivePeriod).toBe(30);
|
|
});
|
|
|
|
it('keeps maxConcurrency when maxConnections is 0/unset', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'stream-one',
|
|
xmux: { maxConcurrency: '16-32', maxConnections: 0 },
|
|
}, 'outbound');
|
|
|
|
const xmux = out.xmux as Record<string, unknown>;
|
|
expect(xmux.maxConcurrency).toBe('16-32');
|
|
expect(xmux.maxConnections).toBe(0);
|
|
});
|
|
|
|
it('applies xmux exclusivity on the outbound side too', () => {
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'stream-one',
|
|
xmux: { maxConcurrency: '16-32', maxConnections: '8' },
|
|
}, 'outbound');
|
|
|
|
const xmux = out.xmux as Record<string, unknown>;
|
|
expect(xmux).not.toHaveProperty('maxConcurrency');
|
|
expect(xmux.maxConnections).toBe('8');
|
|
});
|
|
|
|
it('bare schema defaults keep only one exclusive xmux strategy non-zero', () => {
|
|
expect(XHttpXmuxSchema.parse({}).maxConnections).toBe(0);
|
|
expect(XHttpXmuxSchema.parse({}).maxConcurrency).toBe('16-32');
|
|
});
|
|
|
|
it('XMUX_FRESH_DEFAULTS seeds the core maxConnections fallback without a competing maxConcurrency', () => {
|
|
expect(XMUX_FRESH_DEFAULTS.maxConnections).toBe(3);
|
|
expect(XMUX_FRESH_DEFAULTS.maxConcurrency).toBe('');
|
|
|
|
const out = normalizeXhttpForWire({
|
|
path: '/app',
|
|
mode: 'stream-one',
|
|
enableXmux: true,
|
|
xmux: XMUX_FRESH_DEFAULTS,
|
|
}, 'outbound');
|
|
|
|
const xmux = out.xmux as Record<string, unknown>;
|
|
expect(xmux.maxConnections).toBe(3);
|
|
expect(xmux.maxConcurrency).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('normalizeSockoptForWire', () => {
|
|
it('omits doc-example defaults that throttle throughput', () => {
|
|
const out = normalizeSockoptForWire({
|
|
tcpWindowClamp: 0,
|
|
tcpMaxSeg: 0,
|
|
tcpUserTimeout: 0,
|
|
tcpFastOpen: true,
|
|
tcpcongestion: 'bbr',
|
|
domainStrategy: 'AsIs',
|
|
tproxy: 'off',
|
|
mark: 0,
|
|
});
|
|
|
|
expect(out).toEqual({
|
|
tcpFastOpen: true,
|
|
tcpcongestion: 'bbr',
|
|
});
|
|
});
|
|
|
|
it('preserves happyEyeballs on freedom-style outbound', () => {
|
|
const out = normalizeSockoptForWire({
|
|
domainStrategy: 'UseIP',
|
|
happyEyeballs: {
|
|
tryDelayMs: 150,
|
|
prioritizeIPv6: true,
|
|
interleave: 1,
|
|
maxConcurrentTry: 4,
|
|
},
|
|
});
|
|
|
|
expect(out?.happyEyeballs).toMatchObject({
|
|
tryDelayMs: 150,
|
|
prioritizeIPv6: true,
|
|
});
|
|
expect(out?.domainStrategy).toBe('UseIP');
|
|
});
|
|
|
|
it('keeps a freshly toggled happyEyeballs (schema defaults) across the wire round trip', () => {
|
|
const out = normalizeSockoptForWire({
|
|
happyEyeballs: HappyEyeballsSchema.parse({}),
|
|
});
|
|
|
|
expect(out?.happyEyeballs).toEqual({ tryDelayMs: 250 });
|
|
});
|
|
|
|
it('keeps an explicit tryDelayMs of 0 so it cannot rehydrate as the 250 default', () => {
|
|
const out = normalizeSockoptForWire({
|
|
happyEyeballs: { tryDelayMs: 0, prioritizeIPv6: true, interleave: 1, maxConcurrentTry: 4 },
|
|
});
|
|
|
|
expect(out?.happyEyeballs).toEqual({ tryDelayMs: 0, prioritizeIPv6: true });
|
|
});
|
|
});
|
|
|
|
describe('normalizeStreamSettingsForWire reality', () => {
|
|
it('preserves the nested client settings on inbound (share links read publicKey from there)', () => {
|
|
const out = normalizeStreamSettingsForWire({
|
|
network: 'xhttp',
|
|
security: 'reality',
|
|
realitySettings: {
|
|
target: 'play.google.com:443',
|
|
privateKey: 'priv',
|
|
serverNames: ['play.google.com'],
|
|
shortIds: ['abcd'],
|
|
settings: {
|
|
publicKey: 'pub',
|
|
fingerprint: 'chrome',
|
|
spiderX: '/',
|
|
},
|
|
},
|
|
}, { side: 'inbound' });
|
|
|
|
const reality = out.realitySettings as Record<string, unknown>;
|
|
expect(reality.target).toBe('play.google.com:443');
|
|
expect(reality.privateKey).toBe('priv');
|
|
const settings = reality.settings as Record<string, unknown>;
|
|
expect(settings.publicKey).toBe('pub');
|
|
expect(settings.spiderX).toBe('/');
|
|
});
|
|
|
|
it('passes client realitySettings through unchanged on outbound', () => {
|
|
const out = normalizeStreamSettingsForWire({
|
|
network: 'xhttp',
|
|
security: 'reality',
|
|
realitySettings: {
|
|
publicKey: 'pub',
|
|
fingerprint: 'chrome',
|
|
serverName: 'play.google.com',
|
|
shortId: 'abcd',
|
|
spiderX: '/x',
|
|
},
|
|
}, { side: 'outbound' });
|
|
|
|
const reality = out.realitySettings as Record<string, unknown>;
|
|
expect(reality.publicKey).toBe('pub');
|
|
expect(reality.serverName).toBe('play.google.com');
|
|
expect(reality.spiderX).toBe('/x');
|
|
});
|
|
});
|
|
|
|
describe('normalizeStreamSettingsForWire tls', () => {
|
|
it('drops empty uTLS fingerprints from inbound and outbound TLS shapes', () => {
|
|
const out = normalizeStreamSettingsForWire({
|
|
network: 'hysteria',
|
|
security: 'tls',
|
|
tlsSettings: {
|
|
fingerprint: '',
|
|
settings: {
|
|
fingerprint: '',
|
|
echConfigList: '',
|
|
},
|
|
},
|
|
}, { side: 'inbound' });
|
|
|
|
const tls = out.tlsSettings as Record<string, unknown>;
|
|
const settings = tls.settings as Record<string, unknown>;
|
|
expect(tls).not.toHaveProperty('fingerprint');
|
|
expect(settings).not.toHaveProperty('fingerprint');
|
|
expect(settings.echConfigList).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('inbound formValuesToWirePayload integration', () => {
|
|
it('emits lean stream-one xhttp + sockopt on save', () => {
|
|
const values = {
|
|
remark: 't',
|
|
enable: true,
|
|
port: 443,
|
|
listen: '0.0.0.0',
|
|
tag: 'in-443',
|
|
expiryTime: 0,
|
|
sniffing: { enabled: false },
|
|
up: 0,
|
|
down: 0,
|
|
total: 0,
|
|
trafficReset: 'never',
|
|
lastTrafficResetTime: 0,
|
|
nodeId: null,
|
|
protocol: 'vless',
|
|
settings: { clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }], decryption: 'none' },
|
|
streamSettings: {
|
|
network: 'xhttp',
|
|
security: 'reality',
|
|
realitySettings: {
|
|
target: 'play.google.com:443',
|
|
privateKey: 'priv',
|
|
serverNames: ['play.google.com'],
|
|
shortIds: ['44003d86dc1e'],
|
|
settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
|
|
},
|
|
xhttpSettings: {
|
|
path: '/app',
|
|
host: 'play.google.com',
|
|
mode: 'stream-one',
|
|
xPaddingBytes: '100-1000',
|
|
scMaxEachPostBytes: '1000000',
|
|
scMinPostsIntervalMs: '30',
|
|
enableXmux: false,
|
|
},
|
|
sockopt: {
|
|
tcpWindowClamp: 0,
|
|
tcpMaxSeg: 0,
|
|
tcpUserTimeout: 0,
|
|
tcpFastOpen: true,
|
|
tcpcongestion: 'bbr',
|
|
},
|
|
},
|
|
} as InboundFormValues;
|
|
|
|
const payload = formValuesToWirePayload(values);
|
|
const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
|
|
const xhttp = stream.xhttpSettings as Record<string, unknown>;
|
|
const sockopt = stream.sockopt as Record<string, unknown>;
|
|
const reality = stream.realitySettings as Record<string, unknown>;
|
|
|
|
expect(xhttp).not.toHaveProperty('scMaxEachPostBytes');
|
|
expect(sockopt).not.toHaveProperty('tcpWindowClamp');
|
|
expect(sockopt.tcpFastOpen).toBe(true);
|
|
const realitySettings = reality.settings as Record<string, unknown>;
|
|
expect(realitySettings.publicKey).toBe('pub');
|
|
});
|
|
|
|
it('accepts Hysteria TLS with uTLS None and omits fingerprint on save', () => {
|
|
const values = {
|
|
remark: 'hy2',
|
|
enable: true,
|
|
port: 443,
|
|
listen: '',
|
|
tag: 'hy2-443',
|
|
expiryTime: 0,
|
|
sniffing: { enabled: false },
|
|
up: 0,
|
|
down: 0,
|
|
total: 0,
|
|
trafficReset: 'never',
|
|
lastTrafficResetTime: 0,
|
|
nodeId: null,
|
|
protocol: 'hysteria',
|
|
settings: { version: 2, clients: [] },
|
|
streamSettings: {
|
|
network: 'hysteria',
|
|
security: 'tls',
|
|
hysteriaSettings: {
|
|
version: 2,
|
|
auth: 'auth',
|
|
udpIdleTimeout: 60,
|
|
},
|
|
tlsSettings: {
|
|
alpn: ['h3'],
|
|
settings: {
|
|
fingerprint: '',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const parsed = InboundFormSchema.safeParse(values);
|
|
expect(parsed.success).toBe(true);
|
|
if (!parsed.success) throw parsed.error;
|
|
|
|
const payload = formValuesToWirePayload(parsed.data);
|
|
const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
|
|
const tls = stream.tlsSettings as Record<string, unknown>;
|
|
const settings = tls.settings as Record<string, unknown>;
|
|
expect(settings).not.toHaveProperty('fingerprint');
|
|
});
|
|
|
|
it('preserves non-default scMinPostsIntervalMs in packet-up inbound wire payload for subscriptions', () => {
|
|
const values = {
|
|
remark: 't',
|
|
enable: true,
|
|
port: 443,
|
|
listen: '0.0.0.0',
|
|
tag: 'in-443',
|
|
expiryTime: 0,
|
|
sniffing: { enabled: false },
|
|
up: 0,
|
|
down: 0,
|
|
total: 0,
|
|
trafficReset: 'never',
|
|
lastTrafficResetTime: 0,
|
|
nodeId: null,
|
|
protocol: 'vless',
|
|
settings: { clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }], decryption: 'none' },
|
|
streamSettings: {
|
|
network: 'xhttp',
|
|
security: 'reality',
|
|
realitySettings: {
|
|
target: 'play.google.com:443',
|
|
privateKey: 'priv',
|
|
serverNames: ['play.google.com'],
|
|
shortIds: ['44003d86dc1e'],
|
|
settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
|
|
},
|
|
xhttpSettings: {
|
|
path: '/app',
|
|
host: 'play.google.com',
|
|
mode: 'packet-up',
|
|
scMinPostsIntervalMs: '50-150',
|
|
},
|
|
sockopt: {},
|
|
},
|
|
};
|
|
|
|
const parsed = InboundFormSchema.safeParse(values);
|
|
expect(parsed.success).toBe(true);
|
|
if (!parsed.success) throw parsed.error;
|
|
|
|
const payload = formValuesToWirePayload(parsed.data);
|
|
const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
|
|
const xhttp = stream.xhttpSettings as Record<string, unknown>;
|
|
|
|
expect(xhttp.scMinPostsIntervalMs).toBe('50-150');
|
|
});
|
|
|
|
it('strips default scMinPostsIntervalMs=30 from inbound wire payload', () => {
|
|
const values = {
|
|
remark: 't',
|
|
enable: true,
|
|
port: 443,
|
|
listen: '0.0.0.0',
|
|
tag: 'in-443',
|
|
expiryTime: 0,
|
|
sniffing: { enabled: false },
|
|
up: 0,
|
|
down: 0,
|
|
total: 0,
|
|
trafficReset: 'never',
|
|
lastTrafficResetTime: 0,
|
|
nodeId: null,
|
|
protocol: 'vless',
|
|
settings: { clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }], decryption: 'none' },
|
|
streamSettings: {
|
|
network: 'xhttp',
|
|
security: 'reality',
|
|
realitySettings: {
|
|
target: 'play.google.com:443',
|
|
privateKey: 'priv',
|
|
serverNames: ['play.google.com'],
|
|
shortIds: ['44003d86dc1e'],
|
|
settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
|
|
},
|
|
xhttpSettings: {
|
|
path: '/app',
|
|
host: 'play.google.com',
|
|
mode: 'packet-up',
|
|
scMinPostsIntervalMs: '30',
|
|
},
|
|
sockopt: {},
|
|
},
|
|
};
|
|
|
|
const parsed = InboundFormSchema.safeParse(values);
|
|
expect(parsed.success).toBe(true);
|
|
if (!parsed.success) throw parsed.error;
|
|
|
|
const payload = formValuesToWirePayload(parsed.data);
|
|
const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
|
|
const xhttp = stream.xhttpSettings as Record<string, unknown>;
|
|
|
|
expect(xhttp).not.toHaveProperty('scMinPostsIntervalMs');
|
|
});
|
|
});
|
|
|
|
describe('freedom outbound sockopt wire payload', () => {
|
|
it('preserves happyEyeballs on direct freedom outbound', () => {
|
|
const wire = outboundToWire({
|
|
protocol: 'freedom',
|
|
tag: 'direct',
|
|
settings: { domainStrategy: 'UseIP' },
|
|
streamSettings: {
|
|
sockopt: {
|
|
domainStrategy: 'UseIP',
|
|
happyEyeballs: {
|
|
tryDelayMs: 150,
|
|
prioritizeIPv6: true,
|
|
interleave: 1,
|
|
maxConcurrentTry: 4,
|
|
},
|
|
},
|
|
},
|
|
} as Parameters<typeof outboundToWire>[0]);
|
|
|
|
expect(wire.streamSettings).toMatchObject({
|
|
sockopt: {
|
|
domainStrategy: 'UseIP',
|
|
happyEyeballs: {
|
|
tryDelayMs: 150,
|
|
prioritizeIPv6: true,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|