UNPKG

prodobit

Version:

Open-core business application development platform

561 lines (451 loc) 17.2 kB
import React from 'react' import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { renderHook, act, waitFor } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useAuth } from '../useAuth.js' import { ProdobitProvider } from '../../providers/ProdobitProvider.js' import { ProdobitClient } from '@prodobit/sdk' import type { ReactNode } from 'react' // Mock the SDK client vi.mock('@prodobit/sdk') const MockProdobitClient = ProdobitClient as any describe('useAuth', () => { let queryClient: QueryClient let mockClient: any let wrapper: ({ children }: { children: ReactNode }) => JSX.Element beforeEach(() => { vi.clearAllMocks() // Create fresh query client for each test queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, mutations: { retry: false, }, }, }) // Mock ProdobitClient mockClient = { auth: { login: vi.fn(), register: vi.fn(), logout: vi.fn(), refresh: vi.fn(), getCurrentUser: vi.fn(), isAuthenticated: vi.fn() }, setAuthToken: vi.fn(), clearAuthToken: vi.fn() } MockProdobitClient.mockImplementation(() => mockClient) // Create wrapper with providers wrapper = ({ children }: { children: ReactNode }) => { return React.createElement( QueryClientProvider, { client: queryClient }, React.createElement( ProdobitProvider, { client: mockClient }, children ) ) } // Mock localStorage Object.defineProperty(window, 'localStorage', { value: { getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), clear: vi.fn() }, writable: true }) }) afterEach(() => { vi.restoreAllMocks() queryClient.clear() }) describe('useAuth hook', () => { it('should initialize with default state', () => { mockClient.auth.isAuthenticated.mockReturnValue(false) const { result } = renderHook(() => useAuth(), { wrapper }) expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() expect(result.current.isLoading).toBe(true) // Initially loading expect(result.current.error).toBeNull() expect(typeof result.current.login).toBe('function') expect(typeof result.current.logout).toBe('function') expect(typeof result.current.register).toBe('function') }) it('should initialize as authenticated when token exists', async () => { const mockUser = { id: 'user-123', email: 'test@example.com', firstName: 'John', lastName: 'Doe', tenantId: 'tenant-123', roles: ['admin'] } mockClient.auth.isAuthenticated.mockReturnValue(true) mockClient.auth.getCurrentUser.mockResolvedValue(mockUser) const { result } = renderHook(() => useAuth(), { wrapper }) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) expect(result.current.isAuthenticated).toBe(true) expect(result.current.user).toEqual(mockUser) }) }) describe('login functionality', () => { it('should login successfully', async () => { const mockLoginResponse = { user: { id: 'user-123', email: 'test@example.com', firstName: 'John', lastName: 'Doe', tenantId: 'tenant-123', roles: ['admin'] }, token: 'jwt-token-123' } mockClient.auth.login.mockResolvedValue(mockLoginResponse) mockClient.auth.isAuthenticated.mockReturnValue(true) const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { await result.current.login('test@example.com', 'password') }) expect(mockClient.auth.login).toHaveBeenCalledWith('test@example.com', 'password') expect(mockClient.setAuthToken).toHaveBeenCalledWith('jwt-token-123') expect(localStorage.setItem).toHaveBeenCalledWith('prodobit_token', 'jwt-token-123') await waitFor(() => { expect(result.current.isAuthenticated).toBe(true) expect(result.current.user).toEqual(mockLoginResponse.user) }) }) it('should handle login errors', async () => { const loginError = new Error('Invalid credentials') mockClient.auth.login.mockRejectedValue(loginError) const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { try { await result.current.login('test@example.com', 'wrongpassword') } catch (error) { // Expected error } }) expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() expect(result.current.error).toBe('Invalid credentials') }) it('should validate email format before login', async () => { const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { try { await result.current.login('invalid-email', 'password') } catch (error) { // Expected validation error } }) expect(mockClient.auth.login).not.toHaveBeenCalled() expect(result.current.error).toContain('Invalid email format') }) it('should validate password before login', async () => { const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { try { await result.current.login('test@example.com', '') } catch (error) { // Expected validation error } }) expect(mockClient.auth.login).not.toHaveBeenCalled() expect(result.current.error).toContain('Password is required') }) }) describe('register functionality', () => { const validRegisterData = { email: 'test@example.com', password: 'SecurePassword123!', firstName: 'John', lastName: 'Doe', tenantName: 'Test Company' } it('should register successfully', async () => { const mockRegisterResponse = { user: { id: 'user-123', email: 'test@example.com', firstName: 'John', lastName: 'Doe', tenantId: 'tenant-123', roles: ['admin'] }, token: 'jwt-token-123' } mockClient.auth.register.mockResolvedValue(mockRegisterResponse) mockClient.auth.isAuthenticated.mockReturnValue(true) const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { await result.current.register(validRegisterData) }) expect(mockClient.auth.register).toHaveBeenCalledWith(validRegisterData) expect(mockClient.setAuthToken).toHaveBeenCalledWith('jwt-token-123') expect(localStorage.setItem).toHaveBeenCalledWith('prodobit_token', 'jwt-token-123') await waitFor(() => { expect(result.current.isAuthenticated).toBe(true) expect(result.current.user).toEqual(mockRegisterResponse.user) }) }) it('should handle registration errors', async () => { const registerError = new Error('Email already exists') mockClient.auth.register.mockRejectedValue(registerError) const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { try { await result.current.register(validRegisterData) } catch (error) { // Expected error } }) expect(result.current.isAuthenticated).toBe(false) expect(result.current.error).toBe('Email already exists') }) it('should validate registration data', async () => { const invalidData = { ...validRegisterData, email: 'invalid-email', password: '123' // Too short } const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { try { await result.current.register(invalidData) } catch (error) { // Expected validation error } }) expect(mockClient.auth.register).not.toHaveBeenCalled() expect(result.current.error).toContain('Invalid') }) }) describe('logout functionality', () => { it('should logout successfully', async () => { // Setup authenticated state first const mockUser = { id: 'user-123', email: 'test@example.com', firstName: 'John', lastName: 'Doe', tenantId: 'tenant-123', roles: ['admin'] } mockClient.auth.isAuthenticated.mockReturnValue(true) mockClient.auth.getCurrentUser.mockResolvedValue(mockUser) mockClient.auth.logout.mockResolvedValue(undefined) const { result } = renderHook(() => useAuth(), { wrapper }) // Wait for initial load to complete await waitFor(() => { expect(result.current.isAuthenticated).toBe(true) }) // Now logout await act(async () => { await result.current.logout() }) expect(mockClient.auth.logout).toHaveBeenCalled() expect(mockClient.clearAuthToken).toHaveBeenCalled() expect(localStorage.removeItem).toHaveBeenCalledWith('prodobit_token') expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() }) it('should handle logout errors gracefully', async () => { const logoutError = new Error('Logout failed') mockClient.auth.logout.mockRejectedValue(logoutError) const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { await result.current.logout() }) // Even if logout fails, should clear local state expect(mockClient.clearAuthToken).toHaveBeenCalled() expect(localStorage.removeItem).toHaveBeenCalledWith('prodobit_token') expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() }) }) describe('token refresh functionality', () => { it('should refresh token automatically when expired', async () => { const newToken = 'new-jwt-token-456' const mockUser = { id: 'user-123', email: 'test@example.com', firstName: 'John', lastName: 'Doe', tenantId: 'tenant-123', roles: ['admin'] } // Mock initial authentication failure (expired token) mockClient.auth.getCurrentUser .mockRejectedValueOnce(new Error('Token expired')) .mockResolvedValue(mockUser) mockClient.auth.refresh.mockResolvedValue({ token: newToken }) localStorage.getItem = vi.fn().mockReturnValue('expired-token') const { result } = renderHook(() => useAuth(), { wrapper }) await waitFor(() => { expect(mockClient.auth.refresh).toHaveBeenCalledWith('expired-token') expect(mockClient.setAuthToken).toHaveBeenCalledWith(newToken) expect(localStorage.setItem).toHaveBeenCalledWith('prodobit_token', newToken) }) }) it('should logout when refresh fails', async () => { mockClient.auth.getCurrentUser.mockRejectedValue(new Error('Token expired')) mockClient.auth.refresh.mockRejectedValue(new Error('Refresh failed')) localStorage.getItem = vi.fn().mockReturnValue('expired-token') const { result } = renderHook(() => useAuth(), { wrapper }) await waitFor(() => { expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() expect(localStorage.removeItem).toHaveBeenCalledWith('prodobit_token') }) }) }) describe('loading states', () => { it('should show loading state during initial authentication check', () => { mockClient.auth.getCurrentUser.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve(null), 100)) ) const { result } = renderHook(() => useAuth(), { wrapper }) expect(result.current.isLoading).toBe(true) }) it('should show loading state during login', async () => { let resolveLogin: any const loginPromise = new Promise(resolve => { resolveLogin = resolve }) mockClient.auth.login.mockReturnValue(loginPromise) const { result } = renderHook(() => useAuth(), { wrapper }) act(() => { result.current.login('test@example.com', 'password') }) expect(result.current.isLoading).toBe(true) act(() => { resolveLogin({ user: { id: '123', email: 'test@example.com' }, token: 'token' }) }) await waitFor(() => { expect(result.current.isLoading).toBe(false) }) }) }) describe('error handling', () => { it('should clear errors on successful operations', async () => { const { result } = renderHook(() => useAuth(), { wrapper }) // First, cause an error mockClient.auth.login.mockRejectedValueOnce(new Error('Login failed')) await act(async () => { try { await result.current.login('test@example.com', 'wrongpassword') } catch (error) { // Expected error } }) expect(result.current.error).toBe('Login failed') // Now succeed mockClient.auth.login.mockResolvedValue({ user: { id: '123', email: 'test@example.com' }, token: 'token' }) await act(async () => { await result.current.login('test@example.com', 'correctpassword') }) expect(result.current.error).toBeNull() }) it('should handle network errors gracefully', async () => { const networkError = new Error('Network error') networkError.name = 'NetworkError' mockClient.auth.login.mockRejectedValue(networkError) const { result } = renderHook(() => useAuth(), { wrapper }) await act(async () => { try { await result.current.login('test@example.com', 'password') } catch (error) { // Expected error } }) expect(result.current.error).toContain('Network error') }) }) describe('permission checking', () => { it('should provide permission checking utilities', async () => { const mockUser = { id: 'user-123', email: 'test@example.com', firstName: 'John', lastName: 'Doe', tenantId: 'tenant-123', roles: ['admin', 'user'] } mockClient.auth.isAuthenticated.mockReturnValue(true) mockClient.auth.getCurrentUser.mockResolvedValue(mockUser) const { result } = renderHook(() => useAuth(), { wrapper }) await waitFor(() => { expect(result.current.user).toEqual(mockUser) }) // Test permission checking functions (if implemented) expect(result.current.hasRole?.('admin')).toBe(true) expect(result.current.hasRole?.('manager')).toBe(false) expect(result.current.hasAnyRole?.(['admin', 'manager'])).toBe(true) expect(result.current.hasAnyRole?.(['manager', 'supervisor'])).toBe(false) }) }) describe('edge cases', () => { it('should handle simultaneous login/logout calls', async () => { const { result } = renderHook(() => useAuth(), { wrapper }) mockClient.auth.login.mockResolvedValue({ user: { id: '123', email: 'test@example.com' }, token: 'token' }) // Simultaneous calls const loginPromise = act(async () => { await result.current.login('test@example.com', 'password') }) const logoutPromise = act(async () => { await result.current.logout() }) await Promise.all([loginPromise, logoutPromise]) // Should handle gracefully without errors expect(result.current.error).toBeNull() }) it('should handle component unmount during async operations', async () => { const { result, unmount } = renderHook(() => useAuth(), { wrapper }) const longRunningLogin = new Promise(resolve => setTimeout(() => resolve({ user: { id: '123', email: 'test@example.com' }, token: 'token' }), 1000) ) mockClient.auth.login.mockReturnValue(longRunningLogin) act(() => { result.current.login('test@example.com', 'password') }) // Unmount component before login completes unmount() // Should not cause memory leaks or errors await expect(longRunningLogin).resolves.toBeDefined() }) it('should handle malformed token in localStorage', () => { localStorage.getItem = vi.fn().mockReturnValue('malformed.token.here') mockClient.auth.getCurrentUser.mockRejectedValue(new Error('Invalid token')) mockClient.auth.refresh.mockRejectedValue(new Error('Refresh failed')) const { result } = renderHook(() => useAuth(), { wrapper }) // Should clear invalid token and reset auth state expect(localStorage.removeItem).toHaveBeenCalledWith('prodobit_token') expect(result.current.isAuthenticated).toBe(false) }) }) })