Files
3x-ui/frontend/src/test/inbound-defaults.test.ts
T
MHSanaei 9c8cd08f90 feat(wireguard): multi-client support
WireGuard inbounds now manage per-client peers using xray-core's native WireGuard users (AddUser/RemoveUser). Each client lives in settings.clients (canonical, like every other protocol) and is projected to peers[] only when emitting the xray config, at level 0 so the dispatcher's per-user traffic/online counters work with no extra plumbing.

Backend: internal/util/wireguard gains KeyToHex (base64 to hex for the gRPC path), PublicKeyFromPrivate and GenerateWireguardPSK; xray/api.go builds a wireguard account in AddUser with hex keys (RemoveUser already worked); client CRUD generates a keypair and allocates a unique tunnel address per client and never rotates keys on edit; an idempotent migration converts legacy settings.peers into managed clients; WireGuard is included in the raw subscription.

Frontend: WireGuard in the add-client modal with keys on the credential tab, client schema, per-client QR/link/.conf, inbound form reduced to server settings; i18n added across 13 locales.

Fix: guard the settings[clients] assertion in add/update so a legacy WireGuard inbound stored without a clients key no longer panics.
2026-06-28 00:44:38 +02:00

167 lines
6.0 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 { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-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.allowTransparent).toBe(false);
const accounts = s.accounts ?? [];
expect(accounts).toHaveLength(1);
expect(accounts[0].user.length).toBe(8);
expect(accounts[0].pass.length).toBe(12);
expect(HttpInboundSettingsSchema.parse(s)).toEqual(s);
});
it('mixed', () => {
const s = createDefaultMixedInboundSettings();
expect(s.auth).toBe('password');
expect(s.udp).toBe(false);
expect(s.ip).toBe('127.0.0.1');
const accounts = s.accounts ?? [];
expect(accounts).toHaveLength(1);
expect(accounts[0].user.length).toBe(8);
expect(accounts[0].pass.length).toBe(12);
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=',
});
expect(s).toMatchSnapshot();
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
expect(s.peers).toEqual([]);
expect(s.clients).toEqual([]);
});
});
describe('createHysteriaTlsSettingsWithDefaultCert', () => {
it('defaults Hysteria TLS to uTLS None and h3 ALPN', () => {
const tls = createHysteriaTlsSettingsWithDefaultCert();
expect(tls.alpn).toEqual(['h3']);
expect((tls.settings as Record<string, unknown>).fingerprint).toBe('');
expect(tls.certificates).toEqual([
expect.objectContaining({
useFile: true,
certificateFile: '',
keyFile: '',
}),
]);
});
});