import { describe, expect, it } from 'vitest'; import { buildClonePayload, pickClonePort } from '@/lib/xray/inbound-clone'; import { DBInbound } from '@/models/dbinbound'; function sourceInbound() { return new DBInbound({ id: 7, port: 443, listen: '0.0.0.0', protocol: 'vless', remark: 'edge', enable: true, settings: JSON.stringify({ clients: [{ id: 'uuid-1', email: 'a@test', flow: 'xtls-rprx-vision' }], decryption: 'none', }), streamSettings: { network: 'tcp', security: 'reality', realitySettings: { dest: 'www.lovelive-anime.jp:443' } }, sniffing: { enabled: true }, nodeId: 2, shareAddrStrategy: 'node', shareAddr: '', }); } describe('buildClonePayload', () => { it('omits nodeId for a local-panel target so the row stays panel-local', () => { const payload = buildClonePayload(sourceInbound(), 23456, null); expect(payload).not.toHaveProperty('nodeId'); }); it('carries nodeId for a node target', () => { const payload = buildClonePayload(sourceInbound(), 23456, 5); expect(payload.nodeId).toBe(5); }); it('stages the clone disabled with cleared clients, fresh port, and no tag', () => { const payload = buildClonePayload(sourceInbound(), 23456, 3); expect(payload.enable).toBe(false); expect(payload.port).toBe(23456); expect(payload.listen).toBe(''); expect(payload).not.toHaveProperty('tag'); expect(payload.remark).toBe('edge (clone)'); const settings = JSON.parse(payload.settings); // Clients are dropped (emails are unique panel-wide, UUIDs must not // repeat across nodes) while the rest of the settings survive verbatim. expect(settings.clients).toEqual([]); expect(settings.decryption).toBe('none'); }); it('stringifies object-shaped streamSettings and sniffing from hydrated rows', () => { const payload = buildClonePayload(sourceInbound(), 23456, null); expect(JSON.parse(payload.streamSettings)).toEqual({ network: 'tcp', security: 'reality', realitySettings: { dest: 'www.lovelive-anime.jp:443' }, }); expect(JSON.parse(payload.sniffing)).toEqual({ enabled: true }); }); it('survives malformed settings JSON with an empty client list fallback', () => { const broken = sourceInbound(); broken.settings = '{not json'; const payload = buildClonePayload(broken, 23456, null); const settings = JSON.parse(payload.settings); expect(settings.clients ?? []).toEqual([]); }); }); describe('pickClonePort', () => { it('never returns a port already bound on the target', () => { const used = new Set(); for (let p = 10000; p <= 60000; p++) if (p !== 23456) used.add(p); expect(pickClonePort(used)).toBe(23456); }); it('stops probing when the range looks exhausted instead of spinning', () => { const used = new Set(); for (let p = 10000; p <= 60000; p++) used.add(p); const port = pickClonePort(used); expect(port).toBeGreaterThanOrEqual(10000); expect(port).toBeLessThanOrEqual(60000); }); });