inbound-clone.test.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. import { describe, expect, it } from 'vitest';
  2. import { buildClonePayload, pickClonePort } from '@/lib/xray/inbound-clone';
  3. import { DBInbound } from '@/models/dbinbound';
  4. function sourceInbound() {
  5. return new DBInbound({
  6. id: 7,
  7. port: 443,
  8. listen: '0.0.0.0',
  9. protocol: 'vless',
  10. remark: 'edge',
  11. enable: true,
  12. settings: JSON.stringify({
  13. clients: [{ id: 'uuid-1', email: 'a@test', flow: 'xtls-rprx-vision' }],
  14. decryption: 'none',
  15. }),
  16. streamSettings: { network: 'tcp', security: 'reality', realitySettings: { dest: 'www.lovelive-anime.jp:443' } },
  17. sniffing: { enabled: true },
  18. nodeId: 2,
  19. shareAddrStrategy: 'node',
  20. shareAddr: '',
  21. });
  22. }
  23. describe('buildClonePayload', () => {
  24. it('omits nodeId for a local-panel target so the row stays panel-local', () => {
  25. const payload = buildClonePayload(sourceInbound(), 23456, null);
  26. expect(payload).not.toHaveProperty('nodeId');
  27. });
  28. it('carries nodeId for a node target', () => {
  29. const payload = buildClonePayload(sourceInbound(), 23456, 5);
  30. expect(payload.nodeId).toBe(5);
  31. });
  32. it('stages the clone disabled with cleared clients, fresh port, and no tag', () => {
  33. const payload = buildClonePayload(sourceInbound(), 23456, 3);
  34. expect(payload.enable).toBe(false);
  35. expect(payload.port).toBe(23456);
  36. expect(payload.listen).toBe('');
  37. expect(payload).not.toHaveProperty('tag');
  38. expect(payload.remark).toBe('edge (clone)');
  39. const settings = JSON.parse(payload.settings);
  40. // Clients are dropped (emails are unique panel-wide, UUIDs must not
  41. // repeat across nodes) while the rest of the settings survive verbatim.
  42. expect(settings.clients).toEqual([]);
  43. expect(settings.decryption).toBe('none');
  44. });
  45. it('stringifies object-shaped streamSettings and sniffing from hydrated rows', () => {
  46. const payload = buildClonePayload(sourceInbound(), 23456, null);
  47. expect(JSON.parse(payload.streamSettings)).toEqual({
  48. network: 'tcp',
  49. security: 'reality',
  50. realitySettings: { dest: 'www.lovelive-anime.jp:443' },
  51. });
  52. expect(JSON.parse(payload.sniffing)).toEqual({ enabled: true });
  53. });
  54. it('survives malformed settings JSON with an empty client list fallback', () => {
  55. const broken = sourceInbound();
  56. broken.settings = '{not json';
  57. const payload = buildClonePayload(broken, 23456, null);
  58. const settings = JSON.parse(payload.settings);
  59. expect(settings.clients ?? []).toEqual([]);
  60. });
  61. });
  62. describe('pickClonePort', () => {
  63. it('never returns a port already bound on the target', () => {
  64. const used = new Set<number>();
  65. for (let p = 10000; p <= 60000; p++) if (p !== 23456) used.add(p);
  66. expect(pickClonePort(used)).toBe(23456);
  67. });
  68. it('stops probing when the range looks exhausted instead of spinning', () => {
  69. const used = new Set<number>();
  70. for (let p = 10000; p <= 60000; p++) used.add(p);
  71. const port = pickClonePort(used);
  72. expect(port).toBeGreaterThanOrEqual(10000);
  73. expect(port).toBeLessThanOrEqual(60000);
  74. });
  75. });