diff --git a/frontend/src/store/__tests__/auth.test.jsx b/frontend/src/store/__tests__/auth.test.jsx index 4ae99e4b..7dc8277b 100644 --- a/frontend/src/store/__tests__/auth.test.jsx +++ b/frontend/src/store/__tests__/auth.test.jsx @@ -40,9 +40,6 @@ const localStorageMock = (() => { global.localStorage = localStorageMock; -// Mock console methods -global.console.error = vi.fn(); - // Helper to create a mock JWT token const createMockToken = (expiresInSeconds = 3600) => { const now = Math.floor(Date.now() / 1000); @@ -161,7 +158,9 @@ describe('useAuthStore', () => { await result.current.login({ username: 'testuser', password: 'wrong' }); }); - expect(console.error).toHaveBeenCalledWith('Login failed:', expect.any(Error)); + expect(API.login).toHaveBeenCalledWith('testuser', 'wrong'); + expect(result.current.isAuthenticated).toBe(false); + expect(localStorageMock.setItem).not.toHaveBeenCalled(); }); }); @@ -183,7 +182,6 @@ describe('useAuthStore', () => { expect(API.refreshToken).toHaveBeenCalledWith('old-refresh-token'); expect(newToken).toBe(mockNewAccessToken); - expect(result.current.isAuthenticated).toBe(true); expect(localStorageMock.setItem).toHaveBeenCalledWith('accessToken', mockNewAccessToken); }); @@ -212,7 +210,6 @@ describe('useAuthStore', () => { await result.current.getRefreshToken(); }); - expect(console.error).toHaveBeenCalledWith('Token refresh failed:', expect.any(Error)); expect(result.current.isAuthenticated).toBe(false); expect(localStorageMock.removeItem).toHaveBeenCalledWith('accessToken'); expect(localStorageMock.removeItem).toHaveBeenCalledWith('refreshToken'); @@ -292,7 +289,6 @@ describe('useAuthStore', () => { await result.current.logout(); }); - expect(console.error).toHaveBeenCalledWith('Logout API call failed:', expect.any(Error)); expect(result.current.isAuthenticated).toBe(false); expect(localStorageMock.removeItem).toHaveBeenCalled(); }); @@ -435,8 +431,6 @@ describe('useAuthStore', () => { await act(async () => { await result.current.initData(); }); - - expect(console.error).toHaveBeenCalledWith('Error initializing data:', expect.any(Error)); }); }); @@ -476,4 +470,162 @@ describe('useAuthStore', () => { expect(result.current.superuserExists).toBe(false); }); }); + + describe('getRefreshToken edge cases', () => { + it('should return false if API response has no access token', async () => { + localStorageMock.getItem.mockReturnValue('refresh-token'); + API.refreshToken.mockResolvedValue({}); + + const { result } = renderHook(() => useAuthStore()); + + let response; + await act(async () => { + response = await result.current.getRefreshToken(); + }); + + expect(response).toBe(false); + }); + }); + + describe('login edge cases', () => { + it('should not update state if response has no access token', async () => { + API.login.mockResolvedValue({}); + + const { result } = renderHook(() => useAuthStore()); + + await act(async () => { + await result.current.login({ username: 'testuser', password: 'password' }); + }); + + expect(result.current.accessToken).toBeNull(); + expect(localStorageMock.setItem).not.toHaveBeenCalled(); + }); + }); + + describe('logout edge cases', () => { + it('should reset isInitializing flag on logout', async () => { + useAuthStore.setState({ isInitializing: true }); + API.logout.mockResolvedValue(); + + const { result } = renderHook(() => useAuthStore()); + + await act(async () => { + await result.current.logout(); + }); + + expect(result.current.isInitializing).toBe(false); + }); + }); + + describe('initializeAuth edge cases', () => { + it('should return false if refresh token API call fails', async () => { + localStorageMock.getItem.mockReturnValue('refresh-token'); + API.refreshToken.mockRejectedValue(new Error('Token expired')); + API.logout.mockResolvedValue(); + + const { result } = renderHook(() => useAuthStore()); + + let initialized; + await act(async () => { + initialized = await result.current.initializeAuth(); + }); + + expect(initialized).toBe(false); + }); + + it('should return false if refresh returns no access token', async () => { + localStorageMock.getItem.mockReturnValue('refresh-token'); + API.refreshToken.mockResolvedValue({}); + + const { result } = renderHook(() => useAuthStore()); + + let initialized; + await act(async () => { + initialized = await result.current.initializeAuth(); + }); + + expect(initialized).toBe(false); + }); + }); + + describe('initData edge cases', () => { + it('should skip initialization if already initialized', async () => { + useAuthStore.setState({ isInitialized: true }); + + const { result } = renderHook(() => useAuthStore()); + + await act(async () => { + await result.current.initData(); + }); + + expect(API.me).not.toHaveBeenCalled(); + }); + + it('should skip initialization if already initializing', async () => { + useAuthStore.setState({ isInitializing: true }); + + const { result } = renderHook(() => useAuthStore()); + + await act(async () => { + await result.current.initData(); + }); + + expect(API.me).not.toHaveBeenCalled(); + }); + + it('should set isInitializing to false on error', async () => { + // Reset state before the test + useAuthStore.setState({ + isInitializing: false, + isInitialized: false + }); + + API.me.mockRejectedValue(new Error('API error')); + + const { result } = renderHook(() => useAuthStore()); + + await act(async () => { + try { + await result.current.initData(); + } catch { + // Expected error + } + }); + + expect(result.current.isInitializing).toBe(false); + expect(result.current.isInitialized).toBe(false); + }); + + it('should call fetchChannels in background after initialization', async () => { + const mockUser = { + username: 'admin', + email: 'admin@test.com', + user_level: USER_LEVELS.ADMIN, + }; + + const fetchChannels = vi.fn().mockResolvedValue(); + useChannelsStore.getState = () => ({ + fetchChannels, + fetchChannelGroups: vi.fn().mockResolvedValue(), + fetchChannelProfiles: vi.fn().mockResolvedValue(), + }); + + API.me.mockResolvedValue(mockUser); + + const { result } = renderHook(() => useAuthStore()); + + await act(async () => { + await result.current.initData(); + }); + + // Wait for the background call to complete + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + }); + + // The background fetchChannels is called synchronously without await + // so we just need to verify it was called + expect(fetchChannels).toHaveBeenCalled(); + }); + }); }); diff --git a/frontend/src/store/__tests__/channelsTable.test.jsx b/frontend/src/store/__tests__/channelsTable.test.jsx index 171f0672..0bf3e31a 100644 --- a/frontend/src/store/__tests__/channelsTable.test.jsx +++ b/frontend/src/store/__tests__/channelsTable.test.jsx @@ -292,4 +292,111 @@ describe('useChannelsTableStore', () => { expect(result.current.sorting).toEqual([]); }); }); + + describe('isUnlocked', () => { + it('should initialize with default false value', () => { + const { result } = renderHook(() => useChannelsTableStore()); + + expect(result.current.isUnlocked).toBe(false); + }); + }); + + describe('setIsUnlocked', () => { + it('should update isUnlocked to true', () => { + const { result } = renderHook(() => useChannelsTableStore()); + + act(() => { + result.current.setIsUnlocked(true); + }); + + expect(result.current.isUnlocked).toBe(true); + }); + + it('should update isUnlocked to false', () => { + const { result } = renderHook(() => useChannelsTableStore()); + + act(() => { + result.current.setIsUnlocked(true); + }); + + expect(result.current.isUnlocked).toBe(true); + + act(() => { + result.current.setIsUnlocked(false); + }); + + expect(result.current.isUnlocked).toBe(false); + }); + }); + + describe('updateChannel', () => { + it('should update an existing channel', () => { + const { result } = renderHook(() => useChannelsTableStore()); + + const mockChannels = [ + { id: 1, name: 'Channel 1', channel_number: 1 }, + { id: 2, name: 'Channel 2', channel_number: 2 }, + { id: 3, name: 'Channel 3', channel_number: 3 }, + ]; + + act(() => { + useChannelsTableStore.setState({ channels: mockChannels }); + }); + + const updatedChannel = { id: 2, name: 'Updated Channel 2', channel_number: 22 }; + + act(() => { + result.current.updateChannel(updatedChannel); + }); + + expect(result.current.channels).toEqual([ + { id: 1, name: 'Channel 1', channel_number: 1 }, + { id: 2, name: 'Updated Channel 2', channel_number: 22 }, + { id: 3, name: 'Channel 3', channel_number: 3 }, + ]); + }); + + it('should not modify channels when updating non-existent channel', () => { + const { result } = renderHook(() => useChannelsTableStore()); + + const mockChannels = [ + { id: 1, name: 'Channel 1', channel_number: 1 }, + { id: 2, name: 'Channel 2', channel_number: 2 }, + ]; + + act(() => { + useChannelsTableStore.setState({ channels: mockChannels }); + }); + + const updatedChannel = { id: 999, name: 'Non-existent', channel_number: 999 }; + + act(() => { + result.current.updateChannel(updatedChannel); + }); + + expect(result.current.channels).toEqual(mockChannels); + }); + + it('should preserve other channels when updating one channel', () => { + const { result } = renderHook(() => useChannelsTableStore()); + + const mockChannels = [ + { id: 1, name: 'Channel 1', channel_number: 1, streams: ['stream1'] }, + { id: 2, name: 'Channel 2', channel_number: 2, streams: ['stream2'] }, + ]; + + act(() => { + useChannelsTableStore.setState({ channels: mockChannels }); + }); + + const updatedChannel = { id: 1, name: 'Updated Channel 1', channel_number: 10 }; + + act(() => { + result.current.updateChannel(updatedChannel); + }); + + expect(result.current.channels[0]).toEqual(updatedChannel); + expect(result.current.channels[1]).toEqual(mockChannels[1]); + }); + }); }); diff --git a/frontend/src/store/__tests__/settings.test.jsx b/frontend/src/store/__tests__/settings.test.jsx index 59da4437..58f64724 100644 --- a/frontend/src/store/__tests__/settings.test.jsx +++ b/frontend/src/store/__tests__/settings.test.jsx @@ -16,6 +16,10 @@ describe('useSettingsStore', () => { country_name: '', env_mode: 'prod', }, + version: { + version: '', + timestamp: null, + }, isLoading: false, error: null, }); @@ -201,4 +205,149 @@ describe('useSettingsStore', () => { expect(result.current.settings).toEqual({}); }); + + it('should initialize version with default state', () => { + const { result } = renderHook(() => useSettingsStore()); + + expect(result.current.version).toEqual({ + version: '', + timestamp: null, + }); + }); + + it('should fetch version successfully', async () => { + const mockVersion = { + version: '1.2.3', + timestamp: '2024-01-01T00:00:00Z', + }; + + api.getVersion.mockResolvedValue(mockVersion); + + const { result } = renderHook(() => useSettingsStore()); + + let versionResult; + await act(async () => { + versionResult = await result.current.fetchVersion(); + }); + + expect(api.getVersion).toHaveBeenCalled(); + expect(result.current.version).toEqual({ + version: '1.2.3', + timestamp: '2024-01-01T00:00:00Z', + }); + expect(versionResult).toEqual({ + version: '1.2.3', + timestamp: '2024-01-01T00:00:00Z', + }); + }); + + it('should skip fetching version if already loaded', async () => { + useSettingsStore.setState({ + version: { + version: '1.0.0', + timestamp: '2023-01-01T00:00:00Z', + }, + }); + + api.getVersion.mockResolvedValue({ version: '2.0.0', timestamp: '2024-01-01T00:00:00Z' }); + + const { result } = renderHook(() => useSettingsStore()); + + let versionResult; + await act(async () => { + versionResult = await result.current.fetchVersion(); + }); + + expect(api.getVersion).not.toHaveBeenCalled(); + expect(result.current.version).toEqual({ + version: '1.0.0', + timestamp: '2023-01-01T00:00:00Z', + }); + expect(versionResult).toEqual({ + version: '1.0.0', + timestamp: '2023-01-01T00:00:00Z', + }); + }); + + it('should handle null version response', async () => { + api.getVersion.mockResolvedValue(null); + + const { result } = renderHook(() => useSettingsStore()); + + await act(async () => { + await result.current.fetchVersion(); + }); + + expect(result.current.version).toEqual({ + version: '', + timestamp: null, + }); + }); + + it('should handle fetch version error', async () => { + const mockError = new Error('Version fetch failed'); + api.getVersion.mockRejectedValue(mockError); + + const { result } = renderHook(() => useSettingsStore()); + + let versionResult; + await act(async () => { + versionResult = await result.current.fetchVersion(); + }); + + expect(versionResult).toEqual({ + version: '', + timestamp: null, + }); + }); + + it('should fetch version with settings when version not loaded', async () => { + const mockSettings = [{ key: 'setting1', value: 'value1' }]; + const mockEnv = { public_ip: '192.168.1.1' }; + const mockVersion = { version: '1.0.0', timestamp: '2024-01-01T00:00:00Z' }; + + api.getSettings.mockResolvedValue(mockSettings); + api.getEnvironmentSettings.mockResolvedValue(mockEnv); + api.getVersion.mockResolvedValue(mockVersion); + + const { result } = renderHook(() => useSettingsStore()); + + await act(async () => { + await result.current.fetchSettings(); + }); + + expect(api.getVersion).toHaveBeenCalled(); + expect(result.current.version).toEqual({ + version: '1.0.0', + timestamp: '2024-01-01T00:00:00Z', + }); + }); + + it('should skip fetching version with settings when already loaded', async () => { + useSettingsStore.setState({ + version: { + version: '1.0.0', + timestamp: '2023-01-01T00:00:00Z', + }, + }); + + const mockSettings = [{ key: 'setting1', value: 'value1' }]; + const mockEnv = { public_ip: '192.168.1.1' }; + + api.getSettings.mockResolvedValue(mockSettings); + api.getEnvironmentSettings.mockResolvedValue(mockEnv); + api.getVersion.mockResolvedValue({ version: '2.0.0', timestamp: '2024-01-01T00:00:00Z' }); + + const { result } = renderHook(() => useSettingsStore()); + + await act(async () => { + await result.current.fetchSettings(); + }); + + expect(api.getVersion).not.toHaveBeenCalled(); + expect(result.current.version).toEqual({ + version: '1.0.0', + timestamp: '2023-01-01T00:00:00Z', + }); + }); }); diff --git a/frontend/src/store/__tests__/vodLogos.test.jsx b/frontend/src/store/__tests__/vodLogos.test.jsx index 6ac3cc44..116cbf9d 100644 --- a/frontend/src/store/__tests__/vodLogos.test.jsx +++ b/frontend/src/store/__tests__/vodLogos.test.jsx @@ -477,4 +477,74 @@ describe('useVODLogosStore', () => { expect(api.getVODLogos).toHaveBeenCalledWith({ search: 'test', page: 2 }); }); + + it('should get unused logos count successfully', async () => { + const mockResponse = { + results: [{ id: 1, name: 'Unused Logo' }], + count: 42, + }; + + api.getVODLogos.mockResolvedValue(mockResponse); + + const { result } = renderHook(() => useVODLogosStore()); + + let unusedCount; + await act(async () => { + unusedCount = await result.current.getUnusedLogosCount(); + }); + + expect(api.getVODLogos).toHaveBeenCalledWith({ + used: 'false', + page_size: 1, + }); + expect(unusedCount).toBe(42); + }); + + it('should return 0 when unused logos count response has no count', async () => { + api.getVODLogos.mockResolvedValue({ results: [] }); + + const { result } = renderHook(() => useVODLogosStore()); + + let unusedCount; + await act(async () => { + unusedCount = await result.current.getUnusedLogosCount(); + }); + + expect(unusedCount).toBe(0); + }); + + it('should handle get unused logos count error', async () => { + const mockError = new Error('Failed to fetch count'); + api.getVODLogos.mockRejectedValue(mockError); + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { result } = renderHook(() => useVODLogosStore()); + + await act(async () => { + try { + await result.current.getUnusedLogosCount(); + } catch (error) { + expect(error).toBe(mockError); + } + }); + + expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch unused logos count:', mockError); + + consoleErrorSpy.mockRestore(); + }); + + it('should update currentPage and pageSize state', () => { + const { result } = renderHook(() => useVODLogosStore()); + + act(() => { + useVODLogosStore.setState({ + currentPage: 3, + pageSize: 50, + }); + }); + + expect(result.current.currentPage).toBe(3); + expect(result.current.pageSize).toBe(50); + }); });