/** * TDD Tests for RouteGuard Component * * These tests verify the RouteGuard component logic following TDD principles. * Note: These are integration tests that verify the component behavior. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import { RouteGuard } from './RouteGuard'; import { useAuth } from '@/lib/auth/AuthContext'; import { useRouter } from 'next/navigation'; import type { AuthContextValue } from '@/lib/auth/AuthContext'; import type { SessionViewModel } from '@/lib/view-models/SessionViewModel'; // Mock dependencies vi.mock('@/lib/auth/AuthContext'); vi.mock('next/navigation'); // Mock SessionViewModel factory function createMockSession(overrides: Partial = {}): SessionViewModel { const baseSession = { isAuthenticated: true, userId: 'user-123', email: 'test@example.com', displayName: 'Test User', role: undefined, }; // Handle the case where overrides might have a user object // (for backward compatibility with existing test patterns) if (overrides.user) { const { user, ...rest } = overrides; return { ...baseSession, ...rest, userId: user.userId || baseSession.userId, email: user.email || baseSession.email, displayName: user.displayName || baseSession.displayName, role: user.role, }; } return { ...baseSession, ...overrides, }; } // Mock AuthContext factory function createMockAuthContext(overrides: Partial = {}): AuthContextValue { return { session: null, loading: false, login: vi.fn(), logout: vi.fn(), refreshSession: vi.fn(), ...overrides, }; } describe('RouteGuard', () => { const mockUseAuth = vi.mocked(useAuth); const mockUseRouter = vi.mocked(useRouter); let mockRouter: { push: ReturnType }; beforeEach(() => { mockRouter = { push: vi.fn() }; mockUseRouter.mockReturnValue(mockRouter as any); }); afterEach(() => { vi.clearAllMocks(); }); describe('Authentication State', () => { it('should render children when user is authenticated', async () => { const mockAuthContext = createMockAuthContext({ session: createMockSession(), loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(screen.getByTestId('protected-content')).toBeInTheDocument(); }); }); it('should show loading state when auth context is loading', () => { const mockAuthContext = createMockAuthContext({ session: null, loading: true, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); // Should show loading state, not children expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument(); }); it('should redirect when user is not authenticated', async () => { const mockAuthContext = createMockAuthContext({ session: null, loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(mockRouter.push).toHaveBeenCalledWith('/auth/login'); }); }); }); describe('Custom Configuration', () => { it('should use custom redirect path when specified', async () => { const mockAuthContext = createMockAuthContext({ session: null, loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(mockRouter.push).toHaveBeenCalledWith('/custom-login'); }); }); it('should not redirect when redirectOnUnauthorized is false', async () => { const mockAuthContext = createMockAuthContext({ session: null, loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); // Wait for any potential redirects await new Promise(resolve => setTimeout(resolve, 100)); expect(mockRouter.push).not.toHaveBeenCalled(); }); it('should show unauthorized component when redirect is disabled', async () => { const mockAuthContext = createMockAuthContext({ session: null, loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); const unauthorizedComponent =
Access Denied
; render(
Protected Content
); await waitFor(() => { expect(screen.getByTestId('unauthorized')).toBeInTheDocument(); }); }); }); describe('Custom Loading Component', () => { it('should show custom loading component when specified', () => { const mockAuthContext = createMockAuthContext({ session: null, loading: true, }); mockUseAuth.mockReturnValue(mockAuthContext); const loadingComponent =
Custom Loading...
; render(
Protected Content
); expect(screen.getByTestId('custom-loading')).toBeInTheDocument(); expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument(); }); }); describe('Role-Based Access', () => { it('should allow access when user has required role', async () => { const mockAuthContext = createMockAuthContext({ session: createMockSession({ user: { userId: 'user-123', email: 'admin@example.com', displayName: 'Admin User', role: 'admin', }, }), loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(screen.getByTestId('protected-content')).toBeInTheDocument(); }); }); it('should redirect when user lacks required role', async () => { const mockAuthContext = createMockAuthContext({ session: createMockSession({ user: { userId: 'user-123', email: 'user@example.com', displayName: 'Regular User', role: 'user', }, }), loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(mockRouter.push).toHaveBeenCalledWith('/auth/login'); }); }); }); describe('Edge Cases', () => { it('should handle undefined session gracefully', async () => { const mockAuthContext = createMockAuthContext({ session: undefined as any, loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(mockRouter.push).toHaveBeenCalledWith('/auth/login'); }); }); it('should handle empty required roles array', async () => { const mockAuthContext = createMockAuthContext({ session: createMockSession(), loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); await waitFor(() => { expect(screen.getByTestId('protected-content')).toBeInTheDocument(); }); }); it('should handle rapid session state changes', async () => { const mockAuthContext = createMockAuthContext({ session: null, loading: true, }); mockUseAuth.mockReturnValue(mockAuthContext); const { rerender } = render(
Protected Content
); // Simulate session becoming available mockAuthContext.session = createMockSession(); mockAuthContext.loading = false; rerender(
Protected Content
); await waitFor(() => { expect(screen.getByTestId('protected-content')).toBeInTheDocument(); }); }); }); describe('Redirect Timing', () => { it('should wait before redirecting (500ms delay)', async () => { const mockAuthContext = createMockAuthContext({ session: null, loading: false, }); mockUseAuth.mockReturnValue(mockAuthContext); render(
Protected Content
); // Should not redirect immediately expect(mockRouter.push).not.toHaveBeenCalled(); // Wait for the delay await waitFor(() => { expect(mockRouter.push).toHaveBeenCalledWith('/auth/login'); }, { timeout: 1000 }); }); }); });