mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
84c5aef4a1
* fix(panel): match the probe and egress readers to what the core loads Two readers left over from the case-sensitivity sweep still disagreed with the core, both raised reviewing #6523. isUdpOutbound compared the protocol id and the transport name exactly. The core lowercases both before it resolves them (infra/conf/loader.go:46 for the id, TransportProtocol.Build at infra/conf/transport_internet.go:16-17 for the name), so an outbound spelled "WireGuard" or a stream named "KCP" still built a UDP handler but was probed with a dial-only TCP request, and Test All Outbounds reported a working outbound as down. The mtproto egress picker asked for outbound tags without excludeBlackhole, so the block outbound stayed selectable there. Choosing it looks like a working selection and discards that inbound's Telegram traffic. * fix(panel): recognise the mkcp transport alias and pin the picker's field id Review findings on #6525. TransportProtocol.Build resolves both "kcp" and "mkcp" to the same mKCP transport, so comparing the transport name against "kcp" alone left a template spelling "network": "mkcp" in the TCP lane and reported a working outbound as down — the same trigger this PR already fixed for the "KCP" capitalisation. The egress picker now carries an explicit id, the way the inbound form's protocol select does, so the test addresses that field rather than the first searchable select on the page and reuses the shared dropdown helper instead of duplicating it.
116 lines
4.6 KiB
TypeScript
116 lines
4.6 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { useXraySetting } from '@/hooks/useXraySetting';
|
|
import { makeTestQueryClient } from '@/test/test-utils';
|
|
import { HttpUtil, Msg } from '@/utils';
|
|
|
|
function xrayPayload(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
xraySetting: {},
|
|
inboundTags: [],
|
|
clientReverseTags: [],
|
|
outboundTestUrl: 'https://test.example',
|
|
subscriptionOutbounds: [],
|
|
subscriptionOutboundTags: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', []));
|
|
});
|
|
|
|
describe('useXraySetting', () => {
|
|
it('refreshes server-derived outbounds while the editor is dirty', async () => {
|
|
let payload = xrayPayload({ subscriptionOutbounds: [{ tag: 'before' }] });
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
|
|
if (url === '/panel/api/xray/') return new Msg(true, '', JSON.stringify(payload));
|
|
return new Msg(true, '');
|
|
});
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
const { result } = renderHook(() => useXraySetting(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
act(() => result.current.setXraySetting('{"outbounds":[]}'));
|
|
payload = xrayPayload({ subscriptionOutbounds: [{ tag: 'after' }] });
|
|
await act(async () => result.current.fetchAll());
|
|
|
|
await waitFor(() => expect(result.current.subscriptionOutbounds).toEqual([{ tag: 'after' }]));
|
|
expect(result.current.xraySetting).toBe('{"outbounds":[]}');
|
|
});
|
|
|
|
it('keeps the outbound test URL input empty when it is cleared', async () => {
|
|
const payload = xrayPayload({ outboundTestUrl: 'https://www.google.com/generate_204' });
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
|
|
if (url === '/panel/api/xray/') return new Msg(true, '', JSON.stringify(payload));
|
|
return new Msg(true, '');
|
|
});
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
const { result } = renderHook(() => useXraySetting(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
act(() => result.current.setOutboundTestUrl(''));
|
|
|
|
expect(result.current.outboundTestUrl).toBe('');
|
|
expect(result.current.saveDisabled).toBe(true);
|
|
});
|
|
|
|
// The core lowercases a protocol id and a transport name before resolving
|
|
// either, so a differently spelled UDP outbound must still skip the TCP dial.
|
|
it.each<[string, Record<string, unknown>, string]>([
|
|
['probes a canonical UDP outbound over HTTP', { protocol: 'wireguard', tag: 'wg' }, 'http'],
|
|
[
|
|
'probes a "WireGuard"-spelled outbound over HTTP',
|
|
{ protocol: 'WireGuard', tag: 'wg' },
|
|
'http',
|
|
],
|
|
['probes a "HyStErIa"-spelled outbound over HTTP', { protocol: 'HyStErIa', tag: 'hy' }, 'http'],
|
|
[
|
|
'probes a "KCP" transport over HTTP',
|
|
{ protocol: 'vless', tag: 'kcp', streamSettings: { network: 'KCP' } },
|
|
'http',
|
|
],
|
|
[
|
|
'probes an "mkcp" transport over HTTP',
|
|
{ protocol: 'vless', tag: 'mkcp', streamSettings: { network: 'mkcp' } },
|
|
'http',
|
|
],
|
|
['probes a plain vless outbound over TCP', { protocol: 'vless', tag: 'plain' }, 'tcp'],
|
|
])('%s', async (_name, outbound, want) => {
|
|
const bodies: Array<Record<string, unknown>> = [];
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url, data) => {
|
|
if (url === '/panel/api/xray/') {
|
|
return new Msg(true, '', JSON.stringify(xrayPayload()));
|
|
}
|
|
bodies.push(data as Record<string, unknown>);
|
|
return new Msg(true, '', [{ success: true, mode: 'http' }]);
|
|
});
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
const { result } = renderHook(() => useXraySetting(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
await act(async () => {
|
|
await result.current.testOutbound(0, outbound, 'tcp');
|
|
});
|
|
|
|
expect(bodies).toHaveLength(1);
|
|
expect(bodies[0].mode).toBe(want);
|
|
});
|
|
});
|