refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
/// <reference types="vite/client" />
|
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
|
|
2026-05-25 22:27:11 +00:00
|
|
|
import {
|
|
|
|
|
genHysteriaLink,
|
2026-05-25 22:31:25 +00:00
|
|
|
genInboundLinks,
|
2026-05-25 22:27:11 +00:00
|
|
|
genShadowsocksLink,
|
|
|
|
|
genTrojanLink,
|
|
|
|
|
genVlessLink,
|
|
|
|
|
genVmessLink,
|
|
|
|
|
genWireguardConfig,
|
|
|
|
|
genWireguardLink,
|
2026-05-25 22:31:25 +00:00
|
|
|
resolveAddr,
|
2026-05-25 22:27:11 +00:00
|
|
|
} from '@/lib/xray/inbound-link';
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
import { InboundSchema } from '@/schemas/api/inbound';
|
2026-05-25 22:27:11 +00:00
|
|
|
import type { WireguardInboundSettings } from '@/schemas/protocols/inbound/wireguard';
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
// 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.
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genVmessLink', () => {
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
const fixtures = fixturesForProtocol('vmess');
|
|
|
|
|
expect(fixtures.length, 'need at least one vmess full-inbound fixture').toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
const typed = InboundSchema.parse(raw);
|
|
|
|
|
const settings = (raw as { settings: { clients: Array<{ id: string; security?: string }> } }).settings;
|
|
|
|
|
const client = settings.clients[0];
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
const link = genVmessLink({
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
inbound: typed,
|
2026-05-26 10:27:25 +00:00
|
|
|
address: 'example.test',
|
|
|
|
|
port: typed.port,
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
forceTls: 'same',
|
2026-05-26 10:27:25 +00:00
|
|
|
remark: 'parity-test',
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
clientId: client.id,
|
|
|
|
|
security: client.security as never,
|
|
|
|
|
externalProxy: null,
|
|
|
|
|
});
|
2026-05-26 10:27:25 +00:00
|
|
|
expect(link).toMatchSnapshot();
|
refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts
First link generator to leave the class hierarchy. genVmessLink takes a
typed Inbound + client args and returns the base64-encoded vmess://
URL. Internal helpers (buildXhttpExtra, applyXhttpExtraToObj,
applyFinalMaskToObj, applyExternalProxyTLSObj, serializeFinalMask,
hasShareableFinalMaskValue, externalProxyAlpn) port across from
XrayCommonClass — same logic, rewritten to read the Zod schemas'
Record<string, string> headers instead of the legacy HeaderEntry[].
Parity test (inbound-link.test.ts) loads each vmess fixture in
golden/fixtures/inbound-full, parses it with InboundSchema for the new
pure fn AND constructs LegacyInbound.fromJson(raw) for the class method,
then asserts the URLs match byte-for-byte. Drift between the two impls
fails here before the call sites in pages/inbounds/* get swapped.
Adds a small test setup file that aliases globalThis.window to globalThis
so Base64.encode's window.btoa works under Node — keeps the test env at
'node' and avoids pulling jsdom as a new dep.
A first vmess-tcp-tls full-inbound fixture pins the round-trip path.
Suite: 67 tests across 8 files; typecheck + lint clean. Five more link
generators (vless/trojan/ss/hysteria/wireguard) plus the orchestrator
(toShareLink, genAllLinks) follow in subsequent turns.
2026-05-25 22:07:36 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-25 22:15:03 +00:00
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genVlessLink', () => {
|
2026-05-25 22:15:03 +00:00
|
|
|
const fixtures = fixturesForProtocol('vless');
|
|
|
|
|
expect(fixtures.length, 'need at least one vless full-inbound fixture').toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
2026-05-25 22:15:03 +00:00
|
|
|
const typed = InboundSchema.parse(raw);
|
|
|
|
|
const settings = (raw as { settings: { clients: Array<{ id: string; flow?: string }> } }).settings;
|
|
|
|
|
const client = settings.clients[0];
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
const link = genVlessLink({
|
2026-05-25 22:15:03 +00:00
|
|
|
inbound: typed,
|
2026-05-26 10:27:25 +00:00
|
|
|
address: 'example.test',
|
|
|
|
|
port: typed.port,
|
2026-05-25 22:15:03 +00:00
|
|
|
forceTls: 'same',
|
2026-05-26 10:27:25 +00:00
|
|
|
remark: 'parity-test',
|
2026-05-25 22:15:03 +00:00
|
|
|
clientId: client.id,
|
|
|
|
|
flow: client.flow as never,
|
|
|
|
|
externalProxy: null,
|
|
|
|
|
});
|
2026-05-26 10:27:25 +00:00
|
|
|
expect(link).toMatchSnapshot();
|
2026-05-25 22:15:03 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-25 22:18:55 +00:00
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genTrojanLink', () => {
|
2026-05-25 22:18:55 +00:00
|
|
|
const fixtures = fixturesForProtocol('trojan');
|
|
|
|
|
expect(fixtures.length, 'need at least one trojan full-inbound fixture').toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
2026-05-25 22:18:55 +00:00
|
|
|
const typed = InboundSchema.parse(raw);
|
|
|
|
|
const settings = (raw as { settings: { clients: Array<{ password: string }> } }).settings;
|
|
|
|
|
const client = settings.clients[0];
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
const link = genTrojanLink({
|
2026-05-25 22:18:55 +00:00
|
|
|
inbound: typed,
|
2026-05-26 10:27:25 +00:00
|
|
|
address: 'example.test',
|
|
|
|
|
port: typed.port,
|
2026-05-25 22:18:55 +00:00
|
|
|
forceTls: 'same',
|
2026-05-26 10:27:25 +00:00
|
|
|
remark: 'parity-test',
|
2026-05-25 22:18:55 +00:00
|
|
|
clientPassword: client.password,
|
|
|
|
|
externalProxy: null,
|
|
|
|
|
});
|
2026-05-26 10:27:25 +00:00
|
|
|
expect(link).toMatchSnapshot();
|
2026-05-25 22:18:55 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genHysteriaLink', () => {
|
2026-05-25 22:27:11 +00:00
|
|
|
const fixtures = fixturesForProtocol('hysteria');
|
|
|
|
|
expect(fixtures.length, 'need at least one hysteria full-inbound fixture').toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
2026-05-25 22:27:11 +00:00
|
|
|
const typed = InboundSchema.parse(raw);
|
|
|
|
|
const settings = (raw as { settings: { clients: Array<{ auth: string }> } }).settings;
|
|
|
|
|
const client = settings.clients[0];
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
const link = genHysteriaLink({
|
2026-05-25 22:27:11 +00:00
|
|
|
inbound: typed,
|
2026-05-26 10:27:25 +00:00
|
|
|
address: 'example.test',
|
|
|
|
|
port: typed.port,
|
|
|
|
|
remark: 'parity-test',
|
2026-05-25 22:27:11 +00:00
|
|
|
clientAuth: client.auth,
|
|
|
|
|
});
|
2026-05-26 10:27:25 +00:00
|
|
|
expect(link).toMatchSnapshot();
|
2026-05-25 22:27:11 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genWireguardLink + genWireguardConfig', () => {
|
2026-05-25 22:27:11 +00:00
|
|
|
const fixtures = fixturesForProtocol('wireguard');
|
|
|
|
|
expect(fixtures.length, 'need at least one wireguard full-inbound fixture').toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
2026-05-25 22:27:11 +00:00
|
|
|
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;
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
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();
|
2026-05-25 22:27:11 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-25 22:31:25 +00:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genInboundLinks orchestrator', () => {
|
2026-05-25 22:31:25 +00:00
|
|
|
// Every full-inbound fixture should produce the same \r\n-joined link
|
2026-05-26 10:27:25 +00:00
|
|
|
// block at this baseline.
|
2026-05-25 22:31:25 +00:00
|
|
|
const fixtures = Object.entries(fullFixtures)
|
|
|
|
|
.map(([path, raw]): [string, Record<string, unknown>] => [fixtureName(path), raw as Record<string, unknown>])
|
|
|
|
|
.sort(([a], [b]) => a.localeCompare(b));
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
|
|
|
|
const protocol = (raw as { protocol?: string }).protocol;
|
2026-05-26 10:27:25 +00:00
|
|
|
// 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.
|
2026-05-25 22:31:25 +00:00
|
|
|
if (protocol === 'hysteria2') continue;
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
2026-05-25 22:31:25 +00:00
|
|
|
const typed = InboundSchema.parse(raw);
|
2026-05-26 10:27:25 +00:00
|
|
|
const block = genInboundLinks({
|
2026-05-25 22:31:25 +00:00
|
|
|
inbound: typed,
|
2026-05-26 10:27:25 +00:00
|
|
|
remark: 'parity-test',
|
|
|
|
|
hostOverride: 'override.test',
|
|
|
|
|
fallbackHostname: 'fallback.test',
|
2026-05-25 22:31:25 +00:00
|
|
|
});
|
2026-05-26 10:27:25 +00:00
|
|
|
expect(block).toMatchSnapshot();
|
2026-05-25 22:31:25 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
describe('genShadowsocksLink', () => {
|
2026-05-25 22:18:55 +00:00
|
|
|
const fixtures = fixturesForProtocol('shadowsocks');
|
|
|
|
|
expect(fixtures.length, 'need at least one shadowsocks full-inbound fixture').toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
for (const [name, raw] of fixtures) {
|
2026-05-26 10:27:25 +00:00
|
|
|
it(`${name}: byte-stable`, () => {
|
2026-05-25 22:18:55 +00:00
|
|
|
const typed = InboundSchema.parse(raw);
|
|
|
|
|
const settings = (raw as { settings: { clients?: Array<{ password: string }> } }).settings;
|
|
|
|
|
const client = settings.clients?.[0];
|
|
|
|
|
|
2026-05-26 10:27:25 +00:00
|
|
|
const link = genShadowsocksLink({
|
2026-05-25 22:18:55 +00:00
|
|
|
inbound: typed,
|
2026-05-26 10:27:25 +00:00
|
|
|
address: 'example.test',
|
|
|
|
|
port: typed.port,
|
2026-05-25 22:18:55 +00:00
|
|
|
forceTls: 'same',
|
2026-05-26 10:27:25 +00:00
|
|
|
remark: 'parity-test',
|
|
|
|
|
clientPassword: client?.password ?? '',
|
2026-05-25 22:18:55 +00:00
|
|
|
externalProxy: null,
|
|
|
|
|
});
|
2026-05-26 10:27:25 +00:00
|
|
|
expect(link).toMatchSnapshot();
|
2026-05-25 22:18:55 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|