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 }) => {
}
+ aria-label="Create user"
onClick={() => setCreateModalOpened(true)}
>
Create User
diff --git a/app/src/components/settings/admin/AdminWorkspacesTab.test.tsx b/app/src/components/settings/admin/AdminWorkspacesTab.test.tsx
new file mode 100644
index 0000000..b8776bd
--- /dev/null
+++ b/app/src/components/settings/admin/AdminWorkspacesTab.test.tsx
@@ -0,0 +1,140 @@
+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 AdminWorkspacesTab from './AdminWorkspacesTab';
+import type { WorkspaceStats } 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('AdminWorkspacesTab', () => {
+ const mockWorkspaces: WorkspaceStats[] = [
+ {
+ workspaceID: 1,
+ userID: 1,
+ userEmail: 'user1@example.com',
+ workspaceName: 'Project Alpha',
+ workspaceCreatedAt: '2024-01-15T10:30:00Z',
+ fileCountStats: {
+ totalFiles: 25,
+ totalSize: 1048576, // 1MB
+ },
+ },
+ {
+ workspaceID: 2,
+ userID: 2,
+ userEmail: 'user2@example.com',
+ workspaceName: 'Project Beta',
+ workspaceCreatedAt: '2024-02-20T14:45:00Z',
+ fileCountStats: {
+ totalFiles: 42,
+ totalSize: 2097152, // 2MB
+ },
+ },
+ ];
+
+ beforeEach(async () => {
+ vi.clearAllMocks();
+
+ const { useAdminData } = await import('../../../hooks/useAdminData');
+ vi.mocked(useAdminData).mockReturnValue({
+ data: mockWorkspaces,
+ loading: false,
+ error: null,
+ reload: vi.fn(),
+ });
+ });
+
+ it('renders workspace table with all columns', () => {
+ render();
+
+ expect(screen.getByText('Workspace Management')).toBeInTheDocument();
+ expect(screen.getByText('Owner')).toBeInTheDocument();
+ expect(screen.getByText('Name')).toBeInTheDocument();
+ expect(screen.getByText('Created At')).toBeInTheDocument();
+ expect(screen.getByText('Total Files')).toBeInTheDocument();
+ expect(screen.getByText('Total Size')).toBeInTheDocument();
+ });
+
+ it('displays workspace data correctly', () => {
+ render();
+
+ expect(screen.getByText('user1@example.com')).toBeInTheDocument();
+ expect(screen.getByText('Project Alpha')).toBeInTheDocument();
+ expect(screen.getByText('1/15/2024')).toBeInTheDocument();
+ expect(screen.getByText('25')).toBeInTheDocument();
+ expect(screen.getByText('1048576 bytes')).toBeInTheDocument();
+
+ expect(screen.getByText('user2@example.com')).toBeInTheDocument();
+ expect(screen.getByText('Project Beta')).toBeInTheDocument();
+ expect(screen.getByText('2/20/2024')).toBeInTheDocument();
+ expect(screen.getByText('42')).toBeInTheDocument();
+ expect(screen.getByText('2097152 bytes')).toBeInTheDocument();
+ });
+
+ it('shows loading state', async () => {
+ const { useAdminData } = await import('../../../hooks/useAdminData');
+ vi.mocked(useAdminData).mockReturnValue({
+ data: [],
+ loading: true,
+ error: null,
+ reload: vi.fn(),
+ });
+
+ render();
+
+ expect(
+ document.querySelector('.mantine-LoadingOverlay-root')
+ ).toBeInTheDocument();
+ });
+
+ it('shows error state', async () => {
+ const { useAdminData } = await import('../../../hooks/useAdminData');
+ vi.mocked(useAdminData).mockReturnValue({
+ data: [],
+ loading: false,
+ error: 'Failed to load workspaces',
+ reload: vi.fn(),
+ });
+
+ render();
+
+ expect(screen.getByText('Error')).toBeInTheDocument();
+ expect(screen.getByText('Failed to load workspaces')).toBeInTheDocument();
+ });
+
+ it('handles empty workspace list', async () => {
+ const { useAdminData } = await import('../../../hooks/useAdminData');
+ vi.mocked(useAdminData).mockReturnValue({
+ data: [],
+ loading: false,
+ error: null,
+ reload: vi.fn(),
+ });
+
+ render();
+
+ expect(screen.getByText('Workspace Management')).toBeInTheDocument();
+ // Table headers should still be present
+ expect(screen.getByText('Owner')).toBeInTheDocument();
+ expect(screen.getByText('Name')).toBeInTheDocument();
+ });
+});