Files
3x-ui/frontend/src/test/inbound-link.test.ts
MHSanaei 79c076ee11 refactor(frontend): extract genVlessLink to lib/xray/inbound-link
Second link generator. genVlessLink builds the
vless://<uuid>@<host>:<port>?<query>#<remark> share URL from a typed
Inbound + client args, dispatching on streamSettings.network for the
network-specific knobs and on streamSettings.security for the
TLS/Reality knobs. Three param-style helpers move alongside the obj-
style ones already in this file:

  - applyXhttpExtraToParams — writes path/host/mode/x_padding_bytes and
    the JSON extra blob into URLSearchParams
  - applyFinalMaskToParams — writes the fm payload when shareable
  - applyExternalProxyTLSParams — overrides sni/fp/alpn when an external
    proxy entry is supplied and security is tls

A vless-tcp-reality fixture lands alongside the existing vless-ws-tls
one, so the parity test now exercises both security branches.

Discovered a latent legacy bug while writing parity: the old class
stored realitySettings.serverNames as a comma-joined string and gated
SNI on `!ObjectUtil.isArrEmpty(serverNames)`, which always returns true
for strings — so SNI was never written into Reality share URLs.
Existing clients rely on the omission (they pull SNI from
realitySettings.target instead). We preserve the omission here to keep
this extraction byte-stable; an inline comment marks the spot for a
separate intentional fix.

Suite: 70 tests across 8 files; typecheck + lint clean.
2026-05-26 00:15:03 +02:00

99 lines
3.4 KiB
TypeScript

/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { genVlessLink, genVmessLink } from '@/lib/xray/inbound-link';
import { Inbound as LegacyInbound } from '@/models/inbound';
import { InboundSchema } from '@/schemas/api/inbound';
// Parity harness for the share-link extraction. For each full inbound
// fixture matching the protocol under test, we:
// 1. Parse with the Zod InboundSchema -> typed input for the new pure fn
// 2. Construct the legacy Inbound class via Inbound.fromJson(fixture)
// 3. Call both link generators with matching args
// 4. Assert the URLs match byte-for-byte
// Drift between the new pure fn and the legacy class method fails the
// test here, before the call sites in pages/ get swapped.
const fullFixtures = import.meta.glob<unknown>(
'./golden/fixtures/inbound-full/*.json',
{ eager: true, import: 'default' },
);
function fixtureName(path: string): string {
const file = path.split('/').pop() ?? path;
return file.replace(/\.json$/, '');
}
function fixturesForProtocol(protocol: string): Array<[string, Record<string, unknown>]> {
return Object.entries(fullFixtures)
.filter(([, raw]) => (raw as { protocol?: string }).protocol === protocol)
.map(([path, raw]): [string, Record<string, unknown>] => [fixtureName(path), raw as Record<string, unknown>])
.sort(([a], [b]) => a.localeCompare(b));
}
describe('genVmessLink parity', () => {
const fixtures = fixturesForProtocol('vmess');
expect(fixtures.length, 'need at least one vmess full-inbound fixture').toBeGreaterThan(0);
for (const [name, raw] of fixtures) {
it(`${name}: matches legacy Inbound.genVmessLink`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients: Array<{ id: string; security?: string }> } }).settings;
const client = settings.clients[0];
const address = 'example.test';
const port = typed.port;
const remark = 'parity-test';
const newLink = genVmessLink({
inbound: typed,
address,
port,
forceTls: 'same',
remark,
clientId: client.id,
security: client.security as never,
externalProxy: null,
});
const legacy = LegacyInbound.fromJson(raw);
const legacyLink = legacy.genVmessLink(address, port, 'same', remark, client.id, client.security, null);
expect(newLink).toBe(legacyLink);
});
}
});
describe('genVlessLink parity', () => {
const fixtures = fixturesForProtocol('vless');
expect(fixtures.length, 'need at least one vless full-inbound fixture').toBeGreaterThan(0);
for (const [name, raw] of fixtures) {
it(`${name}: matches legacy Inbound.genVLESSLink`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients: Array<{ id: string; flow?: string }> } }).settings;
const client = settings.clients[0];
const address = 'example.test';
const port = typed.port;
const remark = 'parity-test';
const newLink = genVlessLink({
inbound: typed,
address,
port,
forceTls: 'same',
remark,
clientId: client.id,
flow: client.flow as never,
externalProxy: null,
});
const legacy = LegacyInbound.fromJson(raw);
const legacyLink = legacy.genVLESSLink(address, port, 'same', remark, client.id, client.flow, null);
expect(newLink).toBe(legacyLink);
});
}
});