From 8d5d11cafc4cf7f0ad4c77f9357a97d98824d171 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 25 May 2026 23:42:30 +0200 Subject: [PATCH] refactor(frontend): extract createDefault*Client factories to lib/xray MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next Step 3d slice. Five plain-object factories — Vless, Vmess, Trojan, Shadowsocks, Hysteria — replace the legacy `new Inbound.Settings.(...)` constructor chain and the ClientBase XrayCommonClass machinery. Each factory takes an optional seed; missing random fields (id, password, auth, email, subId) fall through to RandomUtil at call time. Forms can hand-pick a UUID; tests pass deterministic seeds so the suite never touches window.crypto. Tests double-verify each factory: a snapshot locks the exact shape, and the matching Zod ClientSchema.parse(out) must equal `out` — no missing defaults, no stray fields, type-narrowed end-to-end. Discovered: VmessClientSchema and VlessClientSchema enforce z.uuid() format, so the test seeds use real-shape UUIDs. Suite: 49 tests across 6 files; typecheck + lint clean. Outbound and inbound-settings factories follow in subsequent turns alongside the toShareLink extraction. --- frontend/src/lib/xray/inbound-defaults.ts | 122 ++++++++++++++++++ .../inbound-defaults.test.ts.snap | 79 ++++++++++++ frontend/src/test/inbound-defaults.test.ts | 67 ++++++++++ 3 files changed, 268 insertions(+) create mode 100644 frontend/src/lib/xray/inbound-defaults.ts create mode 100644 frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap create mode 100644 frontend/src/test/inbound-defaults.test.ts diff --git a/frontend/src/lib/xray/inbound-defaults.ts b/frontend/src/lib/xray/inbound-defaults.ts new file mode 100644 index 00000000..a3ec5df2 --- /dev/null +++ b/frontend/src/lib/xray/inbound-defaults.ts @@ -0,0 +1,122 @@ +import { RandomUtil } from '@/utils'; + +import type { HysteriaClient } from '@/schemas/protocols/inbound/hysteria'; +import type { ShadowsocksClient } from '@/schemas/protocols/inbound/shadowsocks'; +import type { TrojanClient } from '@/schemas/protocols/inbound/trojan'; +import type { VlessClient } from '@/schemas/protocols/inbound/vless'; +import type { VmessClient } from '@/schemas/protocols/inbound/vmess'; + +// Plain-object factories for protocol clients. Each returns a Zod-parsable +// object matching the wire shape. Random fields (id, password, auth, +// email, subId) call RandomUtil at invocation time — pass them in +// `overrides` for deterministic tests or for forms that pre-seed values. +// +// These replace the legacy `new Inbound..()` constructors +// and the Inbound.ClientBase machinery. Callers no longer carry the +// XrayCommonClass dependency once the swap lands. + +interface ClientBaseSeed { + email?: string; + subId?: string; + limitIp?: number; + totalGB?: number; + expiryTime?: number; + enable?: boolean; + tgId?: number; + comment?: string; + reset?: number; +} + +interface ClientBase { + email: string; + limitIp: number; + totalGB: number; + expiryTime: number; + enable: boolean; + tgId: number; + subId: string; + comment: string; + reset: number; +} + +function clientBase(seed: ClientBaseSeed = {}): ClientBase { + return { + email: seed.email ?? RandomUtil.randomLowerAndNum(8), + limitIp: seed.limitIp ?? 0, + totalGB: seed.totalGB ?? 0, + expiryTime: seed.expiryTime ?? 0, + enable: seed.enable ?? true, + tgId: seed.tgId ?? 0, + subId: seed.subId ?? RandomUtil.randomLowerAndNum(16), + comment: seed.comment ?? '', + reset: seed.reset ?? 0, + }; +} + +export interface VlessClientSeed extends ClientBaseSeed { + id?: string; + flow?: VlessClient['flow']; +} + +export function createDefaultVlessClient(seed: VlessClientSeed = {}): VlessClient { + return { + id: seed.id ?? RandomUtil.randomUUID(), + flow: seed.flow ?? '', + ...clientBase(seed), + }; +} + +export interface VmessClientSeed extends ClientBaseSeed { + id?: string; + security?: VmessClient['security']; +} + +export function createDefaultVmessClient(seed: VmessClientSeed = {}): VmessClient { + return { + id: seed.id ?? RandomUtil.randomUUID(), + security: seed.security ?? 'auto', + ...clientBase(seed), + }; +} + +export interface TrojanClientSeed extends ClientBaseSeed { + password?: string; +} + +export function createDefaultTrojanClient(seed: TrojanClientSeed = {}): TrojanClient { + return { + password: seed.password ?? RandomUtil.randomSeq(10), + ...clientBase(seed), + }; +} + +export interface ShadowsocksClientSeed extends ClientBaseSeed { + method?: string; + password?: string; + ssMethod?: string; +} + +// Shadowsocks clients ship with an empty `method` on single-user inbounds +// (the parent inbound's method is authoritative); only 2022-blake3 multi- +// user inbounds use the per-client method. Callers pass `ssMethod` to seed +// a method-specific password length when creating a multi-user client. +export function createDefaultShadowsocksClient(seed: ShadowsocksClientSeed = {}): ShadowsocksClient { + const method = seed.method ?? ''; + const password = seed.password ?? RandomUtil.randomShadowsocksPassword(seed.ssMethod ?? '2022-blake3-aes-256-gcm'); + return { + method, + password, + ...clientBase(seed), + }; +} + +export interface HysteriaClientSeed extends ClientBaseSeed { + auth?: string; +} + +export function createDefaultHysteriaClient(seed: HysteriaClientSeed = {}): HysteriaClient { + return { + auth: seed.auth ?? RandomUtil.randomSeq(10), + ...clientBase(seed), + }; +} diff --git a/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap b/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap new file mode 100644 index 00000000..95a44adf --- /dev/null +++ b/frontend/src/test/__snapshots__/inbound-defaults.test.ts.snap @@ -0,0 +1,79 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`createDefaultHysteriaClient > produces a Zod-valid client 1`] = ` +{ + "auth": "fixed-hyst-auth", + "comment": "", + "email": "fixture@example.test", + "enable": true, + "expiryTime": 0, + "limitIp": 0, + "reset": 0, + "subId": "fixed-sub-id-1234", + "tgId": 0, + "totalGB": 0, +} +`; + +exports[`createDefaultShadowsocksClient > produces a Zod-valid client 1`] = ` +{ + "comment": "", + "email": "fixture@example.test", + "enable": true, + "expiryTime": 0, + "limitIp": 0, + "method": "", + "password": "ZmFrZS1zcy1wYXNzd29yZA==", + "reset": 0, + "subId": "fixed-sub-id-1234", + "tgId": 0, + "totalGB": 0, +} +`; + +exports[`createDefaultTrojanClient > produces a Zod-valid client 1`] = ` +{ + "comment": "", + "email": "fixture@example.test", + "enable": true, + "expiryTime": 0, + "limitIp": 0, + "password": "fixed-trojan-pw", + "reset": 0, + "subId": "fixed-sub-id-1234", + "tgId": 0, + "totalGB": 0, +} +`; + +exports[`createDefaultVlessClient > produces a Zod-valid client 1`] = ` +{ + "comment": "", + "email": "fixture@example.test", + "enable": true, + "expiryTime": 0, + "flow": "", + "id": "11111111-2222-4333-8444-555555555555", + "limitIp": 0, + "reset": 0, + "subId": "fixed-sub-id-1234", + "tgId": 0, + "totalGB": 0, +} +`; + +exports[`createDefaultVmessClient > produces a Zod-valid client 1`] = ` +{ + "comment": "", + "email": "fixture@example.test", + "enable": true, + "expiryTime": 0, + "id": "aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee", + "limitIp": 0, + "reset": 0, + "security": "auto", + "subId": "fixed-sub-id-1234", + "tgId": 0, + "totalGB": 0, +} +`; diff --git a/frontend/src/test/inbound-defaults.test.ts b/frontend/src/test/inbound-defaults.test.ts new file mode 100644 index 00000000..f99e9a17 --- /dev/null +++ b/frontend/src/test/inbound-defaults.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest'; + +import { + createDefaultHysteriaClient, + createDefaultShadowsocksClient, + createDefaultTrojanClient, + createDefaultVlessClient, + createDefaultVmessClient, +} from '@/lib/xray/inbound-defaults'; +import { HysteriaClientSchema } from '@/schemas/protocols/inbound/hysteria'; +import { ShadowsocksClientSchema } from '@/schemas/protocols/inbound/shadowsocks'; +import { TrojanClientSchema } from '@/schemas/protocols/inbound/trojan'; +import { VlessClientSchema } from '@/schemas/protocols/inbound/vless'; +import { VmessClientSchema } from '@/schemas/protocols/inbound/vmess'; + +// 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); + }); +});