| import { renderHook, act } from '@testing-library/react'; |
| import { useQueryClient } from '@tanstack/react-query'; |
| import { useHealthCheck } from '../connection'; |
| import { QueryKeys, Time, dataService } from 'librechat-data-provider'; |
|
|
| |
| jest.mock('@tanstack/react-query'); |
| jest.mock('librechat-data-provider', () => ({ |
| QueryKeys: { health: 'health' }, |
| Time: { TEN_MINUTES: 600000, FIVE_MINUTES: 300000 }, |
| dataService: { healthCheck: jest.fn() }, |
| })); |
|
|
| jest.mock('~/utils', () => ({ |
| logger: { log: jest.fn() }, |
| })); |
|
|
| |
| jest.useFakeTimers(); |
|
|
| const mockQueryClient = { |
| fetchQuery: jest.fn(), |
| getQueryState: jest.fn(), |
| getQueryData: jest.fn(), |
| invalidateQueries: jest.fn(), |
| } as any; |
|
|
| const mockUseQueryClient = useQueryClient as jest.MockedFunction<typeof useQueryClient>; |
|
|
| describe('useHealthCheck', () => { |
| let addEventListenerSpy: jest.SpyInstance; |
| let removeEventListenerSpy: jest.SpyInstance; |
|
|
| beforeEach(() => { |
| jest.clearAllMocks(); |
| jest.clearAllTimers(); |
| mockUseQueryClient.mockReturnValue(mockQueryClient); |
|
|
| addEventListenerSpy = jest.spyOn(window, 'addEventListener'); |
| removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); |
|
|
| mockQueryClient.fetchQuery.mockResolvedValue({}); |
| mockQueryClient.getQueryState.mockReturnValue(null); |
| }); |
|
|
| afterEach(() => { |
| addEventListenerSpy.mockRestore(); |
| removeEventListenerSpy.mockRestore(); |
| }); |
|
|
| describe('when not authenticated', () => { |
| it('should not start health check', () => { |
| renderHook(() => useHealthCheck(false)); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(1000); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); |
| expect(addEventListenerSpy).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|
| describe('when authenticated', () => { |
| it('should start health check after delay', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| |
| expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); |
|
|
| |
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledWith( |
| [QueryKeys.health], |
| expect.any(Function), |
| { |
| retry: false, |
| cacheTime: 0, |
| staleTime: 0, |
| }, |
| ); |
| }); |
|
|
| it('should set up 10-minute interval', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| |
| mockQueryClient.fetchQuery.mockClear(); |
|
|
| |
| await act(async () => { |
| jest.advanceTimersByTime(Time.TEN_MINUTES); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it('should run health check continuously every 10 minutes', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| |
| mockQueryClient.fetchQuery.mockClear(); |
|
|
| |
| for (let i = 1; i <= 5; i++) { |
| await act(async () => { |
| jest.advanceTimersByTime(Time.TEN_MINUTES); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(i); |
| } |
|
|
| |
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(5); |
|
|
| |
| await act(async () => { |
| jest.advanceTimersByTime(Time.TEN_MINUTES * 3); |
| }); |
|
|
| |
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(8); |
| }); |
|
|
| it('should add window focus event listener', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| expect(addEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); |
| }); |
|
|
| it('should handle window focus correctly when no previous check', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| |
| const focusHandler = addEventListenerSpy.mock.calls[0][1]; |
|
|
| |
| mockQueryClient.getQueryState.mockReturnValue(null); |
| mockQueryClient.fetchQuery.mockClear(); |
|
|
| |
| await act(async () => { |
| focusHandler(); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it('should handle window focus correctly when check is recent', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| |
| const focusHandler = addEventListenerSpy.mock.calls[0][1]; |
|
|
| |
| mockQueryClient.getQueryState.mockReturnValue({ |
| dataUpdatedAt: Date.now() - 300000, |
| }); |
| mockQueryClient.fetchQuery.mockClear(); |
|
|
| |
| await act(async () => { |
| focusHandler(); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); |
| }); |
|
|
| it('should handle window focus correctly when check is old', async () => { |
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| |
| const focusHandler = addEventListenerSpy.mock.calls[0][1]; |
|
|
| |
| mockQueryClient.getQueryState.mockReturnValue({ |
| dataUpdatedAt: Date.now() - 700000, |
| }); |
| mockQueryClient.fetchQuery.mockClear(); |
|
|
| |
| await act(async () => { |
| focusHandler(); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it('should prevent multiple initializations', async () => { |
| const { rerender } = renderHook(({ auth }) => useHealthCheck(auth), { |
| initialProps: { auth: true }, |
| }); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| const initialCallCount = addEventListenerSpy.mock.calls.length; |
|
|
| |
| rerender({ auth: true }); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| |
| expect(addEventListenerSpy).toHaveBeenCalledTimes(initialCallCount); |
| }); |
|
|
| it('should handle API errors gracefully', async () => { |
| const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); |
| mockQueryClient.fetchQuery.mockRejectedValue(new Error('API Error')); |
|
|
| renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| expect(consoleSpy).toHaveBeenCalledWith('Health check failed:', expect.any(Error)); |
| consoleSpy.mockRestore(); |
| }); |
| }); |
|
|
| describe('cleanup', () => { |
| it('should clear intervals on unmount', async () => { |
| const { unmount } = renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); |
|
|
| unmount(); |
|
|
| expect(clearIntervalSpy).toHaveBeenCalled(); |
| clearIntervalSpy.mockRestore(); |
| }); |
|
|
| it('should remove event listeners on unmount', async () => { |
| const { unmount } = renderHook(() => useHealthCheck(true)); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| unmount(); |
|
|
| expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); |
| }); |
|
|
| it('should clear timeout on unmount before initialization', () => { |
| const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); |
| const { unmount } = renderHook(() => useHealthCheck(true)); |
|
|
| |
| unmount(); |
|
|
| expect(clearTimeoutSpy).toHaveBeenCalled(); |
| clearTimeoutSpy.mockRestore(); |
| }); |
| }); |
|
|
| describe('authentication state changes', () => { |
| it('should start health check when authentication becomes true', async () => { |
| const { rerender } = renderHook(({ auth }) => useHealthCheck(auth), { |
| initialProps: { auth: false }, |
| }); |
|
|
| |
| act(() => { |
| jest.advanceTimersByTime(1000); |
| }); |
| expect(mockQueryClient.fetchQuery).not.toHaveBeenCalled(); |
|
|
| |
| rerender({ auth: true }); |
|
|
| await act(async () => { |
| jest.advanceTimersByTime(500); |
| }); |
|
|
| expect(mockQueryClient.fetchQuery).toHaveBeenCalled(); |
| }); |
| }); |
| }); |
|
|