Compare commits

...

8 Commits

Author SHA1 Message Date
Felipe Cardoso
26d43ff9e1 Refactor useAuth imports to utilize AuthContext and enhance test store injection handling
- Replaced `useAuthStore` imports with `useAuth` from `AuthContext` in `AuthGuard` and `Header` for consistency.
- Enhanced `getAuthStore` to prioritize E2E test store injection for improved testability.
- Updated comments to reflect changes and clarify usage patterns.
2025-11-04 00:01:33 +01:00
Felipe Cardoso
4bf34ea287 Update tests to replace useAuthStore with useAuth from AuthContext
- Replaced legacy `useAuthStore` mocks with `useAuth` for consistency with the `AuthContext` pattern.
- Updated test cases in `Header` and `AuthGuard` to mock `AuthContext` instead of Zustand hooks.
- Improved test isolation by injecting `AuthProvider` where applicable.
2025-11-03 14:35:21 +01:00
Felipe Cardoso
852c7eceff Migrate auth hooks to AuthContext and update tests for compatibility
- Refactored `useIsAuthenticated` and `useCurrentUser` to use `useAuth` from `AuthContext` instead of `useAuthStore`.
- Updated test setups to inject `AuthProvider` with mocked store hooks for improved test isolation and consistency.
- Replaced legacy `useAuthStore` mocks with `AuthContext`-compatible implementations in affected tests.
2025-11-03 14:27:25 +01:00
Felipe Cardoso
532577f36c Mark Phase 2 as completed in AUTH_CONTEXT_MIGRATION_PLAN.md
- Updated the plan to reflect the completion of Phase 2 tasks, including the migration of Core Auth Components (`AuthGuard`, `Header`).
- Added detailed verification results, success criteria, and status for Task 2.1, 2.2, and 2.3.
- Highlighted the next steps for Phase 3 (migrating Auth hooks for testability).
2025-11-03 13:16:44 +01:00
Felipe Cardoso
9843cf8218 Refactor auth hooks and add database existence check during migrations
- Consolidated `useAuthStore` into the unified `useAuth` hook for cleaner imports and consistency across frontend components.
- Enhanced database management in Alembic migrations by introducing `ensure_database_exists` to automatically create the database if missing.
2025-11-03 13:16:34 +01:00
Felipe Cardoso
2ee48bf3fa Document common pitfalls for the frontend and enhance architecture guidelines
- Added `COMMON_PITFALLS.md` to document frequent mistakes and best practices in frontend development, focusing on React Hooks, Context API, Zustand patterns, TypeScript type safety, and more.
- Updated `ARCHITECTURE.md` with detailed insights on the `AuthContext` dependency injection pattern, including usage examples, provider tree structure, polymorphic hooks, and testing strategies.
- Emphasized compliance with React Rules of Hooks, performance optimizations, and separation of concerns in component design.
- Included implementation-ready examples, checklists, and resources to guide maintainable and testable frontend development.
2025-11-03 11:59:21 +01:00
Felipe Cardoso
a36c1b61bb Document Phase 1 lessons learned for AuthContext migration and update hooks for compliance with React Rules of Hooks
- Added detailed documentation in `AUTH_CONTEXT_MIGRATION_PLAN.md` for lessons learned during Phase 1 of the `AuthContext` migration.
- Highlighted critical implementation insights, including refactoring `useAuth` to call Zustand hooks internally, strict type safety with the `AuthState` interface, and dependency injection via `AuthProvider`.
- Defined updated architecture for provider placement and emphasized the importance of documentation, barrel exports, and hook compliance with React rules.
- Included comprehensive examples, verification checklists, common mistakes to avoid, and future-proofing guidelines.
2025-11-03 11:40:46 +01:00
Felipe Cardoso
0cba8ea62a Introduce AuthContext and refactor layout for dependency injection
- Added `AuthContext` as a dependency injection wrapper over the Zustand auth store to support test isolation, E2E testability, and clean architecture patterns.
- Updated `layout.tsx` to utilize `AuthProvider` and initialize authentication context.
- Removed redundant `AuthInitializer` from `providers.tsx`.
- Enhanced modularity and testability by decoupling authentication context from direct store dependency.
2025-11-03 11:33:39 +01:00
17 changed files with 2440 additions and 257 deletions

2
.gitignore vendored
View File

@@ -302,6 +302,6 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
*.iml
.junie/*
# Docker volumes
postgres_data*/

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,9 @@ import sys
from logging.config import fileConfig
from pathlib import Path
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlalchemy import engine_from_config, pool, text, create_engine
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import OperationalError
from alembic import context
@@ -35,6 +36,51 @@ target_metadata = Base.metadata
config.set_main_option("sqlalchemy.url", settings.database_url)
def ensure_database_exists(db_url: str) -> None:
"""
Ensure the target PostgreSQL database exists.
If connection to the target DB fails because it doesn't exist, connect to the
default 'postgres' database and create it. Safe to call multiple times.
"""
try:
# First, try connecting to the target database
test_engine = create_engine(db_url, poolclass=pool.NullPool)
with test_engine.connect() as conn:
conn.execute(text("SELECT 1"))
test_engine.dispose()
return
except OperationalError:
# Likely the database does not exist; proceed to create it
pass
url = make_url(db_url)
# Only handle PostgreSQL here
if url.get_backend_name() != "postgresql":
return
target_db = url.database
if not target_db:
return
# Build admin URL pointing to the default 'postgres' database
admin_url = url.set(database="postgres")
# CREATE DATABASE cannot run inside a transaction
admin_engine = create_engine(str(admin_url), isolation_level="AUTOCOMMIT", poolclass=pool.NullPool)
try:
with admin_engine.connect() as conn:
exists = conn.execute(
text("SELECT 1 FROM pg_database WHERE datname = :dbname"),
{"dbname": target_db},
).scalar()
if not exists:
# Quote the database name safely
dbname_quoted = '"' + target_db.replace('"', '""') + '"'
conn.execute(text(f"CREATE DATABASE {dbname_quoted}"))
finally:
admin_engine.dispose()
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
@@ -66,6 +112,9 @@ def run_migrations_online() -> None:
and associate a connection with the context.
"""
# Ensure the target database exists (handles first-run cases)
ensure_database_exists(settings.database_url)
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",

View File

@@ -463,7 +463,242 @@ interface UIStore {
## 6. Authentication Architecture
### 6.1 Token Management Strategy
### 6.1 Context-Based Dependency Injection Pattern
**Architecture Overview:**
This project uses a **hybrid authentication pattern** combining Zustand for state management and React Context for dependency injection. This provides the best of both worlds:
```
Component → useAuth() hook → AuthContext → Zustand Store → Storage Layer → Crypto (AES-GCM)
Injectable for tests
Production: Real store | Tests: Mock store
```
**Why This Pattern?**
**Benefits:**
- **Testable**: E2E tests can inject mock stores without backend
- **Performant**: Zustand handles state efficiently, Context is just a thin wrapper
- **Type-safe**: Full TypeScript inference throughout
- **Maintainable**: Clear separation (Context = DI, Zustand = state)
- **Extensible**: Easy to add auth events, middleware, logging
- **React-idiomatic**: Follows React best practices
**Key Design Principles:**
1. **Thin Context Layer**: Context only provides dependency injection, no business logic
2. **Zustand for State**: All state management stays in Zustand (no duplicated state)
3. **Backward Compatible**: Internal refactor only, no API changes
4. **Type Safe**: Context interface exactly matches Zustand store interface
5. **Performance**: Context value is stable (no unnecessary re-renders)
### 6.2 Implementation Components
#### AuthContext Provider (`src/lib/auth/AuthContext.tsx`)
**Purpose**: Wraps Zustand store in React Context for dependency injection
```typescript
// Accepts optional store prop for testing
<AuthProvider store={mockStore}> // Unit tests
<App />
</AuthProvider>
// Or checks window global for E2E tests
window.__TEST_AUTH_STORE__ = mockStoreHook;
// Or uses production singleton (default)
<AuthProvider>
<App />
</AuthProvider>
```
**Implementation Details:**
- Stores Zustand hook function (not state) in Context
- Priority: explicit prop → E2E test store → production singleton
- Type-safe window global extension for E2E injection
- Calls hook internally (follows React Rules of Hooks)
#### useAuth Hook (Polymorphic)
**Supports two usage patterns:**
```typescript
// Pattern 1: Full state access (simple)
const { user, isAuthenticated } = useAuth();
// Pattern 2: Selector (optimized for performance)
const user = useAuth(state => state.user);
```
**Why Polymorphic?**
- Simple pattern for most use cases
- Optimized pattern available when needed
- Type-safe with function overloads
- No performance overhead
**Critical Implementation Detail:**
```typescript
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
}
// CRITICAL: Call the hook internally (follows React Rules of Hooks)
return selector ? storeHook(selector) : storeHook();
}
```
**Do NOT** return the hook function itself - this violates React Rules of Hooks!
### 6.3 Usage Patterns
#### For Components (Rendering Auth State)
**Use `useAuth()` from Context:**
```typescript
import { useAuth } from '@/lib/stores';
function MyComponent() {
// Full state access
const { user, isAuthenticated } = useAuth();
// Or with selector for optimization
const user = useAuth(state => state.user);
if (!isAuthenticated) {
return <LoginPrompt />;
}
return <div>Hello, {user?.first_name}!</div>;
}
```
**Why?**
- Component re-renders when auth state changes
- Type-safe access to all state properties
- Clean, idiomatic React code
#### For Mutation Callbacks (Updating Auth State)
**Use `useAuthStore.getState()` directly:**
```typescript
import { useAuthStore } from '@/lib/stores/authStore';
export function useLogin() {
return useMutation({
mutationFn: async (data) => {
const response = await loginAPI(data);
// Access store directly in callback (outside render)
const setAuth = useAuthStore.getState().setAuth;
await setAuth(response.user, response.token);
},
});
}
```
**Why?**
- Event handlers run outside React render cycle
- Don't need to re-render when state changes
- Using `getState()` directly is cleaner
- Avoids unnecessary hook rules complexity
#### Admin-Only Features
```typescript
import { useAuth } from '@/lib/stores';
function AdminPanel() {
const user = useAuth(state => state.user);
const isAdmin = user?.is_superuser ?? false;
if (!isAdmin) {
return <AccessDenied />;
}
return <AdminDashboard />;
}
```
### 6.4 Testing Integration
#### Unit Tests (Jest)
```typescript
import { useAuth } from '@/lib/stores';
jest.mock('@/lib/stores', () => ({
useAuth: jest.fn(),
}));
test('renders user name', () => {
(useAuth as jest.Mock).mockReturnValue({
user: { first_name: 'John', last_name: 'Doe' },
isAuthenticated: true,
});
render(<MyComponent />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
```
#### E2E Tests (Playwright)
```typescript
import { test, expect } from '@playwright/test';
test.describe('Protected Pages', () => {
test.beforeEach(async ({ page }) => {
// Inject mock store before navigation
await page.addInitScript(() => {
(window as any).__TEST_AUTH_STORE__ = () => ({
user: { id: '1', email: 'test@example.com', first_name: 'Test', last_name: 'User' },
accessToken: 'mock-token',
refreshToken: 'mock-refresh',
isAuthenticated: true,
isLoading: false,
tokenExpiresAt: Date.now() + 900000,
});
});
});
test('should display user profile', async ({ page }) => {
await page.goto('/settings/profile');
// No redirect to login - authenticated via mock
await expect(page).toHaveURL('/settings/profile');
await expect(page.locator('input[name="email"]')).toHaveValue('test@example.com');
});
});
```
### 6.5 Provider Tree Structure
**Correct Order** (Critical for Functionality):
```typescript
// src/app/layout.tsx
<AuthProvider> {/* 1. Provides auth DI layer */}
<AuthInitializer /> {/* 2. Loads auth from storage (needs AuthProvider) */}
<Providers> {/* 3. Other providers (Theme, Query) */}
{children}
</Providers>
</AuthProvider>
```
**Why This Order?**
- AuthProvider must wrap AuthInitializer (AuthInitializer uses auth state)
- AuthProvider should wrap all app providers (auth available everywhere)
- Keep provider tree shallow for performance
### 6.6 Token Management Strategy
**Two-Token System:**
- **Access Token**: Short-lived (15 min), stored in memory/sessionStorage

View File

@@ -0,0 +1,861 @@
# Frontend Common Pitfalls & Solutions
**Project**: Next.js + FastAPI Template
**Version**: 1.0
**Last Updated**: 2025-11-03
**Status**: Living Document
---
## Table of Contents
1. [React Hooks](#1-react-hooks)
2. [Context API & State Management](#2-context-api--state-management)
3. [Zustand Store Patterns](#3-zustand-store-patterns)
4. [TypeScript Type Safety](#4-typescript-type-safety)
5. [Component Patterns](#5-component-patterns)
6. [Provider Architecture](#6-provider-architecture)
7. [Event Handlers & Callbacks](#7-event-handlers--callbacks)
8. [Testing Pitfalls](#8-testing-pitfalls)
9. [Performance](#9-performance)
10. [Import/Export Patterns](#10-importexport-patterns)
---
## 1. React Hooks
### Pitfall 1.1: Returning Hook Function Instead of Calling It
**❌ WRONG:**
```typescript
// Custom hook that wraps Zustand
export function useAuth() {
const storeHook = useContext(AuthContext);
return storeHook; // Returns the hook function itself!
}
// Consumer component
function MyComponent() {
const authHook = useAuth(); // Got the hook function
const { user } = authHook(); // Have to call it here ❌ Rules of Hooks violation!
}
```
**Why It's Wrong:**
- Violates React Rules of Hooks (hook called conditionally/in wrong place)
- Confusing API for consumers
- Can't use in conditionals or callbacks safely
- Type inference breaks
**✅ CORRECT:**
```typescript
// Custom hook that calls the wrapped hook internally
export function useAuth() {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
}
return storeHook(); // Call the hook HERE, return the state
}
// Consumer component
function MyComponent() {
const { user } = useAuth(); // Direct access to state ✅
}
```
**✅ EVEN BETTER (Polymorphic):**
```typescript
// Support both patterns
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
}
return selector ? storeHook(selector) : storeHook();
}
// Usage - both work!
const { user } = useAuth(); // Full state
const user = useAuth(s => s.user); // Optimized selector
```
**Key Takeaway:**
- **Always call hooks internally in custom hooks**
- Return state/values, not hook functions
- Support selectors for performance optimization
---
### Pitfall 1.2: Calling Hooks Conditionally
**❌ WRONG:**
```typescript
function MyComponent({ showUser }) {
if (showUser) {
const { user } = useAuth(); // ❌ Conditional hook call!
return <div>{user?.name}</div>;
}
return null;
}
```
**✅ CORRECT:**
```typescript
function MyComponent({ showUser }) {
const { user } = useAuth(); // ✅ Always call at top level
if (!showUser) {
return null;
}
return <div>{user?.name}</div>;
}
```
**Key Takeaway:**
- **Always call hooks at the top level of your component**
- Never call hooks inside conditionals, loops, or nested functions
- Return early after hooks are called
---
## 2. Context API & State Management
### Pitfall 2.1: Creating New Context Value on Every Render
**❌ WRONG:**
```typescript
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
// New object created every render! ❌
const value = { user, setUser };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
```
**Why It's Wrong:**
- Every render creates a new object
- All consumers re-render even if values unchanged
- Performance nightmare in large apps
**✅ CORRECT:**
```typescript
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
// Memoize value - only changes when dependencies change
const value = useMemo(() => ({ user, setUser }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
```
**✅ EVEN BETTER (Zustand + Context):**
```typescript
export function AuthProvider({ children, store }) {
// Zustand hook function is stable (doesn't change)
const authStore = store ?? useAuthStoreImpl;
// No useMemo needed - hook functions are stable references
return <AuthContext.Provider value={authStore}>{children}</AuthContext.Provider>;
}
```
**Key Takeaway:**
- **Use `useMemo` for Context values that are objects**
- Or use stable references (Zustand hooks, refs)
- Monitor re-renders with React DevTools
---
### Pitfall 2.2: Prop Drilling Instead of Context
**❌ WRONG:**
```typescript
// Passing through 5 levels
<Layout user={user}>
<Sidebar user={user}>
<Navigation user={user}>
<UserMenu user={user}>
<Avatar user={user} />
</UserMenu>
</Navigation>
</Sidebar>
</Layout>
```
**✅ CORRECT:**
```typescript
// Provider at top
<AuthProvider>
<Layout>
<Sidebar>
<Navigation>
<UserMenu>
<Avatar /> {/* Gets user from useAuth() */}
</UserMenu>
</Navigation>
</Sidebar>
</Layout>
</AuthProvider>
```
**Key Takeaway:**
- **Use Context for data needed by many components**
- Avoid prop drilling beyond 2-3 levels
- But don't overuse - local state is often better
---
## 3. Zustand Store Patterns
### Pitfall 3.1: Mixing Render State Access and Mutation Logic
**❌ WRONG (Mixing patterns):**
```typescript
function MyComponent() {
// Using hook for render state
const { user } = useAuthStore();
const handleLogin = async (data) => {
// Also using hook in callback ❌ Inconsistent!
const setAuth = useAuthStore((s) => s.setAuth);
await setAuth(data.user, data.token);
};
}
```
**✅ CORRECT (Separate patterns):**
```typescript
function MyComponent() {
// Hook for render state (subscribes to changes)
const { user } = useAuthStore();
const handleLogin = async (data) => {
// getState() for mutations (no subscription)
const setAuth = useAuthStore.getState().setAuth;
await setAuth(data.user, data.token);
};
}
```
**Why This Pattern?**
- **Render state**: Use hook → component re-renders on changes
- **Mutations**: Use `getState()` → no subscription, no re-renders
- **Performance**: Event handlers don't need to subscribe
- **Clarity**: Clear distinction between read and write
**Key Takeaway:**
- **Use hooks for state that affects rendering**
- **Use `getState()` for mutations in callbacks**
- Don't subscribe when you don't need to
---
### Pitfall 3.2: Not Using Selectors for Optimization
**❌ SUBOPTIMAL:**
```typescript
function UserAvatar() {
// Re-renders on ANY auth state change! ❌
const { user, accessToken, isLoading, isAuthenticated } = useAuthStore();
return <Avatar src={user?.avatar} />;
}
```
**✅ OPTIMIZED:**
```typescript
function UserAvatar() {
// Only re-renders when user changes ✅
const user = useAuthStore((state) => state.user);
return <Avatar src={user?.avatar} />;
}
```
**Key Takeaway:**
- **Use selectors for components that only need subset of state**
- Reduces unnecessary re-renders
- Especially important in frequently updating stores
---
## 4. TypeScript Type Safety
### Pitfall 4.1: Using `any` Type
**❌ WRONG:**
```typescript
function processUser(user: any) { // ❌ Loses all type safety
return user.name.toUpperCase(); // No error if user.name is undefined
}
```
**✅ CORRECT:**
```typescript
function processUser(user: User | null) {
if (!user?.name) {
return '';
}
return user.name.toUpperCase();
}
```
**Key Takeaway:**
- **Never use `any` - use `unknown` if type is truly unknown**
- Define proper types for all function parameters
- Use type guards for runtime checks
---
### Pitfall 4.2: Implicit Types Leading to Errors
**❌ WRONG:**
```typescript
// No explicit return type - type inference can be wrong
export function useAuth() {
const context = useContext(AuthContext);
return context; // What type is this? ❌
}
```
**✅ CORRECT:**
```typescript
// Explicit return type with overloads
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return selector ? context(selector) : context();
}
```
**Key Takeaway:**
- **Always provide explicit return types for public APIs**
- Use function overloads for polymorphic functions
- Document types in JSDoc comments
---
### Pitfall 4.3: Not Using `import type` for Type-Only Imports
**❌ SUBOPTIMAL:**
```typescript
import { ReactNode } from 'react'; // Might be bundled even if only used for types
```
**✅ CORRECT:**
```typescript
import type { ReactNode } from 'react'; // Guaranteed to be stripped from bundle
```
**Key Takeaway:**
- **Use `import type` for type-only imports**
- Smaller bundle size
- Clearer intent
---
## 5. Component Patterns
### Pitfall 5.1: Forgetting Optional Chaining for Nullable Values
**❌ WRONG:**
```typescript
function UserProfile() {
const { user } = useAuth();
return <div>{user.name}</div>; // ❌ Crashes if user is null
}
```
**✅ CORRECT:**
```typescript
function UserProfile() {
const { user } = useAuth();
if (!user) {
return <div>Not logged in</div>;
}
return <div>{user.name}</div>; // ✅ Safe
}
// OR with optional chaining
function UserProfile() {
const { user } = useAuth();
return <div>{user?.name ?? 'Guest'}</div>; // ✅ Safe
}
```
**Key Takeaway:**
- **Always handle null/undefined cases**
- Use optional chaining (`?.`) and nullish coalescing (`??`)
- Provide fallback UI for missing data
---
### Pitfall 5.2: Mixing Concerns in Components
**❌ WRONG:**
```typescript
function UserDashboard() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
// Data fetching mixed with component logic ❌
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data))
.finally(() => setLoading(false));
}, []);
// Business logic mixed with rendering ❌
const activeUsers = users.filter(u => u.isActive);
const sortedUsers = activeUsers.sort((a, b) => a.name.localeCompare(b.name));
return <div>{/* Render sortedUsers */}</div>;
}
```
**✅ CORRECT:**
```typescript
// Custom hook for data fetching
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => UserService.getUsers(),
});
}
// Custom hook for business logic
function useActiveUsersSorted(users: User[] | undefined) {
return useMemo(() => {
if (!users) return [];
return users
.filter(u => u.isActive)
.sort((a, b) => a.name.localeCompare(b.name));
}, [users]);
}
// Component only handles rendering
function UserDashboard() {
const { data: users, isLoading } = useUsers();
const sortedUsers = useActiveUsersSorted(users);
if (isLoading) return <LoadingSpinner />;
return <div>{/* Render sortedUsers */}</div>;
}
```
**Key Takeaway:**
- **Separate concerns: data fetching, business logic, rendering**
- Extract logic to custom hooks
- Keep components focused on UI
---
## 6. Provider Architecture
### Pitfall 6.1: Wrong Provider Order
**❌ WRONG:**
```typescript
// AuthInitializer outside AuthProvider ❌
function RootLayout({ children }) {
return (
<Providers>
<AuthInitializer /> {/* Can't access auth context! */}
<AuthProvider>
{children}
</AuthProvider>
</Providers>
);
}
```
**✅ CORRECT:**
```typescript
function RootLayout({ children }) {
return (
<AuthProvider> {/* Provider first */}
<AuthInitializer /> {/* Can access auth context */}
<Providers>
{children}
</Providers>
</AuthProvider>
);
}
```
**Key Takeaway:**
- **Providers must wrap components that use them**
- Order matters when there are dependencies
- Keep provider tree shallow (performance)
---
### Pitfall 6.2: Creating Too Many Providers
**❌ WRONG:**
```typescript
// Separate provider for every piece of state ❌
<UserProvider>
<ThemeProvider>
<LanguageProvider>
<NotificationProvider>
<SettingsProvider>
<App />
</SettingsProvider>
</NotificationProvider>
</LanguageProvider>
</ThemeProvider>
</UserProvider>
```
**✅ BETTER:**
```typescript
// Combine related state, use Zustand for most things
<AuthProvider> {/* Only for auth DI */}
<ThemeProvider> {/* Built-in from lib */}
<QueryClientProvider> {/* React Query */}
<App />
</QueryClientProvider>
</ThemeProvider>
</AuthProvider>
// Most other state in Zustand stores (no providers needed)
const useUIStore = create(...); // Theme, sidebar, modals
const useUserPreferences = create(...); // User settings
```
**Key Takeaway:**
- **Use Context only when necessary** (DI, third-party integrations)
- **Use Zustand for most global state** (no provider needed)
- Avoid provider hell
---
## 7. Event Handlers & Callbacks
### Pitfall 7.1: Using Hooks in Event Handlers
**❌ WRONG:**
```typescript
function MyComponent() {
const handleClick = () => {
const { user } = useAuth(); // ❌ Hook called in callback!
console.log(user);
};
return <button onClick={handleClick}>Click</button>;
}
```
**✅ CORRECT:**
```typescript
function MyComponent() {
const { user } = useAuth(); // ✅ Hook at component top level
const handleClick = () => {
console.log(user); // Access from closure
};
return <button onClick={handleClick}>Click</button>;
}
// OR for mutations, use getState()
function MyComponent() {
const handleLogout = async () => {
const clearAuth = useAuthStore.getState().clearAuth; // ✅ Not a hook call
await clearAuth();
};
return <button onClick={handleLogout}>Logout</button>;
}
```
**Key Takeaway:**
- **Never call hooks inside event handlers**
- For render state: Call hook at top level, access in closure
- For mutations: Use `store.getState().method()`
---
### Pitfall 7.2: Not Handling Async Errors in Event Handlers
**❌ WRONG:**
```typescript
const handleSubmit = async (data: FormData) => {
await apiCall(data); // ❌ No error handling!
};
```
**✅ CORRECT:**
```typescript
const handleSubmit = async (data: FormData) => {
try {
await apiCall(data);
toast.success('Success!');
} catch (error) {
console.error('Failed to submit:', error);
toast.error('Failed to submit form');
}
};
```
**Key Takeaway:**
- **Always wrap async calls in try/catch**
- Provide user feedback for both success and errors
- Log errors for debugging
---
## 8. Testing Pitfalls
### Pitfall 8.1: Not Mocking Context Providers in Tests
**❌ WRONG:**
```typescript
// Test without provider ❌
test('renders user name', () => {
render(<UserProfile />); // Will crash - no AuthProvider!
expect(screen.getByText('John')).toBeInTheDocument();
});
```
**✅ CORRECT:**
```typescript
// Mock the hook
jest.mock('@/lib/stores', () => ({
useAuth: jest.fn(),
}));
test('renders user name', () => {
(useAuth as jest.Mock).mockReturnValue({
user: { id: '1', name: 'John' },
isAuthenticated: true,
});
render(<UserProfile />);
expect(screen.getByText('John')).toBeInTheDocument();
});
```
**Key Takeaway:**
- **Mock hooks at module level in tests**
- Provide necessary return values for each test case
- Test both success and error states
---
### Pitfall 8.2: Testing Implementation Details
**❌ WRONG:**
```typescript
test('calls useAuthStore hook', () => {
const spy = jest.spyOn(require('@/lib/stores'), 'useAuthStore');
render(<MyComponent />);
expect(spy).toHaveBeenCalled(); // ❌ Testing implementation!
});
```
**✅ CORRECT:**
```typescript
test('displays user name when authenticated', () => {
(useAuth as jest.Mock).mockReturnValue({
user: { name: 'John' },
isAuthenticated: true,
});
render(<MyComponent />);
expect(screen.getByText('John')).toBeInTheDocument(); // ✅ Testing behavior!
});
```
**Key Takeaway:**
- **Test behavior, not implementation**
- Focus on what the user sees/does
- Don't test internal API calls unless critical
---
## 9. Performance
### Pitfall 9.1: Not Using React.memo for Expensive Components
**❌ SUBOPTIMAL:**
```typescript
// Re-renders every time parent re-renders ❌
function ExpensiveChart({ data }) {
// Heavy computation/rendering
return <ComplexVisualization data={data} />;
}
```
**✅ OPTIMIZED:**
```typescript
// Only re-renders when data changes ✅
export const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
return <ComplexVisualization data={data} />;
});
```
**Key Takeaway:**
- **Use `React.memo` for expensive components**
- Especially useful for list items, charts, heavy UI
- Profile with React DevTools to identify candidates
---
### Pitfall 9.2: Creating Functions Inside Render
**❌ SUBOPTIMAL:**
```typescript
function MyComponent() {
return (
<button onClick={() => console.log('clicked')}> {/* New function every render */}
Click
</button>
);
}
```
**✅ OPTIMIZED:**
```typescript
function MyComponent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <button onClick={handleClick}>Click</button>;
}
```
**When to Optimize:**
- **For memoized child components** (memo, PureComponent)
- **For expensive event handlers**
- **When profiling shows performance issues**
**When NOT to optimize:**
- **Simple components with cheap operations** (premature optimization)
- **One-off event handlers**
**Key Takeaway:**
- **Use `useCallback` for functions passed to memoized children**
- But don't optimize everything - profile first
---
## 10. Import/Export Patterns
### Pitfall 10.1: Not Using Barrel Exports
**❌ INCONSISTENT:**
```typescript
// Deep imports all over the codebase
import { useAuth } from '@/lib/auth/AuthContext';
import { useAuthStore } from '@/lib/stores/authStore';
import { User } from '@/lib/stores/authStore';
```
**✅ CONSISTENT:**
```typescript
// Barrel exports in stores/index.ts
export { useAuth, AuthProvider } from '../auth/AuthContext';
export { useAuthStore, type User } from './authStore';
// Clean imports everywhere
import { useAuth, useAuthStore, User } from '@/lib/stores';
```
**Key Takeaway:**
- **Create barrel exports (index.ts) for public APIs**
- Easier to refactor internal structure
- Consistent import paths across codebase
---
### Pitfall 10.2: Circular Dependencies
**❌ WRONG:**
```typescript
// fileA.ts
import { functionB } from './fileB';
export function functionA() { return functionB(); }
// fileB.ts
import { functionA } from './fileA'; // ❌ Circular!
export function functionB() { return functionA(); }
```
**✅ CORRECT:**
```typescript
// utils.ts
export function sharedFunction() { /* shared logic */ }
// fileA.ts
import { sharedFunction } from './utils';
export function functionA() { return sharedFunction(); }
// fileB.ts
import { sharedFunction } from './utils';
export function functionB() { return sharedFunction(); }
```
**Key Takeaway:**
- **Avoid circular imports**
- Extract shared code to separate modules
- Keep dependency graph acyclic
---
## Verification Checklist
Before committing code, always run:
```bash
# Type checking
npm run type-check
# Linting
npm run lint
# Tests
npm test
# Build check
npm run build
```
**In browser:**
- [ ] No console errors or warnings
- [ ] Components render correctly
- [ ] No infinite loops or excessive re-renders (React DevTools)
- [ ] Proper error handling (test error states)
---
## Additional Resources
- [React Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)
- [Zustand Best Practices](https://docs.pmnd.rs/zustand/guides/practice-with-no-store-actions)
- [TypeScript Best Practices](https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html)
- [Testing Library Best Practices](https://testing-library.com/docs/queries/about#priority)
---
**Last Updated**: 2025-11-03
**Maintainer**: Development Team
**Status**: Living Document - Add new pitfalls as they're discovered

View File

@@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
import { AuthProvider } from "@/lib/auth/AuthContext";
import { AuthInitializer } from "@/components/auth";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -58,7 +60,10 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<AuthProvider>
<AuthInitializer />
<Providers>{children}</Providers>
</AuthProvider>
</body>
</html>
);

View File

@@ -3,7 +3,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { lazy, Suspense, useState } from 'react';
import { ThemeProvider } from '@/components/theme';
import { AuthInitializer } from '@/components/auth';
// Lazy load devtools - only in local development (not in Docker), never in production
// Set NEXT_PUBLIC_ENABLE_DEVTOOLS=true in .env.local to enable
@@ -39,7 +38,6 @@ export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<AuthInitializer />
{children}
{ReactQueryDevtools && (
<Suspense fallback={null}>

View File

@@ -8,7 +8,7 @@
import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useAuthStore } from '@/lib/stores/authStore';
import { useAuth } from '@/lib/auth/AuthContext';
import { useMe } from '@/lib/api/hooks/useAuth';
import { AuthLoadingSkeleton } from '@/components/layout';
import config from '@/config/app.config';
@@ -50,7 +50,7 @@ interface AuthGuardProps {
export function AuthGuard({ children, requireAdmin = false, fallback }: AuthGuardProps) {
const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, isLoading: authLoading, user } = useAuthStore();
const { isAuthenticated, isLoading: authLoading, user } = useAuth();
// Delayed loading state - only show skeleton after 150ms to avoid flicker on fast loads
const [showLoading, setShowLoading] = useState(false);

View File

@@ -8,7 +8,7 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useAuthStore } from '@/lib/stores/authStore';
import { useAuth } from '@/lib/auth/AuthContext';
import { useLogout } from '@/lib/api/hooks/useAuth';
import {
DropdownMenu,
@@ -67,7 +67,7 @@ function NavLink({
}
export function Header() {
const { user } = useAuthStore();
const { user } = useAuth();
const { mutate: logout, isPending: isLoggingOut } = useLogout();
const handleLogout = () => {

View File

@@ -28,11 +28,20 @@ let refreshPromise: Promise<string> | null = null;
/**
* Auth store accessor
* Dynamically imported to avoid circular dependencies
* Checks for E2E test store injection before using production store
*
* Note: Tested via E2E tests when interceptors are invoked
*/
/* istanbul ignore next */
const getAuthStore = async () => {
// Check for E2E test store injection (same pattern as AuthProvider)
if (typeof window !== 'undefined' && (window as any).__TEST_AUTH_STORE__) {
const testStore = (window as any).__TEST_AUTH_STORE__;
// Test store must have getState() method for non-React contexts
return testStore.getState();
}
// Production: use real Zustand store
const { useAuthStore } = await import('@/lib/stores/authStore');
return useAuthStore.getState();
};

View File

@@ -22,6 +22,7 @@ import {
} from '../client';
import { useAuthStore } from '@/lib/stores/authStore';
import type { User } from '@/lib/stores/authStore';
import { useAuth } from '@/lib/auth/AuthContext';
import { parseAPIError, getGeneralError } from '../errors';
import { isTokenWithUser } from '../types';
import config from '@/config/app.config';
@@ -481,7 +482,7 @@ export function usePasswordChange(onSuccess?: (message: string) => void) {
* @returns boolean indicating authentication status
*/
export function useIsAuthenticated(): boolean {
return useAuthStore((state) => state.isAuthenticated);
return useAuth((state) => state.isAuthenticated);
}
/**
@@ -489,7 +490,7 @@ export function useIsAuthenticated(): boolean {
* @returns Current user or null
*/
export function useCurrentUser(): User | null {
return useAuthStore((state) => state.user);
return useAuth((state) => state.user);
}
/**

View File

@@ -0,0 +1,149 @@
/**
* Authentication Context - Dependency Injection Wrapper for Auth Store
*
* Provides a thin Context layer over Zustand auth store to enable:
* - Test isolation (inject mock stores)
* - E2E testing without backend
* - Clean architecture (DI pattern)
*
* Design: Context handles dependency injection, Zustand handles state management
*/
"use client";
import { createContext, useContext } from "react";
import type { ReactNode } from "react";
import { useAuthStore as useAuthStoreImpl } from "@/lib/stores/authStore";
import type { User } from "@/lib/stores/authStore";
/**
* Authentication state shape
* Matches the Zustand store interface exactly
*/
interface AuthState {
// State
user: User | null;
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
isLoading: boolean;
tokenExpiresAt: number | null;
// Actions
setAuth: (user: User, accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
setTokens: (accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
setUser: (user: User) => void;
clearAuth: () => Promise<void>;
loadAuthFromStorage: () => Promise<void>;
isTokenExpired: () => boolean;
}
/**
* Type of the Zustand hook function
* Used for Context storage and test injection
*/
type AuthStoreHook = typeof useAuthStoreImpl;
/**
* Global window extension for E2E test injection
* E2E tests can set window.__TEST_AUTH_STORE__ before navigation
*/
declare global {
interface Window {
__TEST_AUTH_STORE__?: AuthStoreHook;
}
}
const AuthContext = createContext<AuthStoreHook | null>(null);
interface AuthProviderProps {
children: ReactNode;
/**
* Optional store override for testing
* Used in unit tests to inject mock store
*/
store?: AuthStoreHook;
}
/**
* Authentication Context Provider
*
* Wraps Zustand auth store in React Context for dependency injection.
* Enables test isolation by allowing mock stores to be injected via:
* 1. `store` prop (unit tests)
* 2. `window.__TEST_AUTH_STORE__` (E2E tests)
* 3. Production singleton (default)
*
* @example
* ```tsx
* // In root layout
* <AuthProvider>
* <App />
* </AuthProvider>
*
* // In unit tests
* <AuthProvider store={mockStore}>
* <ComponentUnderTest />
* </AuthProvider>
*
* // In E2E tests (before navigation)
* window.__TEST_AUTH_STORE__ = mockAuthStoreHook;
* ```
*/
export function AuthProvider({ children, store }: AuthProviderProps) {
// Check for E2E test store injection (SSR-safe)
const testStore =
typeof window !== "undefined" && window.__TEST_AUTH_STORE__
? window.__TEST_AUTH_STORE__
: null;
// Priority: explicit prop > E2E test store > production singleton
const authStore = store ?? testStore ?? useAuthStoreImpl;
return <AuthContext.Provider value={authStore}>{children}</AuthContext.Provider>;
}
/**
* Hook to access authentication state and actions
*
* Supports both full state access and selector patterns for performance optimization.
* Must be used within AuthProvider.
*
* @throws {Error} If used outside of AuthProvider
*
* @example
* ```tsx
* // Full state access (simpler, re-renders on any state change)
* function MyComponent() {
* const { user, isAuthenticated } = useAuth();
* return <div>{user?.first_name}</div>;
* }
*
* // Selector pattern (optimized, re-renders only when selected value changes)
* function UserName() {
* const user = useAuth(state => state.user);
* return <span>{user?.first_name}</span>;
* }
*
* // In mutation callbacks (outside React render)
* const handleLogin = async (data) => {
* const response = await loginAPI(data);
* // Use getState() directly for mutations (see useAuth.ts hooks)
* const setAuth = useAuthStore.getState().setAuth;
* await setAuth(response.user, response.token);
* };
* ```
*/
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
}
// Call the Zustand hook internally (follows React Rules of Hooks)
// This is the key difference from returning the hook function itself
return selector ? storeHook(selector) : storeHook();
}

View File

@@ -2,3 +2,6 @@
// Examples: authStore, uiStore, etc.
export { useAuthStore, initializeAuth, type User } from './authStore';
// Authentication Context (DI wrapper for auth store)
export { useAuth, AuthProvider } from '../auth/AuthContext';

View File

@@ -6,12 +6,40 @@
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ProfileSettingsPage from '@/app/(authenticated)/settings/profile/page';
import { AuthProvider } from '@/lib/auth/AuthContext';
import { useAuthStore } from '@/lib/stores/authStore';
// Mock authStore
jest.mock('@/lib/stores/authStore');
const mockUseAuthStore = useAuthStore as jest.MockedFunction<typeof useAuthStore>;
// Mock store hook for AuthProvider
const mockStoreHook = ((selector?: (state: any) => any) => {
const state = {
isAuthenticated: true,
user: {
id: '1',
email: 'test@example.com',
first_name: 'Test',
last_name: 'User',
is_active: true,
is_superuser: false,
created_at: '2024-01-01T00:00:00Z',
},
accessToken: 'token',
refreshToken: 'refresh',
isLoading: false,
tokenExpiresAt: null,
setAuth: jest.fn(),
setTokens: jest.fn(),
setUser: jest.fn(),
clearAuth: jest.fn(),
loadAuthFromStorage: jest.fn(),
isTokenExpired: jest.fn(() => false),
};
return selector ? selector(state) : state;
}) as any;
describe('ProfileSettingsPage', () => {
const queryClient = new QueryClient({
defaultOptions: {
@@ -44,7 +72,9 @@ describe('ProfileSettingsPage', () => {
const renderWithProvider = (component: React.ReactElement) => {
return render(
<QueryClientProvider client={queryClient}>
<AuthProvider store={mockStoreHook}>
{component}
</AuthProvider>
</QueryClientProvider>
);
};

View File

@@ -18,7 +18,7 @@ jest.mock('next/navigation', () => ({
usePathname: () => mockPathname,
}));
// Mock auth store
// Mock auth state via Context
let mockAuthState: {
isAuthenticated: boolean;
isLoading: boolean;
@@ -29,8 +29,9 @@ let mockAuthState: {
user: null,
};
jest.mock('@/lib/stores/authStore', () => ({
useAuthStore: () => mockAuthState,
jest.mock('@/lib/auth/AuthContext', () => ({
useAuth: () => mockAuthState,
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
// Mock useMe hook

View File

@@ -6,14 +6,15 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Header } from '@/components/layout/Header';
import { useAuthStore } from '@/lib/stores/authStore';
import { useAuth } from '@/lib/auth/AuthContext';
import { useLogout } from '@/lib/api/hooks/useAuth';
import { usePathname } from 'next/navigation';
import type { User } from '@/lib/stores/authStore';
// Mock dependencies
jest.mock('@/lib/stores/authStore', () => ({
useAuthStore: jest.fn(),
jest.mock('@/lib/auth/AuthContext', () => ({
useAuth: jest.fn(),
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
jest.mock('@/lib/api/hooks/useAuth', () => ({
@@ -60,7 +61,7 @@ describe('Header', () => {
describe('Rendering', () => {
it('renders header with logo', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -70,7 +71,7 @@ describe('Header', () => {
});
it('renders theme toggle', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -80,7 +81,7 @@ describe('Header', () => {
});
it('renders user avatar with initials', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: 'Doe',
@@ -93,7 +94,7 @@ describe('Header', () => {
});
it('renders user avatar with single initial when no last name', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: null,
@@ -106,7 +107,7 @@ describe('Header', () => {
});
it('renders default initial when no first name', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: '',
}),
@@ -120,7 +121,7 @@ describe('Header', () => {
describe('Navigation Links', () => {
it('renders home link', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -131,7 +132,7 @@ describe('Header', () => {
});
it('renders admin link for superusers', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: true }),
});
@@ -142,7 +143,7 @@ describe('Header', () => {
});
it('does not render admin link for regular users', () => {
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: false }),
});
@@ -158,7 +159,7 @@ describe('Header', () => {
it('highlights active navigation link', () => {
(usePathname as jest.Mock).mockReturnValue('/admin');
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: true }),
});
@@ -173,7 +174,7 @@ describe('Header', () => {
it('opens dropdown when avatar is clicked', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: 'Doe',
@@ -195,7 +196,7 @@ describe('Header', () => {
it('displays user info in dropdown', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: 'Doe',
@@ -217,7 +218,7 @@ describe('Header', () => {
it('includes profile link in dropdown', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -233,7 +234,7 @@ describe('Header', () => {
it('includes settings link in dropdown', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -249,7 +250,7 @@ describe('Header', () => {
it('includes admin panel link for superusers', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: true }),
});
@@ -265,7 +266,7 @@ describe('Header', () => {
it('does not include admin panel link for regular users', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: false }),
});
@@ -284,7 +285,7 @@ describe('Header', () => {
it('calls logout when logout button is clicked', async () => {
const user = userEvent.setup();
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -307,7 +308,7 @@ describe('Header', () => {
isPending: true,
});
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
@@ -329,7 +330,7 @@ describe('Header', () => {
isPending: true,
});
(useAuthStore as unknown as jest.Mock).mockReturnValue({
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});

View File

@@ -11,28 +11,29 @@ import {
useCurrentUser,
useIsAdmin,
} from '@/lib/api/hooks/useAuth';
import { AuthProvider } from '@/lib/auth/AuthContext';
// Mock auth store
let mockAuthState: {
isAuthenticated: boolean;
user: any;
accessToken: string | null;
refreshToken: string | null;
} = {
// Mock auth state (Context-injected)
let mockAuthState: any = {
isAuthenticated: false,
user: null,
accessToken: null,
refreshToken: null,
isLoading: false,
tokenExpiresAt: null,
// Action stubs (unused in these tests)
setAuth: jest.fn(),
setTokens: jest.fn(),
setUser: jest.fn(),
clearAuth: jest.fn(),
loadAuthFromStorage: jest.fn(),
isTokenExpired: jest.fn(() => false),
};
jest.mock('@/lib/stores/authStore', () => ({
useAuthStore: (selector?: (state: any) => any) => {
if (selector) {
return selector(mockAuthState);
}
return mockAuthState;
},
}));
// Mock store hook compatible with AuthContext (Zustand-like hook)
const mockStoreHook = ((selector?: (state: any) => any) => {
return selector ? selector(mockAuthState) : mockAuthState;
}) as any;
// Mock router
jest.mock('next/navigation', () => ({
@@ -51,7 +52,9 @@ const createWrapper = () => {
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<AuthProvider store={mockStoreHook}>
{children}
</AuthProvider>
</QueryClientProvider>
);
};