import { renderHook, act, waitFor } from '@testing-library/react'; import { useAuth } from './useAuth'; import '@testing-library/jest-dom'; import { AuthenticationConfiguration } from '../types'; import * as authUtils from '../utils/authUtils'; jest.mock('../utils/authUtils'); const mockGetCookieValue = authUtils.getCookieValue as jest.MockedFunction; const mockRefreshAccessToken = authUtils.refreshAccessToken as jest.MockedFunction; describe('useAuth', () => { const authConfig: AuthenticationConfiguration = { authUrl: 'https://auth.example.com', clientId: 'example-client-id', redirectUrl: 'https://example.com/callback', scope: 'offline', }; beforeEach(() => { jest.clearAllMocks(); }); it('should initialize with token and authentication state', async () => { mockGetCookieValue.mockReturnValue('initialAuthToken'); const { result } = renderHook(() => useAuth(authConfig)); await waitFor(() => { expect(result.current.authToken).toBe('initialAuthToken'); expect(result.current.isAuthenticated).toBe(true); expect(result.current.alwaysUseResourceRequestProxy).toBe(false); }); }); it('should do nothing if missing crucial props', async () => { const authConfigMissingProps = { clientId: 'example-client-id', // Missing redirectUrl, authUrl, and scope }; // @ts-ignore const { result } = renderHook(() => useAuth(authConfigMissingProps)); result.current.login(); await waitFor(() => { expect(result.current.authToken).toBe(undefined); expect(result.current.isAuthenticated).toBe(false); }); }); it('should log error and return when popup fails to open', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); jest.spyOn(window, 'open').mockImplementation(() => null); const { result } = renderHook(() => useAuth(authConfig)); result.current.login(); await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith('Popup failed to open'); }); consoleSpy.mockRestore(); }); it('should log error when popup closes before authentication', async () => { jest.useFakeTimers(); const popupMock = { close: jest.fn(), } as unknown as Window; Object.defineProperty(popupMock, 'closed', { get: jest.fn(() => false), set: jest.fn(), }); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); jest.spyOn(window, 'open').mockImplementation(() => popupMock); mockGetCookieValue.mockReturnValueOnce(null); const { result } = renderHook(() => useAuth(authConfig)); result.current.login(); (Object.getOwnPropertyDescriptor(popupMock, 'closed')!.get as jest.Mock).mockReturnValue(true); jest.advanceTimersByTime(1000); await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith('Popup closed before authentication'); }); consoleSpy.mockRestore(); }); it('should login successfully and update state', async () => { jest.useFakeTimers(); const popupMock = { closed: false, close: jest.fn(), } as unknown as Window; jest.spyOn(window, 'open').mockImplementation(() => popupMock); mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken'); const { result } = renderHook(() => useAuth(authConfig)); result.current.login(); expect(window.open).toHaveBeenCalledWith( `${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`, 'Login', 'width=600,height=600', ); act(() => { mockGetCookieValue.mockReturnValue('newAuthToken'); jest.advanceTimersByTime(1000); (popupMock as any).closed = true; }); await waitFor(() => { expect(result.current.authToken).toBe('newAuthToken'); expect(result.current.isAuthenticated).toBe(true); }); expect(popupMock.close).toHaveBeenCalled(); }); it('splits the scope from dxp-console and sends in expected format (space delimited)', async () => { jest.useFakeTimers(); const popupMock = { closed: false, close: jest.fn(), } as unknown as Window; jest.spyOn(window, 'open').mockImplementation(() => popupMock); mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken'); const { result } = renderHook(() => useAuth({ ...authConfig, scope: 'offline;asset:read' })); result.current.login(); expect(window.open).toHaveBeenCalledWith( `${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline asset:read&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`, 'Login', 'width=600,height=600', ); }); it('Works with an empty scope', async () => { jest.useFakeTimers(); const popupMock = { closed: false, close: jest.fn(), } as unknown as Window; jest.spyOn(window, 'open').mockImplementation(() => popupMock); mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken'); const { result } = renderHook(() => useAuth({ ...authConfig, scope: '' })); result.current.login(); expect(window.open).toHaveBeenCalledWith( `${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`, 'Login', 'width=600,height=600', ); }); it('should refresh access token and update state', async () => { mockGetCookieValue.mockReturnValue('initialRefreshToken'); mockRefreshAccessToken.mockResolvedValue('newAuthToken'); const { result } = renderHook(() => useAuth(authConfig)); await waitFor(() => { expect(mockRefreshAccessToken).toHaveBeenCalledWith(authConfig); expect(result.current.authToken).toBe('newAuthToken'); expect(result.current.isAuthenticated).toBe(true); }); }); it('should handle token refresh failure', async () => { mockGetCookieValue.mockReturnValue('initialRefreshToken'); mockRefreshAccessToken.mockRejectedValue(new Error('Failed to refresh token')); const { result } = renderHook(() => useAuth(authConfig)); await waitFor(() => { expect(mockRefreshAccessToken).toHaveBeenCalledWith(authConfig); expect(result.current.isAuthenticated).toBe(false); }); }); it('should update state when the page gains focus', async () => { const { result } = renderHook(() => useAuth(authConfig)); mockGetCookieValue.mockReturnValueOnce('focusedAuthToken'); act(() => { window.dispatchEvent(new Event('focus')); }); await waitFor(() => { expect(result.current.authToken).toBe('focusedAuthToken'); expect(result.current.isAuthenticated).toBe(true); }); }); it('should update state when the document becomes visible', async () => { mockGetCookieValue.mockReturnValueOnce(null); const { result } = renderHook(() => useAuth(authConfig)); mockGetCookieValue.mockReturnValue('visibleAuthToken'); Object.defineProperty(document, 'hidden', { configurable: true, value: false }); act(() => { document.dispatchEvent(new Event('visibilitychange')); }); await waitFor(() => { expect(result.current.authToken).toBe('visibleAuthToken'); expect(result.current.isAuthenticated).toBe(true); }); }); it('should clean up event listeners on unmount', async () => { const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); const { unmount } = renderHook(() => useAuth(authConfig)); unmount(); expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function)); }); it('should log error when authConfig is misconfigured', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const invalidConfig = { clientId: 'example-client-id', scope: 'offline' }; // @ts-ignore const { result } = renderHook(() => useAuth(invalidConfig)); result.current.login(); await waitFor(() => { expect(consoleSpy).toHaveBeenCalledWith('Auth config is misconfigured'); }); consoleSpy.mockRestore(); }); it('should handle missing refreshToken when logging in', async () => { jest.useFakeTimers(); const popupMock = { closed: false, close: jest.fn(), } as unknown as Window; jest.spyOn(window, 'open').mockImplementation(() => popupMock); mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce(null); const { result } = renderHook(() => useAuth(authConfig)); result.current.login(); act(() => { mockGetCookieValue.mockReturnValue('newAuthToken'); jest.advanceTimersByTime(1000); (popupMock as any).closed = true; }); await waitFor(() => { expect(result.current.authToken).toBe(null); expect(result.current.isAuthenticated).toBe(false); }); expect(popupMock.close).toHaveBeenCalled(); }); it('should throw an error when getCookieValue throws unexpectedly', () => { const getCookieValueSpy = jest.spyOn(authUtils, 'getCookieValue').mockImplementation(() => { throw new Error('Unexpected error'); }); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); expect(() => renderHook(() => useAuth(authConfig))).toThrow('Unexpected error'); consoleSpy.mockRestore(); getCookieValueSpy.mockRestore(); }); describe('alwaysUseResourceRequestProxy', () => { const authConfigAlwaysUseResourceRequestProxy = { ...authConfig, alwaysUseResourceRequestProxy: true }; it('should initialize with alwaysUseResourceRequestProxy true', async () => { const { result } = renderHook(() => useAuth(authConfigAlwaysUseResourceRequestProxy)); await waitFor(() => { expect(result.current.authToken).toBe(null); expect(result.current.isAuthenticated).toBe(true); expect(result.current.alwaysUseResourceRequestProxy).toBe(true); }); // Check that refreshAccessToken is not called when authConfig changes // Set up a spy and test that refreshAccessToken is not called when authConfig changes const refreshAccessTokenSpy = jest.spyOn(authUtils, 'refreshAccessToken'); // Change authConfig and rerender const newConfig = { ...authConfigAlwaysUseResourceRequestProxy, clientId: 'changed-client-id' }; const { rerender } = renderHook((props) => useAuth(props), { initialProps: authConfigAlwaysUseResourceRequestProxy }); rerender(newConfig); // refreshAccessToken should not be called expect(refreshAccessTokenSpy).not.toHaveBeenCalled(); }); it('should not attach event listeners when alwaysUseResourceRequestProxy is true', async () => { mockGetCookieValue.mockReturnValueOnce(null); const { result } = renderHook(() => useAuth(authConfigAlwaysUseResourceRequestProxy)); mockGetCookieValue.mockReturnValue('visibleAuthToken'); Object.defineProperty(document, 'hidden', { configurable: true, value: false }); act(() => { document.dispatchEvent(new Event('visibilitychange')); }); await waitFor(() => { expect(result.current.authToken).toBe(null); }); }); }); });