unit test auto-provider refactor
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m30s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m30s
This commit is contained in:
72
src/providers/AppProviders.test.tsx
Normal file
72
src/providers/AppProviders.test.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
// src/providers/AppProviders.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { AppProviders } from './AppProviders';
|
||||
|
||||
// Mock all the providers to avoid their side effects and isolate AppProviders logic.
|
||||
// We render a simple div with a data-testid for each to verify nesting.
|
||||
vi.mock('./ModalProvider', () => ({
|
||||
ModalProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="modal-provider">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./AuthProvider', () => ({
|
||||
AuthProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="auth-provider">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./FlyersProvider', () => ({
|
||||
FlyersProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="flyers-provider">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./MasterItemsProvider', () => ({
|
||||
MasterItemsProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="master-items-provider">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('./UserDataProvider', () => ({
|
||||
UserDataProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="user-data-provider">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('AppProviders', () => {
|
||||
it('renders children correctly', () => {
|
||||
render(
|
||||
<AppProviders>
|
||||
<div data-testid="test-child">Test Child</div>
|
||||
</AppProviders>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('test-child')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders providers in the correct nesting order', () => {
|
||||
render(
|
||||
<AppProviders>
|
||||
<div data-testid="test-child">Test Child</div>
|
||||
</AppProviders>,
|
||||
);
|
||||
|
||||
const modalProvider = screen.getByTestId('modal-provider');
|
||||
const authProvider = screen.getByTestId('auth-provider');
|
||||
const flyersProvider = screen.getByTestId('flyers-provider');
|
||||
const masterItemsProvider = screen.getByTestId('master-items-provider');
|
||||
const userDataProvider = screen.getByTestId('user-data-provider');
|
||||
const child = screen.getByTestId('test-child');
|
||||
|
||||
// Verify nesting structure: Modal -> Auth -> Flyers -> MasterItems -> UserData -> Child
|
||||
expect(modalProvider).toContainElement(authProvider);
|
||||
expect(authProvider).toContainElement(flyersProvider);
|
||||
expect(flyersProvider).toContainElement(masterItemsProvider);
|
||||
expect(masterItemsProvider).toContainElement(userDataProvider);
|
||||
expect(userDataProvider).toContainElement(child);
|
||||
});
|
||||
});
|
||||
232
src/providers/AuthProvider.test.tsx
Normal file
232
src/providers/AuthProvider.test.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
// src/providers/AuthProvider.test.tsx
|
||||
import React, { useContext } from 'react';
|
||||
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import { AuthProvider } from './AuthProvider';
|
||||
import { AuthContext } from '../contexts/AuthContext';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import * as tokenStorage from '../services/tokenStorage';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mocks
|
||||
vi.mock('../services/apiClient');
|
||||
vi.mock('../services/tokenStorage');
|
||||
vi.mock('../services/logger.client', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
|
||||
const mockedTokenStorage = tokenStorage as Mocked<typeof tokenStorage>;
|
||||
|
||||
const mockProfile = createMockUserProfile({
|
||||
user: { user_id: 'user-123', email: 'test@example.com' },
|
||||
});
|
||||
|
||||
// A simple consumer component to access and display context values
|
||||
const TestConsumer = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
return <div>No Context</div>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="auth-status">{context.authStatus}</div>
|
||||
<div data-testid="user-email">{context.userProfile?.user.email ?? 'No User'}</div>
|
||||
<div data-testid="is-loading">{context.isLoading.toString()}</div>
|
||||
<button onClick={() => context.login('test-token', mockProfile)}>Login with Profile</button>
|
||||
<button onClick={() => context.login('test-token-no-profile')}>Login without Profile</button>
|
||||
<button onClick={context.logout}>Logout</button>
|
||||
<button onClick={() => context.updateProfile({ full_name: 'Updated Name' })}>
|
||||
Update Profile
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderWithProvider = () => {
|
||||
return render(
|
||||
<AuthProvider>
|
||||
<TestConsumer />
|
||||
</AuthProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('AuthProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should start in "Determining..." state and transition to "SIGNED_OUT" if no token exists', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue(null);
|
||||
renderWithProvider();
|
||||
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('Determining...');
|
||||
expect(screen.getByTestId('is-loading')).toHaveTextContent('true');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
|
||||
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
||||
});
|
||||
|
||||
expect(mockedApiClient.getAuthenticatedUserProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should transition to "AUTHENTICATED" if a valid token exists', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue('valid-token');
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
||||
new Response(JSON.stringify(mockProfile)),
|
||||
);
|
||||
|
||||
renderWithProvider();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
|
||||
expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com');
|
||||
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
||||
});
|
||||
|
||||
expect(mockedApiClient.getAuthenticatedUserProfile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle token validation failure by signing out', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue('invalid-token');
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Invalid Token'));
|
||||
|
||||
renderWithProvider();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
|
||||
});
|
||||
|
||||
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a valid token that returns no profile by signing out', async () => {
|
||||
// This test covers lines 51-55
|
||||
mockedTokenStorage.getToken.mockReturnValue('valid-token-no-profile');
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
||||
new Response(JSON.stringify(null)),
|
||||
);
|
||||
|
||||
renderWithProvider();
|
||||
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('Determining...');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
|
||||
});
|
||||
|
||||
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
|
||||
expect(screen.getByTestId('user-email')).toHaveTextContent('No User');
|
||||
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
|
||||
});
|
||||
|
||||
it('should log in a user with provided profile data', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue(null);
|
||||
renderWithProvider();
|
||||
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT'));
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: 'Login with Profile' });
|
||||
await act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
});
|
||||
|
||||
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token');
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
|
||||
expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com');
|
||||
// API should not be called if profile is provided
|
||||
expect(mockedApiClient.getAuthenticatedUserProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log in a user and fetch profile if not provided', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue(null);
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
||||
new Response(JSON.stringify(mockProfile)),
|
||||
);
|
||||
renderWithProvider();
|
||||
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT'));
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: 'Login without Profile' });
|
||||
await act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
|
||||
expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token-no-profile');
|
||||
expect(mockedApiClient.getAuthenticatedUserProfile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw an error and log out if profile fetch fails after login', async () => {
|
||||
// This test covers lines 109-111
|
||||
mockedTokenStorage.getToken.mockReturnValue(null);
|
||||
const fetchError = new Error('API is down');
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(fetchError);
|
||||
|
||||
renderWithProvider();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
|
||||
});
|
||||
|
||||
const loginButton = screen.getByRole('button', { name: 'Login without Profile' });
|
||||
|
||||
// The login function throws an error, so we wrap it to assert the throw
|
||||
await expect(
|
||||
act(async () => {
|
||||
fireEvent.click(loginButton);
|
||||
}),
|
||||
).rejects.toThrow('Login succeeded, but failed to fetch your data: API is down');
|
||||
|
||||
// After the error is thrown, the state should be rolled back
|
||||
await waitFor(() => {
|
||||
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token-no-profile');
|
||||
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
|
||||
});
|
||||
});
|
||||
|
||||
it('should log out the user', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue('valid-token');
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
||||
new Response(JSON.stringify(mockProfile)),
|
||||
);
|
||||
renderWithProvider();
|
||||
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED'));
|
||||
|
||||
const logoutButton = screen.getByRole('button', { name: 'Logout' });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
|
||||
expect(screen.getByTestId('user-email')).toHaveTextContent('No User');
|
||||
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update the user profile', async () => {
|
||||
mockedTokenStorage.getToken.mockReturnValue('valid-token');
|
||||
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
|
||||
new Response(JSON.stringify(mockProfile)),
|
||||
);
|
||||
renderWithProvider();
|
||||
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED'));
|
||||
|
||||
const updateButton = screen.getByRole('button', { name: 'Update Profile' });
|
||||
fireEvent.click(updateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// The profile object is internal, so we can't directly check it.
|
||||
// A good proxy is to see if a component that uses it would re-render.
|
||||
// Since our consumer doesn't display the name, we just confirm the function was called.
|
||||
// In a real app, we'd check the updated UI element.
|
||||
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user