import { renderHook, waitFor, act } from '@testing-library/react'; import { AuthProvider, useAuth } from '@/lib/auth/AuthContext'; import apiClient from '@/lib/api/client'; import { useRouter } from 'next/navigation'; // Mock dependencies jest.mock('@/lib/api/client'); jest.mock('next/navigation', () => ({ useRouter: jest.fn(), })); const mockApiClient = apiClient as jest.Mocked; const mockUseRouter = useRouter as jest.MockedFunction; describe('AuthContext', () => { const mockRouter = { push: jest.fn(), replace: jest.fn(), refresh: jest.fn(), back: jest.fn(), forward: jest.fn(), prefetch: jest.fn(), }; beforeEach(() => { jest.clearAllMocks(); mockUseRouter.mockReturnValue(mockRouter as any); localStorage.clear(); }); describe('login', () => { it('should login successfully and store tokens', async () => { const mockUser = { id: 'usr_123', email: 'test@example.com', name: 'Test User', role: 'parent', }; const mockTokens = { accessToken: 'access_token_123', refreshToken: 'refresh_token_123', }; mockApiClient.post.mockResolvedValueOnce({ data: { success: true, data: { user: mockUser, tokens: mockTokens, }, }, }); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { await result.current.login({ email: 'test@example.com', password: 'password123', }); }); await waitFor(() => { expect(result.current.user).toEqual(mockUser); expect(result.current.isAuthenticated).toBe(true); expect(localStorage.setItem).toHaveBeenCalledWith('accessToken', mockTokens.accessToken); expect(localStorage.setItem).toHaveBeenCalledWith('refreshToken', mockTokens.refreshToken); expect(mockRouter.push).toHaveBeenCalledWith('/'); }); }); it('should handle login failure', async () => { mockApiClient.post.mockRejectedValueOnce({ response: { data: { message: 'Invalid credentials', }, }, }); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await expect( result.current.login({ email: 'test@example.com', password: 'wrong_password', }) ).rejects.toThrow('Invalid credentials'); expect(result.current.user).toBeNull(); expect(result.current.isAuthenticated).toBe(false); }); }); describe('register', () => { it('should register successfully and redirect to onboarding', async () => { const mockUser = { id: 'usr_123', email: 'newuser@example.com', name: 'New User', role: 'parent', }; const mockTokens = { accessToken: 'access_token_123', refreshToken: 'refresh_token_123', }; mockApiClient.post.mockResolvedValueOnce({ data: { success: true, data: { user: mockUser, tokens: mockTokens, family: { id: 'fam_123' }, }, }, }); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { await result.current.register({ email: 'newuser@example.com', password: 'password123', name: 'New User', }); }); await waitFor(() => { expect(result.current.user).toEqual(mockUser); expect(localStorage.setItem).toHaveBeenCalledWith('accessToken', mockTokens.accessToken); expect(mockRouter.push).toHaveBeenCalledWith('/onboarding'); }); }); it('should handle registration failure with invalid tokens', async () => { mockApiClient.post.mockResolvedValueOnce({ data: { success: true, data: { user: {}, tokens: {}, // Invalid tokens }, }, }); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await expect( result.current.register({ email: 'newuser@example.com', password: 'password123', name: 'New User', }) ).rejects.toThrow('Invalid response from server'); }); }); describe('logout', () => { it('should logout successfully and clear tokens', async () => { mockApiClient.post.mockResolvedValueOnce({ data: { success: true }, }); localStorage.setItem('accessToken', 'token_123'); localStorage.setItem('refreshToken', 'refresh_123'); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { await result.current.logout(); }); await waitFor(() => { expect(localStorage.removeItem).toHaveBeenCalledWith('accessToken'); expect(localStorage.removeItem).toHaveBeenCalledWith('refreshToken'); expect(result.current.user).toBeNull(); expect(mockRouter.push).toHaveBeenCalledWith('/login'); }); }); it('should clear tokens even if API call fails', async () => { mockApiClient.post.mockRejectedValueOnce(new Error('Network error')); localStorage.setItem('accessToken', 'token_123'); localStorage.setItem('refreshToken', 'refresh_123'); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { await result.current.logout(); }); await waitFor(() => { expect(localStorage.removeItem).toHaveBeenCalledWith('accessToken'); expect(localStorage.removeItem).toHaveBeenCalledWith('refreshToken'); expect(mockRouter.push).toHaveBeenCalledWith('/login'); }); }); }); describe('refreshUser', () => { it('should refresh user data successfully', async () => { const mockUser = { id: 'usr_123', email: 'test@example.com', name: 'Updated Name', role: 'parent', }; mockApiClient.get.mockResolvedValueOnce({ data: { data: mockUser, }, }); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { await result.current.refreshUser(); }); await waitFor(() => { expect(result.current.user).toEqual(mockUser); }); }); it('should handle refresh failure gracefully', async () => { mockApiClient.get.mockRejectedValueOnce(new Error('Unauthorized')); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await act(async () => { await result.current.refreshUser(); }); // User should remain null, no error thrown expect(result.current.user).toBeNull(); }); }); describe('authentication check', () => { it('should check auth on mount with valid token', async () => { const mockUser = { id: 'usr_123', email: 'test@example.com', name: 'Test User', role: 'parent', }; localStorage.setItem('accessToken', 'valid_token'); mockApiClient.get.mockResolvedValueOnce({ data: { data: mockUser, }, }); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); expect(result.current.user).toEqual(mockUser); expect(result.current.isAuthenticated).toBe(true); }); }); it('should handle auth check failure', async () => { localStorage.setItem('accessToken', 'invalid_token'); mockApiClient.get.mockRejectedValueOnce(new Error('Unauthorized')); const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); expect(result.current.user).toBeNull(); expect(localStorage.removeItem).toHaveBeenCalledWith('accessToken'); expect(localStorage.removeItem).toHaveBeenCalledWith('refreshToken'); }); }); it('should not check auth if no token exists', async () => { const { result } = renderHook(() => useAuth(), { wrapper: AuthProvider, }); await waitFor(() => { expect(result.current.isLoading).toBe(false); expect(result.current.user).toBeNull(); expect(mockApiClient.get).not.toHaveBeenCalled(); }); }); }); });