inbound-form-adapter.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /// <reference types="vite/client" />
  2. import { describe, expect, it } from 'vitest';
  3. import {
  4. rawInboundToFormValues,
  5. formValuesToWirePayload,
  6. type RawInboundRow,
  7. } from '@/lib/xray/inbound-form-adapter';
  8. import { InboundFormSchema } from '@/schemas/forms/inbound-form';
  9. // Round-trip: raw DB row → InboundFormValues → wire payload, asserting
  10. // that the JSON-stringified settings/streamSettings/sniffing in the
  11. // payload deserialize back to the same data the raw row carried.
  12. interface FixtureCase {
  13. name: string;
  14. row: RawInboundRow;
  15. expectedProtocol: string;
  16. }
  17. const vlessRow: RawInboundRow = {
  18. id: 7,
  19. port: 12345,
  20. listen: '0.0.0.0',
  21. protocol: 'vless',
  22. remark: 'edge-1',
  23. enable: true,
  24. up: 1024,
  25. down: 2048,
  26. total: 1_000_000_000,
  27. expiryTime: 0,
  28. trafficReset: 'monthly',
  29. lastTrafficResetTime: 0,
  30. tag: 'inbound-1',
  31. nodeId: null,
  32. settings: {
  33. clients: [{
  34. id: '8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02',
  35. email: '[email protected]',
  36. flow: '',
  37. limitIp: 0,
  38. totalGB: 0,
  39. expiryTime: 0,
  40. enable: true,
  41. tgId: 0,
  42. subId: 'abc123def',
  43. comment: '',
  44. reset: 0,
  45. }],
  46. decryption: 'none',
  47. encryption: 'none',
  48. fallbacks: [],
  49. },
  50. streamSettings: {
  51. network: 'tcp',
  52. security: 'none',
  53. tcpSettings: { header: { type: 'none' } },
  54. },
  55. sniffing: {
  56. enabled: false,
  57. destOverride: ['http', 'tls', 'quic', 'fakedns'],
  58. metadataOnly: false,
  59. routeOnly: false,
  60. ipsExcluded: [],
  61. domainsExcluded: [],
  62. },
  63. } as RawInboundRow & { id: number };
  64. const cases: FixtureCase[] = [
  65. { name: 'vless tcp none', row: vlessRow, expectedProtocol: 'vless' },
  66. {
  67. name: 'string-coerced settings',
  68. row: {
  69. ...vlessRow,
  70. settings: JSON.stringify(vlessRow.settings),
  71. streamSettings: JSON.stringify(vlessRow.streamSettings),
  72. sniffing: JSON.stringify(vlessRow.sniffing),
  73. },
  74. expectedProtocol: 'vless',
  75. },
  76. {
  77. name: 'empty stream settings drop to undefined',
  78. row: { ...vlessRow, streamSettings: '' },
  79. expectedProtocol: 'vless',
  80. },
  81. {
  82. name: 'unknown trafficReset coerces to never',
  83. row: { ...vlessRow, trafficReset: 'totally-fabricated' },
  84. expectedProtocol: 'vless',
  85. },
  86. ];
  87. describe('rawInboundToFormValues', () => {
  88. for (const { name, row, expectedProtocol } of cases) {
  89. it(`maps ${name}`, () => {
  90. const values = rawInboundToFormValues(row);
  91. expect(values.protocol).toBe(expectedProtocol);
  92. expect(values.port).toBe(row.port);
  93. expect(values.remark).toBe(row.remark ?? '');
  94. if (name === 'unknown trafficReset coerces to never') {
  95. expect(values.trafficReset).toBe('never');
  96. }
  97. if (name === 'empty stream settings drop to undefined') {
  98. expect(values.streamSettings).toBeUndefined();
  99. }
  100. });
  101. }
  102. it('produces values that the InboundFormSchema accepts', () => {
  103. const values = rawInboundToFormValues(vlessRow);
  104. const result = InboundFormSchema.safeParse(values);
  105. expect(result.success).toBe(true);
  106. });
  107. });
  108. describe('formValuesToWirePayload', () => {
  109. it('stringifies settings/streamSettings/sniffing with empty-array/default pruning', () => {
  110. const values = rawInboundToFormValues(vlessRow);
  111. const payload = formValuesToWirePayload(values);
  112. expect(typeof payload.settings).toBe('string');
  113. expect(typeof payload.streamSettings).toBe('string');
  114. expect(typeof payload.sniffing).toBe('string');
  115. // Empty arrays like `fallbacks: []` drop out of the payload to match
  116. // the legacy panel's minimal JSON.
  117. const parsedSettings = JSON.parse(payload.settings);
  118. const { fallbacks: _f, ...expectedSettings } = vlessRow.settings as Record<string, unknown>;
  119. expect(parsedSettings).toEqual(expectedSettings);
  120. expect(JSON.parse(payload.streamSettings)).toEqual(vlessRow.streamSettings);
  121. // Disabled sniffing collapses to the bare `{ enabled: false }`
  122. // regardless of which destOverride/metadataOnly/etc. defaults the
  123. // form carries.
  124. expect(JSON.parse(payload.sniffing)).toEqual({ enabled: false });
  125. });
  126. it('emits empty string for absent streamSettings', () => {
  127. const values = rawInboundToFormValues({ ...vlessRow, streamSettings: '' });
  128. const payload = formValuesToWirePayload(values);
  129. expect(payload.streamSettings).toBe('');
  130. });
  131. it('omits nodeId when null', () => {
  132. const values = rawInboundToFormValues({ ...vlessRow, nodeId: null });
  133. const payload = formValuesToWirePayload(values);
  134. expect('nodeId' in payload).toBe(false);
  135. });
  136. it('includes nodeId when set', () => {
  137. const values = rawInboundToFormValues({ ...vlessRow, nodeId: 42 });
  138. const payload = formValuesToWirePayload(values);
  139. expect(payload.nodeId).toBe(42);
  140. });
  141. it('round-trips top-level fields through raw → values → payload → values', () => {
  142. // settings/streamSettings/sniffing don't round-trip byte-equal because
  143. // the wire payload prunes empty arrays and collapses disabled sniffing
  144. // to `{ enabled: false }`. Top-level scalars and the protocol picker
  145. // must still survive the round trip without loss.
  146. const original = rawInboundToFormValues(vlessRow);
  147. const payload = formValuesToWirePayload(original);
  148. const replay = rawInboundToFormValues({
  149. port: payload.port,
  150. listen: payload.listen,
  151. protocol: payload.protocol,
  152. tag: payload.tag,
  153. settings: payload.settings,
  154. streamSettings: payload.streamSettings,
  155. sniffing: payload.sniffing,
  156. up: payload.up,
  157. down: payload.down,
  158. total: payload.total,
  159. remark: payload.remark,
  160. enable: payload.enable,
  161. expiryTime: payload.expiryTime,
  162. trafficReset: payload.trafficReset,
  163. lastTrafficResetTime: payload.lastTrafficResetTime,
  164. nodeId: payload.nodeId ?? null,
  165. });
  166. expect(replay.protocol).toBe(original.protocol);
  167. expect(replay.port).toBe(original.port);
  168. expect(replay.tag).toBe(original.tag);
  169. expect(replay.listen).toBe(original.listen);
  170. expect(replay.up).toBe(original.up);
  171. expect(replay.down).toBe(original.down);
  172. expect(replay.streamSettings).toEqual(original.streamSettings);
  173. });
  174. });