Add admin UX improvements, constants refactor, and comprehensive tests
- Introduced constants for admin hooks: `STATS_FETCH_LIMIT`, `DEFAULT_PAGE_LIMIT`, and `STATS_REFETCH_INTERVAL` to enhance readability and maintainability. - Updated query guards to ensure data fetching is restricted to superusers. - Enhanced accessibility across admin components by adding `aria-hidden` attributes and improving focus-visible styles. - Simplified `useAdminStats`, `useAdminUsers`, and `useAdminOrganizations` with shared constants. - Added 403 Forbidden page with proper structure, styling, and tests. - Implemented new tests for admin hooks, DashboardStats, AdminLayout, and ForbiddenPage for better coverage.
This commit is contained in:
330
frontend/tests/lib/api/hooks/useAdmin.test.tsx
Normal file
330
frontend/tests/lib/api/hooks/useAdmin.test.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* Tests for useAdmin hooks
|
||||
* Verifies admin statistics and list fetching functionality
|
||||
*/
|
||||
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useAdminStats, useAdminUsers, useAdminOrganizations } from '@/lib/api/hooks/useAdmin';
|
||||
import { adminListUsers, adminListOrganizations } from '@/lib/api/client';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/api/client');
|
||||
jest.mock('@/lib/auth/AuthContext');
|
||||
|
||||
const mockAdminListUsers = adminListUsers as jest.MockedFunction<typeof adminListUsers>;
|
||||
const mockAdminListOrganizations = adminListOrganizations as jest.MockedFunction<typeof adminListOrganizations>;
|
||||
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
||||
|
||||
describe('useAdmin hooks', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('useAdminStats', () => {
|
||||
const mockUsersData = {
|
||||
data: {
|
||||
data: [
|
||||
{ is_active: true },
|
||||
{ is_active: true },
|
||||
{ is_active: false },
|
||||
],
|
||||
pagination: { total: 3, page: 1, limit: 10000 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockOrgsData = {
|
||||
data: {
|
||||
pagination: { total: 5 },
|
||||
},
|
||||
};
|
||||
|
||||
it('fetches and calculates stats when user is superuser', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListUsers.mockResolvedValue(mockUsersData as any);
|
||||
mockAdminListOrganizations.mockResolvedValue(mockOrgsData as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminStats(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual({
|
||||
totalUsers: 3,
|
||||
activeUsers: 2,
|
||||
totalOrganizations: 5,
|
||||
totalSessions: 0,
|
||||
});
|
||||
|
||||
expect(mockAdminListUsers).toHaveBeenCalledWith({
|
||||
query: { page: 1, limit: 10000 },
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
expect(mockAdminListOrganizations).toHaveBeenCalledWith({
|
||||
query: { page: 1, limit: 10000 },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fetch when user is not superuser', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: false } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdminStats(), { wrapper });
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(mockAdminListUsers).not.toHaveBeenCalled();
|
||||
expect(mockAdminListOrganizations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fetch when user is null', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdminStats(), { wrapper });
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(mockAdminListUsers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles users API error', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListUsers.mockResolvedValue({ error: 'Users fetch failed' } as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminStats(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles organizations API error', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListUsers.mockResolvedValue(mockUsersData as any);
|
||||
mockAdminListOrganizations.mockResolvedValue({ error: 'Orgs fetch failed' } as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminStats(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAdminUsers', () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [{ id: '1' }, { id: '2' }],
|
||||
pagination: { total: 2, page: 1, limit: 50 },
|
||||
},
|
||||
};
|
||||
|
||||
it('fetches users when user is superuser', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListUsers.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminUsers(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockResponse.data);
|
||||
expect(mockAdminListUsers).toHaveBeenCalledWith({
|
||||
query: { page: 1, limit: 50 },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses custom page and limit parameters', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListUsers.mockResolvedValue(mockResponse as any);
|
||||
|
||||
renderHook(() => useAdminUsers(2, 100), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAdminListUsers).toHaveBeenCalledWith({
|
||||
query: { page: 2, limit: 100 },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fetch when user is not superuser', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: false } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdminUsers(), { wrapper });
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(mockAdminListUsers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles API error', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListUsers.mockResolvedValue({ error: 'Fetch failed' } as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminUsers(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAdminOrganizations', () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [{ id: '1' }, { id: '2' }],
|
||||
pagination: { total: 2, page: 1, limit: 50 },
|
||||
},
|
||||
};
|
||||
|
||||
it('fetches organizations when user is superuser', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListOrganizations.mockResolvedValue(mockResponse as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminOrganizations(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual(mockResponse.data);
|
||||
expect(mockAdminListOrganizations).toHaveBeenCalledWith({
|
||||
query: { page: 1, limit: 50 },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses custom page and limit parameters', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListOrganizations.mockResolvedValue(mockResponse as any);
|
||||
|
||||
renderHook(() => useAdminOrganizations(3, 25), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAdminListOrganizations).toHaveBeenCalledWith({
|
||||
query: { page: 3, limit: 25 },
|
||||
throwOnError: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fetch when user is not superuser', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: false } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useAdminOrganizations(), { wrapper });
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(mockAdminListOrganizations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles API error', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
mockAdminListOrganizations.mockResolvedValue({ error: 'Fetch failed' } as any);
|
||||
|
||||
const { result } = renderHook(() => useAdminOrganizations(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user