mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-06 14:12:09 +03:00
fcf60eb2e2
Routine dependency refresh: telego 1.11.2, go-sqlite3 1.14.50, grpc 1.83.1,
miekg/dns 1.1.73, sing 0.8.14 and the usual indirect churn on the Go side;
react-query 5.102.2, i18next 26.4.0, react-hook-form 7.86.0, Storybook
10.5.10 and vite 8.2.2 on the frontend, which also lifts the private frontend
package to 1.0.0.
That left npm run lint:deprecated with five call sites. Zod 4 deprecates the
ZodTypeAny alias in favour of the bare z.ZodType constraint, and react-query
renamed queryClient.fetchQuery to queryClient.query ahead of removing the old
name in the next major — the two share an implementation, so the swap in the
settings test is behaviour-identical.
Also untracks internal/web/dist/.gitkeep. 1872659d dropped its gitignore
exception on the grounds that nothing under dist/ is ever meant to be
tracked, but the file was already in the index, so the rule never applied and
every frontend build that empties dist/ resurfaced it as a spurious deletion.
make dist-stub and every CI job recreate it on disk.
178 lines
7.0 KiB
TypeScript
178 lines
7.0 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { useAllSettings } from '@/api/queries/useAllSettings';
|
|
import { keys } from '@/api/queryKeys';
|
|
import { makeTestQueryClient } from '@/test/test-utils';
|
|
import { HttpUtil, Msg } from '@/utils';
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('useAllSettings', () => {
|
|
it('refreshes cached default settings after a successful save', async () => {
|
|
vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', {}));
|
|
const queryClient = makeTestQueryClient();
|
|
const fetchDefaults = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ subURI: 'https://example.com/sub/' })
|
|
.mockResolvedValueOnce({ subURI: 'https://example.com/my_custom_path/' });
|
|
const defaultsQuery = {
|
|
queryKey: keys.settings.defaults(),
|
|
queryFn: fetchDefaults,
|
|
staleTime: Infinity,
|
|
};
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
|
|
await queryClient.query(defaultsQuery);
|
|
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
await act(async () => {
|
|
await result.current.saveAll();
|
|
});
|
|
|
|
const defaults = await queryClient.query(defaultsQuery);
|
|
expect(fetchDefaults).toHaveBeenCalledTimes(2);
|
|
expect(defaults.subURI).toBe('https://example.com/my_custom_path/');
|
|
});
|
|
|
|
it('keeps cached default settings when a save fails', async () => {
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
|
|
if (url === '/panel/api/setting/update') return new Msg(false, 'Save failed');
|
|
return new Msg(true, '', {});
|
|
});
|
|
const queryClient = makeTestQueryClient();
|
|
const fetchDefaults = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ subURI: 'https://example.com/sub/' })
|
|
.mockResolvedValueOnce({ subURI: 'https://example.com/my_custom_path/' });
|
|
const defaultsQuery = {
|
|
queryKey: keys.settings.defaults(),
|
|
queryFn: fetchDefaults,
|
|
staleTime: Infinity,
|
|
};
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
|
|
await queryClient.query(defaultsQuery);
|
|
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
await act(async () => {
|
|
await result.current.saveAll();
|
|
});
|
|
|
|
const defaults = await queryClient.query(defaultsQuery);
|
|
expect(fetchDefaults).toHaveBeenCalledOnce();
|
|
expect(defaults.subURI).toBe('https://example.com/sub/');
|
|
});
|
|
|
|
it('accepts legacy overlength regex settings without logging a response validation warning', async () => {
|
|
const subJsonUserAgentRegex = 'x'.repeat(2_049);
|
|
vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', { subJsonUserAgentRegex }));
|
|
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
|
|
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
expect(result.current.allSetting.subJsonUserAgentRegex).toBe(subJsonUserAgentRegex);
|
|
expect(warning).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps an edited setting when a refetch returns older server data', async () => {
|
|
const values = [{ webPort: 2053 }, { webPort: 2054 }];
|
|
let index = 0;
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async () => new Msg(true, '', values[index++]));
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
act(() => result.current.updateSetting({ webPort: 3000 }));
|
|
await queryClient.invalidateQueries({ queryKey: keys.settings.all() });
|
|
|
|
await waitFor(() => expect(HttpUtil.post).toHaveBeenCalledTimes(2));
|
|
expect(result.current.allSetting.webPort).toBe(3000);
|
|
expect(result.current.saveDisabled).toBe(false);
|
|
});
|
|
|
|
it('hydrates redacted secrets after a successful save', async () => {
|
|
let fetchCount = 0;
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
|
|
if (url === '/panel/api/setting/all') {
|
|
fetchCount += 1;
|
|
return new Msg(
|
|
true,
|
|
'',
|
|
fetchCount === 1 ? { hasTgBotToken: false } : { hasTgBotToken: true, tgBotToken: '' },
|
|
);
|
|
}
|
|
return new Msg(true, '');
|
|
});
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
act(() => result.current.updateSetting({ tgBotToken: 'secret' }));
|
|
await act(async () => {
|
|
await result.current.saveAll();
|
|
});
|
|
|
|
await waitFor(() => expect(HttpUtil.post).toHaveBeenCalledTimes(3));
|
|
expect(result.current.allSetting.tgBotToken).toBe('');
|
|
expect(result.current.allSetting.hasTgBotToken).toBe(true);
|
|
expect(result.current.saveDisabled).toBe(true);
|
|
});
|
|
|
|
it('establishes a saved baseline for a full-payload security save', async () => {
|
|
let fetchCount = 0;
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
|
|
if (url === '/panel/api/setting/all') {
|
|
fetchCount += 1;
|
|
return new Msg(
|
|
true,
|
|
'',
|
|
fetchCount === 1 ? { hasTgBotToken: false } : { hasTgBotToken: true, tgBotToken: '' },
|
|
);
|
|
}
|
|
return new Msg(true, '');
|
|
});
|
|
const queryClient = makeTestQueryClient();
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
act(() => result.current.updateSetting({ tgBotToken: 'secret' }));
|
|
await act(async () => {
|
|
await result.current.savePayload({
|
|
...result.current.allSetting,
|
|
twoFactorEnable: false,
|
|
twoFactorToken: '',
|
|
});
|
|
});
|
|
|
|
await waitFor(() => expect(HttpUtil.post).toHaveBeenCalledTimes(3));
|
|
expect(result.current.allSetting.tgBotToken).toBe('');
|
|
expect(result.current.allSetting.hasTgBotToken).toBe(true);
|
|
expect(result.current.saveDisabled).toBe(true);
|
|
});
|
|
});
|