Add comprehensive .gitignore
This commit is contained in:
136
maternal-web/__tests__/accessibility/wcag.test.tsx
Normal file
136
maternal-web/__tests__/accessibility/wcag.test.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { axe, toHaveNoViolations } from 'jest-axe';
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
// Mock components for testing
|
||||
const AccessibleButton = () => (
|
||||
<button aria-label="Submit form">Submit</button>
|
||||
);
|
||||
|
||||
const InaccessibleButton = () => (
|
||||
<button>Submit</button>
|
||||
);
|
||||
|
||||
const AccessibleForm = () => (
|
||||
<form>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" type="email" />
|
||||
<button type="submit" aria-label="Submit form">Submit</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
describe('WCAG Accessibility Compliance', () => {
|
||||
it('should not have accessibility violations for buttons with aria-label', async () => {
|
||||
const { container } = render(<AccessibleButton />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have proper form labels', async () => {
|
||||
const { container } = render(<AccessibleForm />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have proper color contrast', async () => {
|
||||
const { container } = render(
|
||||
<div style={{ backgroundColor: '#000', color: '#fff' }}>
|
||||
High contrast text
|
||||
</div>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have accessible heading hierarchy', async () => {
|
||||
const { container } = render(
|
||||
<div>
|
||||
<h1>Main Title</h1>
|
||||
<h2>Subtitle</h2>
|
||||
<h3>Section Title</h3>
|
||||
</div>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have accessible links', async () => {
|
||||
const { container } = render(
|
||||
<a href="/about" aria-label="Learn more about us">
|
||||
Learn more
|
||||
</a>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have accessible images', async () => {
|
||||
const { container } = render(
|
||||
<img src="/test.jpg" alt="Descriptive text for image" />
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should support keyboard navigation', async () => {
|
||||
const { container } = render(
|
||||
<div>
|
||||
<button tabIndex={0}>First</button>
|
||||
<button tabIndex={0}>Second</button>
|
||||
<button tabIndex={0}>Third</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have proper ARIA roles', async () => {
|
||||
const { container } = render(
|
||||
<nav role="navigation" aria-label="Main navigation">
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have accessible modal dialogs', async () => {
|
||||
const { container } = render(
|
||||
<div
|
||||
role="dialog"
|
||||
aria-labelledby="dialog-title"
|
||||
aria-modal="true"
|
||||
>
|
||||
<h2 id="dialog-title">Modal Title</h2>
|
||||
<p>Modal content</p>
|
||||
<button aria-label="Close modal">Close</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have accessible loading states', async () => {
|
||||
const { container } = render(
|
||||
<div role="status" aria-live="polite" aria-busy="true">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import WeeklySleepChart from '@/components/analytics/WeeklySleepChart';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
jest.mock('@/lib/api/client');
|
||||
const mockApiClient = apiClient as jest.Mocked<typeof apiClient>;
|
||||
|
||||
describe('WeeklySleepChart', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render loading state initially', () => {
|
||||
mockApiClient.get.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
render(<WeeklySleepChart />);
|
||||
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render sleep data successfully', async () => {
|
||||
const mockSleepData = [
|
||||
{
|
||||
id: 'slp_1',
|
||||
startTime: new Date('2024-01-01T20:00:00Z').toISOString(),
|
||||
endTime: new Date('2024-01-02T06:00:00Z').toISOString(),
|
||||
duration: 600, // 10 hours
|
||||
quality: 4,
|
||||
},
|
||||
{
|
||||
id: 'slp_2',
|
||||
startTime: new Date('2024-01-02T20:00:00Z').toISOString(),
|
||||
endTime: new Date('2024-01-03T07:00:00Z').toISOString(),
|
||||
duration: 660, // 11 hours
|
||||
quality: 5,
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: mockSleepData,
|
||||
},
|
||||
});
|
||||
|
||||
render(<WeeklySleepChart />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Weekly Sleep Patterns')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('Total Sleep Hours')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sleep Quality Trend')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle API error gracefully', async () => {
|
||||
mockApiClient.get.mockRejectedValueOnce({
|
||||
response: {
|
||||
data: {
|
||||
message: 'Failed to fetch sleep data',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<WeeklySleepChart />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('Failed to fetch sleep data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate sleep data by day', async () => {
|
||||
const mockSleepData = [
|
||||
{
|
||||
id: 'slp_1',
|
||||
startTime: new Date('2024-01-01T20:00:00Z').toISOString(),
|
||||
duration: 600, // Night sleep
|
||||
quality: 4,
|
||||
},
|
||||
{
|
||||
id: 'slp_2',
|
||||
startTime: new Date('2024-01-01T14:00:00Z').toISOString(),
|
||||
duration: 120, // Nap
|
||||
quality: 3,
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: mockSleepData,
|
||||
},
|
||||
});
|
||||
|
||||
render(<WeeklySleepChart />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiClient.get).toHaveBeenCalledWith(
|
||||
'/api/v1/activities/sleep',
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display 7 days of data', async () => {
|
||||
mockApiClient.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
|
||||
render(<WeeklySleepChart />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Weekly Sleep Patterns')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Chart should request exactly 7 days of data
|
||||
expect(mockApiClient.get).toHaveBeenCalledWith(
|
||||
'/api/v1/activities/sleep',
|
||||
expect.objectContaining({
|
||||
params: expect.any(Object),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should calculate quality average correctly', async () => {
|
||||
const mockSleepData = [
|
||||
{
|
||||
id: 'slp_1',
|
||||
startTime: new Date('2024-01-01T20:00:00Z').toISOString(),
|
||||
duration: 600,
|
||||
quality: 4,
|
||||
},
|
||||
{
|
||||
id: 'slp_2',
|
||||
startTime: new Date('2024-01-01T14:00:00Z').toISOString(),
|
||||
duration: 120,
|
||||
quality: 2,
|
||||
},
|
||||
];
|
||||
|
||||
mockApiClient.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: mockSleepData,
|
||||
},
|
||||
});
|
||||
|
||||
render(<WeeklySleepChart />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Sleep Quality Trend')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Average quality should be (4 + 2) / 2 = 3
|
||||
});
|
||||
});
|
||||
170
maternal-web/__tests__/lib/api/client.test.ts
Normal file
170
maternal-web/__tests__/lib/api/client.test.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import axios from 'axios';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
describe('API Client', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('Request Interceptor', () => {
|
||||
it('should add Authorization header when token exists', async () => {
|
||||
const mockToken = 'test_access_token';
|
||||
localStorage.setItem('accessToken', mockToken);
|
||||
|
||||
// Mock the interceptor behavior
|
||||
const config = {
|
||||
headers: {},
|
||||
};
|
||||
|
||||
// Simulate request interceptor
|
||||
const interceptedConfig = {
|
||||
...config,
|
||||
headers: {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
expect(interceptedConfig.headers.Authorization).toBe(`Bearer ${mockToken}`);
|
||||
});
|
||||
|
||||
it('should not add Authorization header when token does not exist', () => {
|
||||
const config = {
|
||||
headers: {},
|
||||
};
|
||||
|
||||
// No token in localStorage
|
||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
||||
|
||||
// Headers should remain unchanged
|
||||
expect(config.headers).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Response Interceptor - Token Refresh', () => {
|
||||
it('should refresh token on 401 error', async () => {
|
||||
const originalRequest = {
|
||||
headers: {},
|
||||
url: '/api/v1/test',
|
||||
};
|
||||
|
||||
const mockRefreshToken = 'refresh_token_123';
|
||||
const mockNewAccessToken = 'new_access_token_123';
|
||||
|
||||
localStorage.setItem('refreshToken', mockRefreshToken);
|
||||
|
||||
// Mock the refresh token endpoint
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: {
|
||||
accessToken: mockNewAccessToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Simulate the retry logic
|
||||
expect(localStorage.getItem('refreshToken')).toBe(mockRefreshToken);
|
||||
});
|
||||
|
||||
it('should redirect to login on refresh token failure', () => {
|
||||
localStorage.setItem('accessToken', 'expired_token');
|
||||
localStorage.setItem('refreshToken', 'invalid_refresh_token');
|
||||
|
||||
// Mock failed refresh
|
||||
mockedAxios.post.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 401,
|
||||
},
|
||||
});
|
||||
|
||||
// Simulate clearing tokens
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
|
||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
||||
expect(localStorage.getItem('refreshToken')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle network errors gracefully', async () => {
|
||||
const networkError = new Error('Network Error');
|
||||
|
||||
mockedAxios.get.mockRejectedValueOnce(networkError);
|
||||
|
||||
try {
|
||||
await mockedAxios.get('/api/v1/test');
|
||||
fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBe(networkError);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle 500 server errors', async () => {
|
||||
const serverError = {
|
||||
response: {
|
||||
status: 500,
|
||||
data: {
|
||||
message: 'Internal Server Error',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockedAxios.get.mockRejectedValueOnce(serverError);
|
||||
|
||||
try {
|
||||
await mockedAxios.get('/api/v1/test');
|
||||
fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toEqual(serverError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Base URL Configuration', () => {
|
||||
it('should use environment variable for base URL', () => {
|
||||
const expectedBaseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
|
||||
|
||||
// The client should be configured with this base URL
|
||||
expect(expectedBaseURL).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Configuration', () => {
|
||||
it('should set default timeout', () => {
|
||||
// Default timeout should be configured (typically 10000ms)
|
||||
const defaultTimeout = 10000;
|
||||
expect(defaultTimeout).toBe(10000);
|
||||
});
|
||||
|
||||
it('should handle FormData requests', async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', new Blob(['test']), 'test.txt');
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: { success: true },
|
||||
});
|
||||
|
||||
await mockedAxios.post('/api/v1/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/upload',
|
||||
formData,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
317
maternal-web/__tests__/lib/auth/AuthContext.test.tsx
Normal file
317
maternal-web/__tests__/lib/auth/AuthContext.test.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
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<typeof apiClient>;
|
||||
const mockUseRouter = useRouter as jest.MockedFunction<typeof useRouter>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user