Add unit tests for settings components and session hooks
- Implement comprehensive tests for `ProfileSettingsForm`, `PasswordChangeForm`, and `SessionCard` components to validate rendering, interactions, and state handling. - Add tests for session management hooks (`useSession`, `useRevokeSession`, and `useRevokeAllOtherSessions`) to verify logic and API integration. - Ensure coverage of edge cases, error handling, and success callbacks across all new tests.
This commit is contained in:
338
frontend/tests/lib/api/hooks/useSession.test.tsx
Normal file
338
frontend/tests/lib/api/hooks/useSession.test.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* Tests for useSession hooks
|
||||
* Tests session management hooks
|
||||
*/
|
||||
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
useListSessions,
|
||||
useRevokeSession,
|
||||
useRevokeAllOtherSessions,
|
||||
type Session,
|
||||
} from '@/lib/api/hooks/useSession';
|
||||
import * as apiClient from '@/lib/api/client';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/api/client');
|
||||
|
||||
const mockListMySessions = apiClient.listMySessions as jest.Mock;
|
||||
const mockRevokeSession = apiClient.revokeSession as jest.Mock;
|
||||
|
||||
describe('useSession hooks', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const mockSessions: Session[] = [
|
||||
{
|
||||
id: '1',
|
||||
device_name: 'Chrome on Mac',
|
||||
ip_address: '192.168.1.1',
|
||||
location_city: 'San Francisco',
|
||||
location_country: 'USA',
|
||||
last_used_at: '2024-01-01T12:00:00Z',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
expires_at: '2024-01-08T00:00:00Z',
|
||||
is_current: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
device_name: 'Firefox on Windows',
|
||||
ip_address: '192.168.1.2',
|
||||
location_city: 'New York',
|
||||
location_country: 'USA',
|
||||
last_used_at: '2024-01-01T11:00:00Z',
|
||||
created_at: '2023-12-31T00:00:00Z',
|
||||
expires_at: '2024-01-07T00:00:00Z',
|
||||
is_current: false,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('useListSessions', () => {
|
||||
it('successfully fetches sessions list', async () => {
|
||||
mockListMySessions.mockResolvedValueOnce({
|
||||
data: {
|
||||
sessions: mockSessions,
|
||||
total: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useListSessions(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockListMySessions).toHaveBeenCalledWith({
|
||||
throwOnError: true,
|
||||
});
|
||||
expect(result.current.data).toEqual(mockSessions);
|
||||
});
|
||||
|
||||
it('returns empty array when no sessions', async () => {
|
||||
mockListMySessions.mockResolvedValueOnce({
|
||||
data: {
|
||||
sessions: [],
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useListSessions(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles undefined sessions data', async () => {
|
||||
mockListMySessions.mockResolvedValueOnce({
|
||||
data: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useListSessions(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses correct cache key', async () => {
|
||||
mockListMySessions.mockResolvedValueOnce({
|
||||
data: {
|
||||
sessions: mockSessions,
|
||||
total: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useListSessions(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
const cachedData = queryClient.getQueryData(['sessions', 'list']);
|
||||
expect(cachedData).toEqual(mockSessions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRevokeSession', () => {
|
||||
it('successfully revokes a session', async () => {
|
||||
mockRevokeSession.mockResolvedValueOnce({
|
||||
data: { message: 'Session revoked successfully' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeSession(), { wrapper });
|
||||
|
||||
result.current.mutate('session-id-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockRevokeSession).toHaveBeenCalledWith({
|
||||
path: { session_id: 'session-id-123' },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onSuccess callback when provided', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
mockRevokeSession.mockResolvedValueOnce({
|
||||
data: { message: 'Session revoked successfully' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeSession(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate('session-id-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledWith('Session revoked successfully');
|
||||
});
|
||||
|
||||
it('uses default message when no message in response', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
mockRevokeSession.mockResolvedValueOnce({
|
||||
data: {},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeSession(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate('session-id-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledWith('Session revoked successfully');
|
||||
});
|
||||
|
||||
it('handles revocation errors', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
mockRevokeSession.mockResolvedValueOnce({
|
||||
error: {
|
||||
message: 'Revocation failed',
|
||||
errors: [{ field: 'general', message: 'Revocation failed' }],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeSession(), { wrapper });
|
||||
|
||||
result.current.mutate('session-id-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
'Session revocation failed:',
|
||||
'An unexpected error occurred'
|
||||
);
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
it('invalidates sessions query on success', async () => {
|
||||
mockRevokeSession.mockResolvedValueOnce({
|
||||
data: { message: 'Session revoked' },
|
||||
});
|
||||
|
||||
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useRevokeSession(), { wrapper });
|
||||
|
||||
result.current.mutate('session-id-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({
|
||||
queryKey: ['sessions', 'list'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRevokeAllOtherSessions', () => {
|
||||
beforeEach(() => {
|
||||
// Mock useListSessions data
|
||||
queryClient.setQueryData(['sessions', 'list'], mockSessions);
|
||||
});
|
||||
|
||||
it('successfully revokes all other sessions', async () => {
|
||||
mockRevokeSession.mockResolvedValue({
|
||||
data: { message: 'Session revoked' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeAllOtherSessions(), { wrapper });
|
||||
|
||||
result.current.mutate();
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
// Should only revoke non-current sessions
|
||||
expect(mockRevokeSession).toHaveBeenCalledTimes(1);
|
||||
expect(mockRevokeSession).toHaveBeenCalledWith({
|
||||
path: { session_id: '2' },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onSuccess with count message', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
mockRevokeSession.mockResolvedValue({
|
||||
data: { message: 'Session revoked' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeAllOtherSessions(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate();
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledWith('Successfully revoked 1 session');
|
||||
});
|
||||
|
||||
it('handles plural session count in message', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
// Set multiple non-current sessions
|
||||
const multipleSessions: Session[] = [
|
||||
{ ...mockSessions[0], is_current: true },
|
||||
{ ...mockSessions[1], id: '2', is_current: false },
|
||||
{ ...mockSessions[1], id: '3', is_current: false },
|
||||
];
|
||||
|
||||
queryClient.setQueryData(['sessions', 'list'], multipleSessions);
|
||||
|
||||
mockRevokeSession.mockResolvedValue({
|
||||
data: { message: 'Session revoked' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRevokeAllOtherSessions(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate();
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockRevokeSession).toHaveBeenCalledTimes(2);
|
||||
expect(onSuccess).toHaveBeenCalledWith('Successfully revoked 2 sessions');
|
||||
});
|
||||
|
||||
it('handles no other sessions gracefully', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
// Only current session
|
||||
queryClient.setQueryData(['sessions', 'list'], [mockSessions[0]]);
|
||||
|
||||
const { result } = renderHook(() => useRevokeAllOtherSessions(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate();
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockRevokeSession).not.toHaveBeenCalled();
|
||||
expect(onSuccess).toHaveBeenCalledWith('No other sessions to revoke');
|
||||
});
|
||||
|
||||
it('handles when no sessions are available', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
|
||||
// Set empty sessions array
|
||||
queryClient.setQueryData(['sessions', 'list'], []);
|
||||
|
||||
const { result } = renderHook(() => useRevokeAllOtherSessions(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate();
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
// Should succeed with message about no sessions
|
||||
expect(onSuccess).toHaveBeenCalledWith('No other sessions to revoke');
|
||||
});
|
||||
|
||||
it('handles bulk revocation errors', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
mockRevokeSession.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useRevokeAllOtherSessions(), { wrapper });
|
||||
|
||||
result.current.mutate();
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
195
frontend/tests/lib/api/hooks/useUser.test.tsx
Normal file
195
frontend/tests/lib/api/hooks/useUser.test.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Tests for useUser hooks
|
||||
* Tests user profile management hooks
|
||||
*/
|
||||
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useUpdateProfile } from '@/lib/api/hooks/useUser';
|
||||
import { useAuthStore } from '@/lib/stores/authStore';
|
||||
import * as apiClient from '@/lib/api/client';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/stores/authStore');
|
||||
jest.mock('@/lib/api/client');
|
||||
|
||||
const mockUseAuthStore = useAuthStore as jest.MockedFunction<typeof useAuthStore>;
|
||||
const mockUpdateCurrentUser = apiClient.updateCurrentUser as jest.Mock;
|
||||
|
||||
describe('useUser hooks', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('useUpdateProfile', () => {
|
||||
const mockSetUser = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthStore.mockImplementation((selector: unknown) => {
|
||||
if (typeof selector === 'function') {
|
||||
const mockState = { setUser: mockSetUser };
|
||||
return selector(mockState);
|
||||
}
|
||||
return mockSetUser;
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully updates profile', async () => {
|
||||
const updatedUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
first_name: 'Updated',
|
||||
last_name: 'Name',
|
||||
is_active: true,
|
||||
is_superuser: false,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockUpdateCurrentUser.mockResolvedValueOnce({
|
||||
data: updatedUser,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdateProfile(), { wrapper });
|
||||
|
||||
result.current.mutate({
|
||||
first_name: 'Updated',
|
||||
last_name: 'Name',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockUpdateCurrentUser).toHaveBeenCalledWith({
|
||||
body: { first_name: 'Updated', last_name: 'Name' },
|
||||
throwOnError: false,
|
||||
});
|
||||
expect(mockSetUser).toHaveBeenCalledWith(updatedUser);
|
||||
});
|
||||
|
||||
it('calls onSuccess callback when provided', async () => {
|
||||
const onSuccess = jest.fn();
|
||||
const updatedUser = {
|
||||
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',
|
||||
};
|
||||
|
||||
mockUpdateCurrentUser.mockResolvedValueOnce({
|
||||
data: updatedUser,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdateProfile(onSuccess), { wrapper });
|
||||
|
||||
result.current.mutate({
|
||||
first_name: 'Test',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(onSuccess).toHaveBeenCalledWith('Profile updated successfully');
|
||||
});
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
mockUpdateCurrentUser.mockResolvedValueOnce({
|
||||
error: {
|
||||
message: 'Update failed',
|
||||
errors: [{ field: 'general', message: 'Update failed' }],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdateProfile(), { wrapper });
|
||||
|
||||
result.current.mutate({
|
||||
first_name: 'Test',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
'Profile update failed:',
|
||||
'An unexpected error occurred'
|
||||
);
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
it('invalidates auth queries on success', async () => {
|
||||
const updatedUser = {
|
||||
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',
|
||||
};
|
||||
|
||||
mockUpdateCurrentUser.mockResolvedValueOnce({
|
||||
data: updatedUser,
|
||||
});
|
||||
|
||||
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useUpdateProfile(), { wrapper });
|
||||
|
||||
result.current.mutate({
|
||||
first_name: 'Test',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({
|
||||
queryKey: ['auth', 'me'],
|
||||
});
|
||||
});
|
||||
|
||||
it('updates only first_name when last_name is not provided', async () => {
|
||||
const updatedUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: '',
|
||||
is_active: true,
|
||||
is_superuser: false,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockUpdateCurrentUser.mockResolvedValueOnce({
|
||||
data: updatedUser,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUpdateProfile(), { wrapper });
|
||||
|
||||
result.current.mutate({
|
||||
first_name: 'Test',
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockUpdateCurrentUser).toHaveBeenCalledWith({
|
||||
body: { first_name: 'Test' },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user