mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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.
This commit is contained in:
parent
922a442264
commit
8d5d11cafc
3 changed files with 268 additions and 0 deletions
122
frontend/src/lib/xray/inbound-defaults.ts
Normal file
122
frontend/src/lib/xray/inbound-defaults.ts
Normal file
|
|
@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
`;
|
||||||
67
frontend/src/test/inbound-defaults.test.ts
Normal file
67
frontend/src/test/inbound-defaults.test.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue