From eaa37a262e57116d3b016680453fbf0f6da2c3b7 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Fri, 4 Jul 2025 20:24:56 +0200 Subject: [PATCH] Add tests for AdminDashboard, AdminStatsTab, AdminUsersTab, and AdminWorkspacesTab components --- .../settings/admin/AdminDashboard.test.tsx | 110 +++++++ .../settings/admin/AdminStatsTab.test.tsx | 126 ++++++++ .../settings/admin/AdminUsersTab.test.tsx | 288 ++++++++++++++++++ .../settings/admin/AdminUsersTab.tsx | 3 + .../admin/AdminWorkspacesTab.test.tsx | 140 +++++++++ 5 files changed, 667 insertions(+) create mode 100644 app/src/components/settings/admin/AdminDashboard.test.tsx create mode 100644 app/src/components/settings/admin/AdminStatsTab.test.tsx create mode 100644 app/src/components/settings/admin/AdminUsersTab.test.tsx create mode 100644 app/src/components/settings/admin/AdminWorkspacesTab.test.tsx diff --git a/app/src/components/settings/admin/AdminDashboard.test.tsx b/app/src/components/settings/admin/AdminDashboard.test.tsx new file mode 100644 index 0000000..fd439a5 --- /dev/null +++ b/app/src/components/settings/admin/AdminDashboard.test.tsx @@ -0,0 +1,110 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render as rtlRender, screen, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { MantineProvider } from '@mantine/core'; +import AdminDashboard from './AdminDashboard'; +import { UserRole, type User } from '@/types/models'; + +// Mock the auth context +const mockCurrentUser: User = { + id: 1, + email: 'admin@example.com', + displayName: 'Admin User', + role: UserRole.Admin, + createdAt: '2024-01-01T00:00:00Z', + lastWorkspaceId: 1, +}; + +vi.mock('../../../contexts/AuthContext', () => ({ + useAuth: () => ({ + user: mockCurrentUser, + }), +})); + +// Mock the sub-components +vi.mock('./AdminUsersTab', () => ({ + default: ({ currentUser }: { currentUser: User }) => ( +
Users Tab - {currentUser.email}
+ ), +})); + +vi.mock('./AdminWorkspacesTab', () => ({ + default: () =>
Workspaces Tab
, +})); + +vi.mock('./AdminStatsTab', () => ({ + default: () =>
Stats Tab
, +})); + +// Helper wrapper component for testing +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +// Custom render function +const render = (ui: React.ReactElement) => { + return rtlRender(ui, { wrapper: TestWrapper }); +}; + +describe('AdminDashboard', () => { + const mockOnClose = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders modal with all tabs', () => { + render(); + + expect(screen.getByText('Admin Dashboard')).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /users/i })).toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: /workspaces/i }) + ).toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: /statistics/i }) + ).toBeInTheDocument(); + }); + + it('shows users tab by default', () => { + render(); + + expect(screen.getByTestId('admin-users-tab')).toBeInTheDocument(); + expect( + screen.getByText('Users Tab - admin@example.com') + ).toBeInTheDocument(); + }); + + it('switches to workspaces tab when clicked', () => { + render(); + + fireEvent.click(screen.getByRole('tab', { name: /workspaces/i })); + + expect(screen.getByTestId('admin-workspaces-tab')).toBeInTheDocument(); + expect(screen.getByText('Workspaces Tab')).toBeInTheDocument(); + }); + + it('switches to statistics tab when clicked', () => { + render(); + + fireEvent.click(screen.getByRole('tab', { name: /statistics/i })); + + expect(screen.getByTestId('admin-stats-tab')).toBeInTheDocument(); + expect(screen.getByText('Stats Tab')).toBeInTheDocument(); + }); + + it('passes current user to users tab', () => { + render(); + + // Should pass current user to AdminUsersTab + expect( + screen.getByText('Users Tab - admin@example.com') + ).toBeInTheDocument(); + }); + + it('does not render when closed', () => { + render(); + + expect(screen.queryByText('Admin Dashboard')).not.toBeInTheDocument(); + }); +}); diff --git a/app/src/components/settings/admin/AdminStatsTab.test.tsx b/app/src/components/settings/admin/AdminStatsTab.test.tsx new file mode 100644 index 0000000..04367b3 --- /dev/null +++ b/app/src/components/settings/admin/AdminStatsTab.test.tsx @@ -0,0 +1,126 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render as rtlRender, screen } from '@testing-library/react'; +import React from 'react'; +import { MantineProvider } from '@mantine/core'; +import AdminStatsTab from './AdminStatsTab'; +import type { SystemStats } from '@/types/models'; + +// Mock the admin data hook +vi.mock('../../../hooks/useAdminData', () => ({ + useAdminData: vi.fn(), +})); + +// Mock the formatBytes utility +vi.mock('../../../utils/formatBytes', () => ({ + formatBytes: (bytes: number) => `${bytes} bytes`, +})); + +// Helper wrapper component for testing +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +// Custom render function +const render = (ui: React.ReactElement) => { + return rtlRender(ui, { wrapper: TestWrapper }); +}; + +describe('AdminStatsTab', () => { + const mockStats: SystemStats = { + totalUsers: 150, + activeUsers: 120, + totalWorkspaces: 85, + totalFiles: 2500, + totalSize: 1073741824, // 1GB in bytes + }; + + beforeEach(async () => { + vi.clearAllMocks(); + + const { useAdminData } = await import('../../../hooks/useAdminData'); + vi.mocked(useAdminData).mockReturnValue({ + data: mockStats, + loading: false, + error: null, + reload: vi.fn(), + }); + }); + + it('renders statistics table with all metrics', () => { + render(); + + expect(screen.getByText('System Statistics')).toBeInTheDocument(); + expect(screen.getByText('Total Users')).toBeInTheDocument(); + expect(screen.getByText('Active Users')).toBeInTheDocument(); + expect(screen.getByText('Total Workspaces')).toBeInTheDocument(); + expect(screen.getByText('Total Files')).toBeInTheDocument(); + expect(screen.getByText('Total Storage Size')).toBeInTheDocument(); + }); + + it('displays correct statistics values', () => { + render(); + + expect(screen.getByText('150')).toBeInTheDocument(); + expect(screen.getByText('120')).toBeInTheDocument(); + expect(screen.getByText('85')).toBeInTheDocument(); + expect(screen.getByText('2500')).toBeInTheDocument(); + expect(screen.getByText('1073741824 bytes')).toBeInTheDocument(); + }); + + it('shows loading state', async () => { + const { useAdminData } = await import('../../../hooks/useAdminData'); + vi.mocked(useAdminData).mockReturnValue({ + data: {} as SystemStats, + loading: true, + error: null, + reload: vi.fn(), + }); + + render(); + + // Mantine LoadingOverlay should be visible + expect( + document.querySelector('.mantine-LoadingOverlay-root') + ).toBeInTheDocument(); + }); + + it('shows error state', async () => { + const { useAdminData } = await import('../../../hooks/useAdminData'); + vi.mocked(useAdminData).mockReturnValue({ + data: {} as SystemStats, + loading: false, + error: 'Failed to load statistics', + reload: vi.fn(), + }); + + render(); + + expect(screen.getByText('Error')).toBeInTheDocument(); + expect(screen.getByText('Failed to load statistics')).toBeInTheDocument(); + }); + + it('handles zero values correctly', async () => { + const zeroStats: SystemStats = { + totalUsers: 0, + activeUsers: 0, + totalWorkspaces: 0, + totalFiles: 0, + totalSize: 0, + }; + + const { useAdminData } = await import('../../../hooks/useAdminData'); + vi.mocked(useAdminData).mockReturnValue({ + data: zeroStats, + loading: false, + error: null, + reload: vi.fn(), + }); + + render(); + + // Should display zeros without issues + const zeros = screen.getAllByText('0'); + expect(zeros.length).toBeGreaterThan(0); + expect(screen.getByText('0 bytes')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/settings/admin/AdminUsersTab.test.tsx b/app/src/components/settings/admin/AdminUsersTab.test.tsx new file mode 100644 index 0000000..79c6ce5 --- /dev/null +++ b/app/src/components/settings/admin/AdminUsersTab.test.tsx @@ -0,0 +1,288 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render as rtlRender, + screen, + fireEvent, + waitFor, +} from '@testing-library/react'; +import React from 'react'; +import { MantineProvider } from '@mantine/core'; +import AdminUsersTab from './AdminUsersTab'; +import { UserRole, type User } from '@/types/models'; + +// Mock the user admin hook +const mockCreate = vi.fn(); +const mockUpdate = vi.fn(); +const mockDelete = vi.fn(); + +vi.mock('../../../hooks/useUserAdmin', () => ({ + useUserAdmin: vi.fn(), +})); + +// Mock notifications +vi.mock('@mantine/notifications', () => ({ + notifications: { + show: vi.fn(), + }, +})); + +// Mock the user modals +vi.mock('../../modals/user/CreateUserModal', () => ({ + default: ({ + opened, + onCreateUser, + }: { + opened: boolean; + onCreateUser: (userData: { + email: string; + password: string; + displayName: string; + role: UserRole; + }) => Promise; + }) => + opened ? ( +
+ +
+ ) : null, +})); + +vi.mock('../../modals/user/EditUserModal', () => ({ + default: ({ + opened, + onEditUser, + user, + }: { + opened: boolean; + onEditUser: ( + userId: number, + userData: { email: string } + ) => Promise; + user: User | null; + }) => + opened ? ( +
+ {user?.email} + +
+ ) : null, +})); + +vi.mock('../../modals/user/DeleteUserModal', () => ({ + default: ({ + opened, + onConfirm, + }: { + opened: boolean; + onConfirm: () => Promise; + }) => + opened ? ( +
+ +
+ ) : null, +})); + +// Helper wrapper component for testing +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +// Custom render function +const render = (ui: React.ReactElement) => { + return rtlRender(ui, { wrapper: TestWrapper }); +}; + +describe('AdminUsersTab', () => { + const mockCurrentUser: User = { + id: 1, + email: 'admin@example.com', + displayName: 'Admin User', + role: UserRole.Admin, + createdAt: '2024-01-01T00:00:00Z', + lastWorkspaceId: 1, + }; + + const mockUsers: User[] = [ + mockCurrentUser, + { + id: 2, + email: 'editor@example.com', + displayName: 'Editor User', + role: UserRole.Editor, + createdAt: '2024-01-15T00:00:00Z', + lastWorkspaceId: 2, + }, + { + id: 3, + email: 'viewer@example.com', + displayName: 'Viewer User', + role: UserRole.Viewer, + createdAt: '2024-02-01T00:00:00Z', + lastWorkspaceId: 3, + }, + ]; + + beforeEach(async () => { + vi.clearAllMocks(); + mockCreate.mockResolvedValue(true); + mockUpdate.mockResolvedValue(true); + mockDelete.mockResolvedValue(true); + + const { useUserAdmin } = await import('../../../hooks/useUserAdmin'); + vi.mocked(useUserAdmin).mockReturnValue({ + users: mockUsers, + loading: false, + error: null, + create: mockCreate, + update: mockUpdate, + delete: mockDelete, + }); + }); + + it('renders users table with all users', () => { + render(); + + expect(screen.getByText('User Management')).toBeInTheDocument(); + expect(screen.getByText('admin@example.com')).toBeInTheDocument(); + expect(screen.getByText('editor@example.com')).toBeInTheDocument(); + expect(screen.getByText('viewer@example.com')).toBeInTheDocument(); + expect(screen.getByText('Admin User')).toBeInTheDocument(); + expect(screen.getByText('Editor User')).toBeInTheDocument(); + expect(screen.getByText('Viewer User')).toBeInTheDocument(); + }); + + it('shows create user button', () => { + render(); + + expect( + screen.getByRole('button', { name: /create user/i }) + ).toBeInTheDocument(); + }); + + it('opens create user modal when create button is clicked', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /create user/i })); + + expect(screen.getByTestId('create-user-modal')).toBeInTheDocument(); + }); + + it('creates new user successfully', async () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: /create user/i })); + fireEvent.click(screen.getByTestId('create-user-button')); + + await waitFor(() => { + expect(mockCreate).toHaveBeenCalledWith({ + email: 'new@example.com', + password: 'pass', + displayName: 'New User', + role: UserRole.Editor, + }); + }); + }); + + it('opens edit modal when edit button is clicked', () => { + render(); + + const editButtons = screen.getAllByLabelText(/edit/i); + expect(editButtons[0]).toBeDefined(); + fireEvent.click(editButtons[0]!); // Click first edit button + + expect(screen.getByTestId('edit-user-modal')).toBeInTheDocument(); + expect(screen.getByTestId('edit-user-email')).toHaveTextContent( + 'admin@example.com' + ); + }); + + it('updates user successfully', async () => { + render(); + + const editButtons = screen.getAllByLabelText(/edit/i); + expect(editButtons[0]).toBeDefined(); + fireEvent.click(editButtons[0]!); + fireEvent.click(screen.getByTestId('edit-user-button')); + + await waitFor(() => { + expect(mockUpdate).toHaveBeenCalledWith(1, { + email: 'updated@example.com', + }); + }); + }); + + it('prevents deleting current user', () => { + render(); + + const deleteButtons = screen.getAllByLabelText(/delete/i); + const currentUserDeleteButton = deleteButtons[0]; // First user is current user + + expect(currentUserDeleteButton).toBeDefined(); + expect(currentUserDeleteButton).toBeDisabled(); + }); + + it('allows deleting other users', () => { + render(); + + const deleteButtons = screen.getAllByLabelText(/delete/i); + expect(deleteButtons[1]).toBeDefined(); + fireEvent.click(deleteButtons[1]!); // Click delete for second user + + expect(screen.getByTestId('delete-user-modal')).toBeInTheDocument(); + }); + + it('deletes user successfully', async () => { + render(); + + const deleteButtons = screen.getAllByLabelText(/delete/i); + expect(deleteButtons[1]).toBeDefined(); + fireEvent.click(deleteButtons[1]!); + fireEvent.click(screen.getByTestId('delete-user-button')); + + await waitFor(() => { + expect(mockDelete).toHaveBeenCalledWith(2); // Second user's ID + }); + }); + + it('shows error state when loading fails', async () => { + const { useUserAdmin } = await import('../../../hooks/useUserAdmin'); + vi.mocked(useUserAdmin).mockReturnValue({ + users: [], + loading: false, + error: 'Failed to load users', + create: mockCreate, + update: mockUpdate, + delete: mockDelete, + }); + + render(); + + expect(screen.getByText('Error')).toBeInTheDocument(); + expect(screen.getByText('Failed to load users')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/settings/admin/AdminUsersTab.tsx b/app/src/components/settings/admin/AdminUsersTab.tsx index 82f1ed3..edd1b29 100644 --- a/app/src/components/settings/admin/AdminUsersTab.tsx +++ b/app/src/components/settings/admin/AdminUsersTab.tsx @@ -86,6 +86,7 @@ const AdminUsersTab: React.FC = ({ currentUser }) => { setEditModalData(user)} > @@ -93,6 +94,7 @@ const AdminUsersTab: React.FC = ({ currentUser }) => { handleDeleteClick(user)} disabled={user.id === currentUser.id} @@ -125,6 +127,7 @@ const AdminUsersTab: React.FC = ({ currentUser }) => {