inbound-clone.test.ts 3.0 KB

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