import { describe, expect, it } from 'vitest'; import { inboundFromDb } from '@/lib/xray/inbound-from-db'; import { genAllLinks, genInboundLinks, genWireguardConfigs, genWireguardLinks, getInboundClients, } from '@/lib/xray/inbound-link'; import { canEnableTlsFlow, isSS2022, isSSMultiUser, } from '@/lib/xray/protocol-capabilities'; const FALLBACK_HOST = 'panel.example.test'; const BASE_DB_FIELDS = { port: 12345, listen: '', tag: '', remark: 'unit', enable: true, expiryTime: 0, up: 0, down: 0, total: 0, sniffing: '', }; describe('inboundFromDb', () => { it('coerces JSON-string settings into a parsed object', () => { const raw = { ...BASE_DB_FIELDS, protocol: 'vless', settings: JSON.stringify({ clients: [{ id: 'abc', email: 'a@test', flow: '' }], decryption: 'none', }), streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }), }; const inbound = inboundFromDb(raw); expect(inbound.protocol).toBe('vless'); expect(inbound.port).toBe(12345); expect((inbound.settings as { decryption?: string }).decryption).toBe('none'); expect((inbound.streamSettings as { network?: string })?.network).toBe('tcp'); }); it('fills schema defaults onto partial object settings', () => { const settings = { clients: [], decryption: 'none' }; const raw = { ...BASE_DB_FIELDS, protocol: 'vless', settings, streamSettings: { network: 'ws', security: 'tls' }, }; const inbound = inboundFromDb(raw); // encryption/fallbacks defaulted by schema, original settings ref not preserved expect(inbound.settings).not.toBe(settings); expect((inbound.settings as { encryption?: string }).encryption).toBe('none'); expect((inbound.streamSettings as { security?: string })?.security).toBe('tls'); }); it('returns schema-default settings for missing/empty fields without throwing', () => { const raw = { ...BASE_DB_FIELDS, protocol: 'http', settings: '', streamSettings: '', sniffing: '', }; const inbound = inboundFromDb(raw); // http settings has its own schema defaults (accounts: [], allowTransparent: false) expect(inbound.settings).toEqual(expect.objectContaining({ accounts: [] })); expect(inbound.streamSettings).toEqual({}); expect(inbound.sniffing).toEqual({}); }); it('feeds genInboundLinks for vless without throwing', () => { const raw = { ...BASE_DB_FIELDS, protocol: 'vless', settings: { clients: [{ id: '8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02', email: 'alice@test', flow: '' }], decryption: 'none', }, streamSettings: { network: 'tcp', security: 'none' }, }; const inbound = inboundFromDb(raw); const links = genInboundLinks({ inbound, remark: 'unit', fallbackHostname: FALLBACK_HOST, }); expect(links).toContain('vless://'); expect(links).toContain('encryption=none'); }); it('feeds genWireguardConfigs + genWireguardLinks for wireguard peers', () => { const raw = { ...BASE_DB_FIELDS, protocol: 'wireguard', settings: { mtu: 1420, secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=', peers: [ { privateKey: 'iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=', publicKey: 'DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=', allowedIPs: ['10.0.0.2/32'], keepAlive: 25, }, ], noKernelTun: false, }, streamSettings: '', }; const inbound = inboundFromDb(raw); const configs = genWireguardConfigs({ inbound, remark: 'wg', fallbackHostname: FALLBACK_HOST }); expect(configs).toContain('[Interface]'); expect(configs).toContain('[Peer]'); const links = genWireguardLinks({ inbound, remark: 'wg', fallbackHostname: FALLBACK_HOST }); expect(links).toMatch(/^wireguard:\/\//); }); it('feeds genAllLinks per client', () => { const raw = { ...BASE_DB_FIELDS, protocol: 'trojan', settings: { clients: [ { password: 'pw1', email: 'one@test' }, { password: 'pw2', email: 'two@test' }, ], }, streamSettings: { network: 'tcp', security: 'tls', tlsSettings: { serverName: 'example.test' } }, }; const inbound = inboundFromDb(raw); const entries = genAllLinks({ inbound, remark: 'trojan', client: { password: 'pw1', email: 'one@test' }, fallbackHostname: FALLBACK_HOST, }); expect(entries.length).toBeGreaterThan(0); expect(entries[0].link).toContain('trojan://'); }); }); describe('protocol-capability helpers with raw coerced shapes', () => { it('isSSMultiUser returns true for legacy SS methods', () => { expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(true); expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } })).toBe(true); }); it('isSSMultiUser returns false for single-user blake3-chacha20 method', () => { expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-chacha20-poly1305' }, })).toBe(false); }); it('isSS2022 detects 2022-blake3 family', () => { expect(isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } })).toBe(true); expect(isSS2022({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(false); }); it('canEnableTlsFlow gates on vless + tcp + tls/reality', () => { expect(canEnableTlsFlow({ protocol: 'vless', streamSettings: { network: 'tcp', security: 'tls' }, })).toBe(true); expect(canEnableTlsFlow({ protocol: 'vless', streamSettings: { network: 'ws', security: 'tls' }, })).toBe(false); expect(canEnableTlsFlow({ protocol: 'vmess', streamSettings: { network: 'tcp', security: 'tls' }, })).toBe(false); }); it('canEnableTlsFlow allows vless + xhttp when vlessenc encryption is set', () => { const enc = 'mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y'; const dec = 'mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E'; // XHTTP + a real (generated) encryption value → Vision flow allowed. expect(canEnableTlsFlow({ protocol: 'vless', settings: { encryption: enc }, streamSettings: { network: 'xhttp', security: 'none' }, })).toBe(true); // decryption alone (server-side value) is enough on XHTTP. expect(canEnableTlsFlow({ protocol: 'vless', settings: { decryption: dec, encryption: 'none' }, streamSettings: { network: 'xhttp', security: 'none' }, })).toBe(true); // No encryption → stays gated off. expect(canEnableTlsFlow({ protocol: 'vless', settings: { encryption: 'none' }, streamSettings: { network: 'xhttp', security: 'none' }, })).toBe(false); // vlessenc is XHTTP-only: TCP without tls/reality is not Vision-capable. expect(canEnableTlsFlow({ protocol: 'vless', settings: { decryption: dec, encryption: enc }, streamSettings: { network: 'tcp', security: 'none' }, })).toBe(false); }); }); describe('getInboundClients with schema-shaped inbound', () => { it('returns clients array for vless/vmess/trojan/hysteria', () => { const inbound = inboundFromDb({ ...BASE_DB_FIELDS, protocol: 'vless', settings: { clients: [{ id: 'x', email: 'e@test' }], decryption: 'none' }, streamSettings: { network: 'tcp', security: 'none' }, }); expect(getInboundClients(inbound)).toHaveLength(1); }); it('returns null for SS single-user', () => { const inbound = inboundFromDb({ ...BASE_DB_FIELDS, protocol: 'shadowsocks', settings: { method: '2022-blake3-chacha20-poly1305', password: 'pw', clients: [] }, streamSettings: { network: 'tcp', security: 'none' }, }); expect(getInboundClients(inbound)).toBeNull(); }); it('returns null for non-client protocols (http/mixed/tun/tunnel)', () => { for (const protocol of ['http', 'mixed', 'tun', 'tunnel']) { const inbound = inboundFromDb({ ...BASE_DB_FIELDS, protocol, settings: {}, streamSettings: '', }); expect(getInboundClients(inbound)).toBeNull(); } }); });