Jelajahi Sumber

refactor(frontend): extract createDefault*Client factories to lib/xray

Next Step 3d slice. Five plain-object factories — Vless, Vmess, Trojan,
Shadowsocks, Hysteria — replace the legacy
`new Inbound.<Protocol>Settings.<Protocol>(...)` 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.
MHSanaei 1 hari lalu
induk
melakukan
8d5d11cafc

+ 122 - 0
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.<Settings>.<Client>()` 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),
+  };
+}

+ 79 - 0
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": "[email protected]",
+  "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": "[email protected]",
+  "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": "[email protected]",
+  "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": "[email protected]",
+  "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": "[email protected]",
+  "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,
+}
+`;

+ 67 - 0
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: '[email protected]',
+  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);
+  });
+});