Files
3x-ui/frontend/src/test/inbound-defaults.test.ts
MHSanaei 5a90f7e348 refactor(frontend): align hysteria with new docs + drop hysteria2 protocol
Phase 2 smoke fixes on the Inbound add flow surfaced that hysteria2 was
modeled as a separate top-level protocol when it's really just hysteria
v2. The xray transports/hysteria.html docs also pin the hysteria stream
to a minimal shape (version/auth/udpIdleTimeout/masquerade) — the
previous schema carried legacy congestion/up/down/udphop/window knobs
that aren't part of the wire contract.

Hysteria2 removal:
- Drop 'hysteria2' from ProtocolSchema enum and Protocols const
- Drop hysteria2 branches from inbound/outbound discriminated unions
- Drop createDefaultHysteria2InboundSettings / OutboundSettings
- Delete schemas/protocols/inbound/hysteria2.ts and outbound/hysteria2.ts
- Drop hysteria2 case in getInboundClients / genLink (fell through to
  the hysteria handler anyway)
- Update client form modals' MULTI_CLIENT_PROTOCOLS sets
- Remove hysteria2-basic fixture + snapshot entries (14 capability
  cases, 1 protocols fixture, 1 inbound-defaults factory)
- Keep parseHysteria2Link() outbound parser since hysteria2:// is the
  share-link URI prefix for hysteria v2

Hysteria stream alignment with xtls docs:
- HysteriaStreamSettingsSchema reduced to version/auth/udpIdleTimeout/
  masquerade per transports/hysteria.html
- Masquerade type adds '' (default 404 page) and defaults to it
- Outbound form drops Congestion/Upload/Download/UDP hop/Max idle/
  Keep alive/Disable Path MTU controls and the receive-window note
- newStreamSlice('hysteria') in OutboundFormModal mirrors the trimmed
  shape; outbound-link-parser emits the trimmed shape too
- InboundFormModal Masquerade Select gains the default option

New TUN inbound schema:
- Add schemas/protocols/inbound/tun.ts with name/mtu/gateway/dns/
  userLevel/autoSystemRoutingTable/autoOutboundsInterface
- Wire into ProtocolSchema enum, InboundSettingsSchema discriminated
  union, createDefaultInboundSettings dispatcher

Other Phase 2 smoke fixes folded in:
- Tunnel portMap UI swaps Form.List for HeaderMapEditor v1 — wire
  shape is Record<string,string> and the List was producing arrays
- Hysteria onValuesChange seeds full TLS schema defaults + one
  empty certificate row (Cipher Suites/Min/Max Version/uTLS/ALPN
  were undefined before)
- HTTP/Mixed accounts Add button auto-fills user/pass with
  RandomUtil.randomLowerAndNum
- Hysteria security tab gates the 'none' radio out — TLS only
- Hysteria stream tab drops the inbound Auth password field (xray
  inbound auth is per-user via 'users', not stream-level)
- Reality onSecurityChange auto-randomizes target/serverNames/
  shortIds and fetches an X25519 keypair
- Tag and DB-side fields (up/down/total/expiryTime/
  lastTrafficResetTime/clientStats/security) gain hidden Form.Items
  so validateFields keeps them in the wire payload (rc-component
  form strips unregistered fields)
- WireGuard inbound auto-seeds one peer with generated keypair,
  allowedIPs ['10.0.0.2/32'], keepAlive 0 — matches legacy
- WireGuard peer rows separated by Divider with the Peer N title
  and a small inline remove button (titlePlacement="center")
2026-05-26 17:49:37 +02:00

140 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
createDefaultHttpInboundSettings,
createDefaultHysteriaClient,
createDefaultHysteriaInboundSettings,
createDefaultMixedInboundSettings,
createDefaultShadowsocksClient,
createDefaultShadowsocksInboundSettings,
createDefaultTrojanClient,
createDefaultTrojanInboundSettings,
createDefaultTunnelInboundSettings,
createDefaultVlessClient,
createDefaultVlessInboundSettings,
createDefaultVmessClient,
createDefaultVmessInboundSettings,
createDefaultWireguardInboundSettings,
} from '@/lib/xray/inbound-defaults';
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
import { ShadowsocksClientSchema, ShadowsocksInboundSettingsSchema } from '@/schemas/protocols/inbound/shadowsocks';
import { TrojanClientSchema, TrojanInboundSettingsSchema } from '@/schemas/protocols/inbound/trojan';
import { TunnelInboundSettingsSchema } from '@/schemas/protocols/inbound/tunnel';
import { VlessClientSchema, VlessInboundSettingsSchema } from '@/schemas/protocols/inbound/vless';
import { VmessClientSchema, VmessInboundSettingsSchema } from '@/schemas/protocols/inbound/vmess';
import { WireguardInboundSettingsSchema } from '@/schemas/protocols/inbound/wireguard';
// Tests pass explicit seeds for every random field so the assertions don't
// depend on window.crypto (the node test env has no crypto.randomUUID).
// Each factory is verified two ways:
// 1. snapshot — locks the exact shape
// 2. Zod parse round-trip — confirms the factory output is a valid
// member of the protocol's client schema (no missing defaults, no
// stray fields)
const seed = {
email: 'fixture@example.test',
subId: 'fixed-sub-id-1234',
};
describe('createDefaultVlessClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultVlessClient({ ...seed, id: '11111111-2222-4333-8444-555555555555' });
expect(c).toMatchSnapshot();
expect(VlessClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultVmessClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultVmessClient({ ...seed, id: 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' });
expect(c).toMatchSnapshot();
expect(VmessClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultTrojanClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultTrojanClient({ ...seed, password: 'fixed-trojan-pw' });
expect(c).toMatchSnapshot();
expect(TrojanClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultShadowsocksClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultShadowsocksClient({ ...seed, password: 'ZmFrZS1zcy1wYXNzd29yZA==' });
expect(c).toMatchSnapshot();
expect(ShadowsocksClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefaultHysteriaClient', () => {
it('produces a Zod-valid client', () => {
const c = createDefaultHysteriaClient({ ...seed, auth: 'fixed-hyst-auth' });
expect(c).toMatchSnapshot();
expect(HysteriaClientSchema.parse(c)).toEqual(c);
});
});
describe('createDefault*InboundSettings factories', () => {
it('vless', () => {
const s = createDefaultVlessInboundSettings();
expect(s).toMatchSnapshot();
expect(VlessInboundSettingsSchema.parse(s)).toEqual(s);
});
it('vmess', () => {
const s = createDefaultVmessInboundSettings();
expect(s).toMatchSnapshot();
expect(VmessInboundSettingsSchema.parse(s)).toEqual(s);
});
it('trojan', () => {
const s = createDefaultTrojanInboundSettings();
expect(s).toMatchSnapshot();
expect(TrojanInboundSettingsSchema.parse(s)).toEqual(s);
});
it('shadowsocks', () => {
const s = createDefaultShadowsocksInboundSettings({ password: 'ZmFrZS1zcy1zZWVk' });
expect(s).toMatchSnapshot();
expect(ShadowsocksInboundSettingsSchema.parse(s)).toEqual(s);
});
it('hysteria (v1, defaults to v2 wire version)', () => {
const s = createDefaultHysteriaInboundSettings();
expect(s).toMatchSnapshot();
expect(HysteriaInboundSettingsSchema.parse(s)).toEqual(s);
});
it('http', () => {
const s = createDefaultHttpInboundSettings();
expect(s).toMatchSnapshot();
expect(HttpInboundSettingsSchema.parse(s)).toEqual(s);
});
it('mixed', () => {
const s = createDefaultMixedInboundSettings();
expect(s).toMatchSnapshot();
expect(MixedInboundSettingsSchema.parse(s)).toEqual(s);
});
it('tunnel', () => {
const s = createDefaultTunnelInboundSettings();
expect(s).toMatchSnapshot();
expect(TunnelInboundSettingsSchema.parse(s)).toEqual(s);
});
it('wireguard', () => {
const s = createDefaultWireguardInboundSettings({
secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
peerPrivateKey: 'cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==',
});
expect(s).toMatchSnapshot();
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
});
});