use-xray-setting.test.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import type { ReactNode } from 'react';
  2. import { act, renderHook, waitFor } from '@testing-library/react';
  3. import { QueryClientProvider } from '@tanstack/react-query';
  4. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
  5. import { useXraySetting } from '@/hooks/useXraySetting';
  6. import { makeTestQueryClient } from '@/test/test-utils';
  7. import { HttpUtil, Msg } from '@/utils';
  8. function xrayPayload(overrides: Record<string, unknown> = {}) {
  9. return {
  10. xraySetting: {},
  11. inboundTags: [],
  12. clientReverseTags: [],
  13. outboundTestUrl: 'https://test.example',
  14. subscriptionOutbounds: [],
  15. subscriptionOutboundTags: [],
  16. ...overrides,
  17. };
  18. }
  19. afterEach(() => {
  20. vi.restoreAllMocks();
  21. });
  22. beforeEach(() => {
  23. vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', []));
  24. });
  25. describe('useXraySetting', () => {
  26. it('refreshes server-derived outbounds while the editor is dirty', async () => {
  27. let payload = xrayPayload({ subscriptionOutbounds: [{ tag: 'before' }] });
  28. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
  29. if (url === '/panel/api/xray/') return new Msg(true, '', JSON.stringify(payload));
  30. return new Msg(true, '');
  31. });
  32. const queryClient = makeTestQueryClient();
  33. const wrapper = ({ children }: { children: ReactNode }) => (
  34. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  35. );
  36. const { result } = renderHook(() => useXraySetting(), { wrapper });
  37. await waitFor(() => expect(result.current.fetched).toBe(true));
  38. act(() => result.current.setXraySetting('{"outbounds":[]}'));
  39. payload = xrayPayload({ subscriptionOutbounds: [{ tag: 'after' }] });
  40. await act(async () => result.current.fetchAll());
  41. await waitFor(() => expect(result.current.subscriptionOutbounds).toEqual([{ tag: 'after' }]));
  42. expect(result.current.xraySetting).toBe('{"outbounds":[]}');
  43. });
  44. // xray-core 26.10.10 runs a Lua dns.script that addresses servers by id; stripped
  45. // on load, the next template save would silently delete both.
  46. it('keeps the dns script and server ids through the load', async () => {
  47. const payload = xrayPayload({
  48. xraySetting: {
  49. dns: { script: 'dns.lua', servers: [{ address: '1.1.1.1', port: 53, id: 'cf' }] },
  50. },
  51. });
  52. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
  53. if (url === '/panel/api/xray/') return new Msg(true, '', JSON.stringify(payload));
  54. return new Msg(true, '');
  55. });
  56. const queryClient = makeTestQueryClient();
  57. const wrapper = ({ children }: { children: ReactNode }) => (
  58. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  59. );
  60. const { result } = renderHook(() => useXraySetting(), { wrapper });
  61. await waitFor(() => expect(result.current.fetched).toBe(true));
  62. const loaded = JSON.parse(result.current.xraySetting) as {
  63. dns: { script?: string; servers: Array<Record<string, unknown>> };
  64. };
  65. expect(loaded.dns.script).toBe('dns.lua');
  66. expect(loaded.dns.servers[0].id).toBe('cf');
  67. });
  68. it('keeps the outbound test URL input empty when it is cleared', async () => {
  69. const payload = xrayPayload({ outboundTestUrl: 'https://www.google.com/generate_204' });
  70. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
  71. if (url === '/panel/api/xray/') return new Msg(true, '', JSON.stringify(payload));
  72. return new Msg(true, '');
  73. });
  74. const queryClient = makeTestQueryClient();
  75. const wrapper = ({ children }: { children: ReactNode }) => (
  76. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  77. );
  78. const { result } = renderHook(() => useXraySetting(), { wrapper });
  79. await waitFor(() => expect(result.current.fetched).toBe(true));
  80. act(() => result.current.setOutboundTestUrl(''));
  81. expect(result.current.outboundTestUrl).toBe('');
  82. expect(result.current.saveDisabled).toBe(true);
  83. });
  84. // The core lowercases a protocol id and a transport name before resolving
  85. // either, so a differently spelled UDP outbound must still skip the TCP dial.
  86. it.each<[string, Record<string, unknown>, string]>([
  87. ['probes a canonical UDP outbound over HTTP', { protocol: 'wireguard', tag: 'wg' }, 'http'],
  88. [
  89. 'probes a "WireGuard"-spelled outbound over HTTP',
  90. { protocol: 'WireGuard', tag: 'wg' },
  91. 'http',
  92. ],
  93. ['probes a "HyStErIa"-spelled outbound over HTTP', { protocol: 'HyStErIa', tag: 'hy' }, 'http'],
  94. [
  95. 'probes a "KCP" transport over HTTP',
  96. { protocol: 'vless', tag: 'kcp', streamSettings: { network: 'KCP' } },
  97. 'http',
  98. ],
  99. [
  100. 'probes an "mkcp" transport over HTTP',
  101. { protocol: 'vless', tag: 'mkcp', streamSettings: { network: 'mkcp' } },
  102. 'http',
  103. ],
  104. ['probes a plain vless outbound over TCP', { protocol: 'vless', tag: 'plain' }, 'tcp'],
  105. ])('%s', async (_name, outbound, want) => {
  106. const bodies: Array<Record<string, unknown>> = [];
  107. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url, data) => {
  108. if (url === '/panel/api/xray/') {
  109. return new Msg(true, '', JSON.stringify(xrayPayload()));
  110. }
  111. bodies.push(data as Record<string, unknown>);
  112. return new Msg(true, '', [{ success: true, mode: 'http' }]);
  113. });
  114. const queryClient = makeTestQueryClient();
  115. const wrapper = ({ children }: { children: ReactNode }) => (
  116. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  117. );
  118. const { result } = renderHook(() => useXraySetting(), { wrapper });
  119. await waitFor(() => expect(result.current.fetched).toBe(true));
  120. await act(async () => {
  121. await result.current.testOutbound(0, outbound, 'tcp');
  122. });
  123. expect(bodies).toHaveLength(1);
  124. expect(bodies[0].mode).toBe(want);
  125. });
  126. });