mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 04:19:34 +00:00
Tiny piece of the toShareLink scaffold. The legacy Inbound.getHeader(obj, name) iterated the panel's internal HeaderEntry[] form; the new getHeaderValue reads the Record<string, string|string[]> map our Zod schemas store on the wire. Case-insensitive, returns '' on miss to match the legacy fallback so link-generator call sites stay simple. For repeated-name maps (TCP/WS-style string[] values) the first value wins — matches the legacy iteration order so the share URL's Host hint stays deterministic. Five unit tests cover undefined/null/empty inputs, case folding, string-valued and array-valued matches, empty-array edge case, and missing-key fallback. Suite: 64 tests across 6 files; typecheck + lint clean. This unblocks the next slice: per-protocol link generators (genVmessLink etc.) take a typed inbound + client and call getHeaderValue against the ws/httpupgrade/xhttp/tcp.request header maps.
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { getHeaderValue, toHeaders, toV2Headers, type HeaderEntry } from '@/lib/xray/headers';
|
|
import { XrayCommonClass } from '@/models/inbound';
|
|
|
|
// Shadow harness: the new pure helpers must agree byte-for-byte with the
|
|
// legacy XrayCommonClass static methods. Drift here is a regression.
|
|
|
|
const headerMapCases: Array<[string, unknown]> = [
|
|
['null', null],
|
|
['undefined', undefined],
|
|
['primitive', 'not-an-object'],
|
|
['empty', {}],
|
|
['single string', { Host: 'example.test' }],
|
|
['single array', { Host: ['a.example.test'] }],
|
|
['multi array', { Accept: ['text/html', 'application/json'] }],
|
|
['mixed', { Host: 'a.example.test', 'X-Trace': ['1', '2'] }],
|
|
];
|
|
|
|
describe('toHeaders parity with XrayCommonClass.toHeaders', () => {
|
|
for (const [label, input] of headerMapCases) {
|
|
it(label, () => {
|
|
expect(toHeaders(input)).toEqual(XrayCommonClass.toHeaders(input));
|
|
});
|
|
}
|
|
});
|
|
|
|
const entryCases: Array<[string, HeaderEntry[]]> = [
|
|
['empty', []],
|
|
['single', [{ name: 'Host', value: 'example.test' }]],
|
|
['duplicate name', [
|
|
{ name: 'Accept', value: 'text/html' },
|
|
{ name: 'Accept', value: 'application/json' },
|
|
]],
|
|
['empty name skipped', [
|
|
{ name: '', value: 'ignored' },
|
|
{ name: 'X-Real', value: 'kept' },
|
|
]],
|
|
['empty value skipped', [
|
|
{ name: 'X-Empty', value: '' },
|
|
{ name: 'X-Real', value: 'kept' },
|
|
]],
|
|
];
|
|
|
|
describe('toV2Headers parity (arr=true)', () => {
|
|
for (const [label, input] of entryCases) {
|
|
it(label, () => {
|
|
expect(toV2Headers(input, true)).toEqual(XrayCommonClass.toV2Headers(input, true));
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('toV2Headers parity (arr=false)', () => {
|
|
for (const [label, input] of entryCases) {
|
|
it(label, () => {
|
|
expect(toV2Headers(input, false)).toEqual(XrayCommonClass.toV2Headers(input, false));
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('getHeaderValue lookups', () => {
|
|
it('returns empty string for missing map', () => {
|
|
expect(getHeaderValue(undefined, 'host')).toBe('');
|
|
expect(getHeaderValue(null, 'host')).toBe('');
|
|
expect(getHeaderValue({}, 'host')).toBe('');
|
|
});
|
|
|
|
it('finds a string-valued header case-insensitively', () => {
|
|
expect(getHeaderValue({ Host: 'example.test' }, 'host')).toBe('example.test');
|
|
expect(getHeaderValue({ host: 'example.test' }, 'HOST')).toBe('example.test');
|
|
});
|
|
|
|
it('returns first value when the header is an array', () => {
|
|
expect(getHeaderValue({ Accept: ['text/html', 'application/json'] }, 'accept')).toBe('text/html');
|
|
});
|
|
|
|
it('returns empty string when the header has empty array', () => {
|
|
expect(getHeaderValue({ Host: [] }, 'host')).toBe('');
|
|
});
|
|
|
|
it('returns empty string for missing header name', () => {
|
|
expect(getHeaderValue({ Host: 'x' }, 'origin')).toBe('');
|
|
});
|
|
});
|