use-all-settings.test.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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, describe, expect, it, vi } from 'vitest';
  5. import { useAllSettings } from '@/api/queries/useAllSettings';
  6. import { keys } from '@/api/queryKeys';
  7. import { makeTestQueryClient } from '@/test/test-utils';
  8. import { HttpUtil, Msg } from '@/utils';
  9. afterEach(() => {
  10. vi.restoreAllMocks();
  11. });
  12. describe('useAllSettings', () => {
  13. it('refreshes cached default settings after a successful save', async () => {
  14. vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', {}));
  15. const queryClient = makeTestQueryClient();
  16. const fetchDefaults = vi
  17. .fn()
  18. .mockResolvedValueOnce({ subURI: 'https://example.com/sub/' })
  19. .mockResolvedValueOnce({ subURI: 'https://example.com/my_custom_path/' });
  20. const defaultsQuery = {
  21. queryKey: keys.settings.defaults(),
  22. queryFn: fetchDefaults,
  23. staleTime: Infinity,
  24. };
  25. const wrapper = ({ children }: { children: ReactNode }) => (
  26. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  27. );
  28. await queryClient.query(defaultsQuery);
  29. const { result } = renderHook(() => useAllSettings(), { wrapper });
  30. await waitFor(() => expect(result.current.fetched).toBe(true));
  31. await act(async () => {
  32. await result.current.saveAll();
  33. });
  34. const defaults = await queryClient.query(defaultsQuery);
  35. expect(fetchDefaults).toHaveBeenCalledTimes(2);
  36. expect(defaults.subURI).toBe('https://example.com/my_custom_path/');
  37. });
  38. it('keeps cached default settings when a save fails', async () => {
  39. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
  40. if (url === '/panel/api/setting/update') return new Msg(false, 'Save failed');
  41. return new Msg(true, '', {});
  42. });
  43. const queryClient = makeTestQueryClient();
  44. const fetchDefaults = vi
  45. .fn()
  46. .mockResolvedValueOnce({ subURI: 'https://example.com/sub/' })
  47. .mockResolvedValueOnce({ subURI: 'https://example.com/my_custom_path/' });
  48. const defaultsQuery = {
  49. queryKey: keys.settings.defaults(),
  50. queryFn: fetchDefaults,
  51. staleTime: Infinity,
  52. };
  53. const wrapper = ({ children }: { children: ReactNode }) => (
  54. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  55. );
  56. await queryClient.query(defaultsQuery);
  57. const { result } = renderHook(() => useAllSettings(), { wrapper });
  58. await waitFor(() => expect(result.current.fetched).toBe(true));
  59. await act(async () => {
  60. await result.current.saveAll();
  61. });
  62. const defaults = await queryClient.query(defaultsQuery);
  63. expect(fetchDefaults).toHaveBeenCalledOnce();
  64. expect(defaults.subURI).toBe('https://example.com/sub/');
  65. });
  66. it('accepts legacy overlength regex settings without logging a response validation warning', async () => {
  67. const subJsonUserAgentRegex = 'x'.repeat(2_049);
  68. vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', { subJsonUserAgentRegex }));
  69. const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
  70. const queryClient = makeTestQueryClient();
  71. const wrapper = ({ children }: { children: ReactNode }) => (
  72. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  73. );
  74. const { result } = renderHook(() => useAllSettings(), { wrapper });
  75. await waitFor(() => expect(result.current.fetched).toBe(true));
  76. expect(result.current.allSetting.subJsonUserAgentRegex).toBe(subJsonUserAgentRegex);
  77. expect(warning).not.toHaveBeenCalled();
  78. });
  79. it('keeps an edited setting when a refetch returns older server data', async () => {
  80. const values = [{ webPort: 2053 }, { webPort: 2054 }];
  81. let index = 0;
  82. vi.spyOn(HttpUtil, 'post').mockImplementation(async () => new Msg(true, '', values[index++]));
  83. const queryClient = makeTestQueryClient();
  84. const wrapper = ({ children }: { children: ReactNode }) => (
  85. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  86. );
  87. const { result } = renderHook(() => useAllSettings(), { wrapper });
  88. await waitFor(() => expect(result.current.fetched).toBe(true));
  89. act(() => result.current.updateSetting({ webPort: 3000 }));
  90. await queryClient.invalidateQueries({ queryKey: keys.settings.all() });
  91. await waitFor(() => expect(HttpUtil.post).toHaveBeenCalledTimes(2));
  92. expect(result.current.allSetting.webPort).toBe(3000);
  93. expect(result.current.saveDisabled).toBe(false);
  94. });
  95. it('hydrates redacted secrets after a successful save', async () => {
  96. let fetchCount = 0;
  97. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
  98. if (url === '/panel/api/setting/all') {
  99. fetchCount += 1;
  100. return new Msg(
  101. true,
  102. '',
  103. fetchCount === 1 ? { hasTgBotToken: false } : { hasTgBotToken: true, tgBotToken: '' },
  104. );
  105. }
  106. return new Msg(true, '');
  107. });
  108. const queryClient = makeTestQueryClient();
  109. const wrapper = ({ children }: { children: ReactNode }) => (
  110. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  111. );
  112. const { result } = renderHook(() => useAllSettings(), { wrapper });
  113. await waitFor(() => expect(result.current.fetched).toBe(true));
  114. act(() => result.current.updateSetting({ tgBotToken: 'secret' }));
  115. await act(async () => {
  116. await result.current.saveAll();
  117. });
  118. await waitFor(() => expect(HttpUtil.post).toHaveBeenCalledTimes(3));
  119. expect(result.current.allSetting.tgBotToken).toBe('');
  120. expect(result.current.allSetting.hasTgBotToken).toBe(true);
  121. expect(result.current.saveDisabled).toBe(true);
  122. });
  123. it('establishes a saved baseline for a full-payload security save', async () => {
  124. let fetchCount = 0;
  125. vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
  126. if (url === '/panel/api/setting/all') {
  127. fetchCount += 1;
  128. return new Msg(
  129. true,
  130. '',
  131. fetchCount === 1 ? { hasTgBotToken: false } : { hasTgBotToken: true, tgBotToken: '' },
  132. );
  133. }
  134. return new Msg(true, '');
  135. });
  136. const queryClient = makeTestQueryClient();
  137. const wrapper = ({ children }: { children: ReactNode }) => (
  138. <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  139. );
  140. const { result } = renderHook(() => useAllSettings(), { wrapper });
  141. await waitFor(() => expect(result.current.fetched).toBe(true));
  142. act(() => result.current.updateSetting({ tgBotToken: 'secret' }));
  143. await act(async () => {
  144. await result.current.savePayload({
  145. ...result.current.allSetting,
  146. twoFactorEnable: false,
  147. twoFactorToken: '',
  148. });
  149. });
  150. await waitFor(() => expect(HttpUtil.post).toHaveBeenCalledTimes(3));
  151. expect(result.current.allSetting.tgBotToken).toBe('');
  152. expect(result.current.allSetting.hasTgBotToken).toBe(true);
  153. expect(result.current.saveDisabled).toBe(true);
  154. });
  155. });