///
import { describe, expect, it } from 'vitest';
import {
genHysteriaLink,
genInboundLinks,
genShadowsocksLink,
genTrojanLink,
genVlessLink,
genVmessLink,
genWireguardConfig,
genWireguardLink,
resolveAddr,
} from '@/lib/xray/inbound-link';
import { InboundSchema } from '@/schemas/api/inbound';
import type { WireguardInboundSettings } from '@/schemas/protocols/inbound/wireguard';
// Snapshot baseline for the share-link generators. Snapshots were locked
// at the close of the legacy class migration — at that point each
// generator was verified byte-equal to the corresponding legacy Inbound
// class method. Future drift past this baseline is a regression.
const fullFixtures = import.meta.glob(
'./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]> {
return Object.entries(fullFixtures)
.filter(([, raw]) => (raw as { protocol?: string }).protocol === protocol)
.map(([path, raw]): [string, Record] => [fixtureName(path), raw as Record])
.sort(([a], [b]) => a.localeCompare(b));
}
describe('genVmessLink', () => {
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}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients: Array<{ id: string; security?: string }> } }).settings;
const client = settings.clients[0];
const link = genVmessLink({
inbound: typed,
address: 'example.test',
port: typed.port,
forceTls: 'same',
remark: 'parity-test',
clientId: client.id,
security: client.security as never,
externalProxy: null,
});
expect(link).toMatchSnapshot();
});
}
});
describe('genVlessLink', () => {
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}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients: Array<{ id: string; flow?: string }> } }).settings;
const client = settings.clients[0];
const link = genVlessLink({
inbound: typed,
address: 'example.test',
port: typed.port,
forceTls: 'same',
remark: 'parity-test',
clientId: client.id,
flow: client.flow as never,
externalProxy: null,
});
expect(link).toMatchSnapshot();
});
}
});
describe('genTrojanLink', () => {
const fixtures = fixturesForProtocol('trojan');
expect(fixtures.length, 'need at least one trojan full-inbound fixture').toBeGreaterThan(0);
for (const [name, raw] of fixtures) {
it(`${name}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients: Array<{ password: string }> } }).settings;
const client = settings.clients[0];
const link = genTrojanLink({
inbound: typed,
address: 'example.test',
port: typed.port,
forceTls: 'same',
remark: 'parity-test',
clientPassword: client.password,
externalProxy: null,
});
expect(link).toMatchSnapshot();
});
}
});
describe('genHysteriaLink', () => {
const fixtures = fixturesForProtocol('hysteria');
expect(fixtures.length, 'need at least one hysteria full-inbound fixture').toBeGreaterThan(0);
for (const [name, raw] of fixtures) {
it(`${name}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients: Array<{ auth: string }> } }).settings;
const client = settings.clients[0];
const link = genHysteriaLink({
inbound: typed,
address: 'example.test',
port: typed.port,
remark: 'parity-test',
clientAuth: client.auth,
});
expect(link).toMatchSnapshot();
});
}
});
describe('genWireguardLink + genWireguardConfig', () => {
const fixtures = fixturesForProtocol('wireguard');
expect(fixtures.length, 'need at least one wireguard full-inbound fixture').toBeGreaterThan(0);
for (const [name, raw] of fixtures) {
it(`${name}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
if (typed.protocol !== 'wireguard') throw new Error('not a wireguard fixture');
// InboundSchema is an intersection of two DUs, so TS can't auto-narrow
// `settings` from `protocol`. The runtime guard above is the real
// check; this cast just helps the type checker.
const settings = typed.settings as WireguardInboundSettings;
const link = genWireguardLink({
settings,
address: 'wg.example.test',
port: typed.port,
remark: 'wg-peer-1',
peerIndex: 0,
});
const config = genWireguardConfig({
settings,
address: 'wg.example.test',
port: typed.port,
remark: 'wg-peer-1',
peerIndex: 0,
});
expect({ link, config }).toMatchSnapshot();
});
}
});
describe('resolveAddr precedence', () => {
const baseInbound = {
listen: '',
port: 443,
protocol: 'vless' as const,
};
it('prefers hostOverride over listen and fallback', () => {
expect(resolveAddr(
{ ...baseInbound, listen: '10.0.0.1' } as never,
'cdn.example.test',
'fallback.test',
)).toBe('cdn.example.test');
});
it('uses listen when override is empty and listen is explicit', () => {
expect(resolveAddr(
{ ...baseInbound, listen: '10.0.0.1' } as never,
'',
'fallback.test',
)).toBe('10.0.0.1');
});
it('skips listen when it is 0.0.0.0 and falls through to fallbackHostname', () => {
expect(resolveAddr(
{ ...baseInbound, listen: '0.0.0.0' } as never,
'',
'fallback.test',
)).toBe('fallback.test');
});
it('falls through to fallbackHostname when listen is empty', () => {
expect(resolveAddr(
baseInbound as never,
'',
'fallback.test',
)).toBe('fallback.test');
});
});
describe('genInboundLinks orchestrator', () => {
// Every full-inbound fixture should produce the same \r\n-joined link
// block at this baseline.
const fixtures = Object.entries(fullFixtures)
.map(([path, raw]): [string, Record] => [fixtureName(path), raw as Record])
.sort(([a], [b]) => a.localeCompare(b));
for (const [name, raw] of fixtures) {
const protocol = (raw as { protocol?: string }).protocol;
// Skip hysteria2 — the legacy class had no dispatch case at the time
// the baseline was locked, so no snapshot exists. The new orchestrator
// covers it via its own logic and the genHysteriaLink unit test.
if (protocol === 'hysteria2') continue;
it(`${name}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
const block = genInboundLinks({
inbound: typed,
remark: 'parity-test',
hostOverride: 'override.test',
fallbackHostname: 'fallback.test',
});
expect(block).toMatchSnapshot();
});
}
});
describe('genShadowsocksLink', () => {
const fixtures = fixturesForProtocol('shadowsocks');
expect(fixtures.length, 'need at least one shadowsocks full-inbound fixture').toBeGreaterThan(0);
for (const [name, raw] of fixtures) {
it(`${name}: byte-stable`, () => {
const typed = InboundSchema.parse(raw);
const settings = (raw as { settings: { clients?: Array<{ password: string }> } }).settings;
const client = settings.clients?.[0];
const link = genShadowsocksLink({
inbound: typed,
address: 'example.test',
port: typed.port,
forceTls: 'same',
remark: 'parity-test',
clientPassword: client?.password ?? '',
externalProxy: null,
});
expect(link).toMatchSnapshot();
});
}
});