mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-10 05:10:58 +00:00
fix(clients): keep VLESS xtls-rprx-vision flow when inbound options reload (#5971)
The client form cleared the `flow` field whenever `showFlow` was false, but `showFlow` is derived from the inbound options list, which is transiently empty while the options query (re)loads (`inboundOptionsQuery.data ?? []`). During that window `showFlow` is a false negative, so the effect silently dropped a valid `xtls-rprx-vision` flow the user had picked for a Reality/TLS inbound and never restored it — the client was then saved with an empty flow and could not connect with XTLS Vision. Guard the clear so it only runs once the inbound options are actually available. Adds a regression test that reproduces the drop across an options reload. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { ThemeProvider } from '@/hooks/useTheme';
|
||||
import ClientFormModal from '@/pages/clients/ClientFormModal';
|
||||
import type { ClientRecord, InboundOption } from '@/hooks/useClients';
|
||||
|
||||
function makeQC() {
|
||||
return new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
}
|
||||
|
||||
const REALITY_INBOUND = {
|
||||
id: 4,
|
||||
port: 10443,
|
||||
protocol: 'vless',
|
||||
tag: 'in-10443-tcp',
|
||||
tlsFlowCapable: true,
|
||||
enable: true,
|
||||
} as unknown as InboundOption;
|
||||
|
||||
const CLIENT = {
|
||||
email: 'testuser',
|
||||
flow: 'xtls-rprx-vision',
|
||||
uuid: '11111111-1111-1111-1111-111111111111',
|
||||
subId: 'subid123',
|
||||
enable: true,
|
||||
} as unknown as ClientRecord;
|
||||
|
||||
function savedFlow(save: ReturnType<typeof vi.fn>): unknown {
|
||||
return (save.mock.calls[0][0] as Record<string, unknown>).flow;
|
||||
}
|
||||
|
||||
describe('ClientFormModal — Vision flow preservation', () => {
|
||||
it('keeps xtls-rprx-vision with a stable Reality inbound', async () => {
|
||||
const qc = makeQC();
|
||||
const save = vi.fn().mockResolvedValue({ success: true });
|
||||
render(
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={qc}>
|
||||
<ClientFormModal open mode="edit" client={CLIENT} inbounds={[REALITY_INBOUND]} attachedIds={[4]} save={save} onOpenChange={() => {}} />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
fireEvent.click(await screen.findByRole('button', { name: /save/i }));
|
||||
await waitFor(() => expect(save).toHaveBeenCalled());
|
||||
expect(savedFlow(save)).toBe('xtls-rprx-vision');
|
||||
});
|
||||
|
||||
it('does not drop a selected Vision flow while the inbound options momentarily reload', async () => {
|
||||
const qc = makeQC();
|
||||
const save = vi.fn().mockResolvedValue({ success: true });
|
||||
const tree = (inbounds: InboundOption[]) => (
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={qc}>
|
||||
<ClientFormModal open mode="edit" client={CLIENT} inbounds={inbounds} attachedIds={[4]} save={save} onOpenChange={() => {}} />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
// Options loaded -> reloading (inboundOptionsQuery.data ?? [] === []) -> loaded again.
|
||||
const { rerender } = render(tree([REALITY_INBOUND]));
|
||||
await screen.findByRole('button', { name: /save/i });
|
||||
rerender(tree([]));
|
||||
rerender(tree([REALITY_INBOUND]));
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /save/i }));
|
||||
await waitFor(() => expect(save).toHaveBeenCalled());
|
||||
expect(savedFlow(save)).toBe('xtls-rprx-vision');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user