/** * Component: Admin Dashboard Page Tests * Documentation: documentation/admin-dashboard.md */ // @vitest-environment jsdom import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import AdminDashboard from '@/app/admin/page'; const authenticatedFetcherMock = vi.hoisted(() => vi.fn()); const fetchJSONMock = vi.hoisted(() => vi.fn()); const mutateMock = vi.hoisted(() => vi.fn()); // Mock next/navigation for RecentRequestsTable component const mockRouter = vi.hoisted(() => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), })); const mockSearchParams = vi.hoisted(() => new URLSearchParams()); vi.mock('next/navigation', () => ({ useRouter: () => mockRouter, usePathname: () => '/admin', useSearchParams: () => mockSearchParams, })); const toastMock = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn(), })); const swrState = new Map }>(); vi.mock('swr', () => ({ default: (key: string) => { return swrState.get(key) || { data: undefined, error: undefined, mutate: vi.fn() }; }, mutate: mutateMock, })); vi.mock('@/lib/utils/api', () => ({ authenticatedFetcher: authenticatedFetcherMock, fetchJSON: fetchJSONMock, fetchWithAuth: vi.fn(), })); vi.mock('@/components/ui/Toast', () => ({ ToastProvider: ({ children }: { children: React.ReactNode }) => <>{children}, useToast: () => toastMock, })); vi.mock('@/components/requests/InteractiveTorrentSearchModal', () => ({ InteractiveTorrentSearchModal: () => null, })); describe('AdminDashboard', () => { beforeEach(() => { swrState.clear(); fetchJSONMock.mockReset(); mutateMock.mockReset(); toastMock.success.mockReset(); toastMock.error.mockReset(); mockRouter.push.mockReset(); mockRouter.replace.mockReset(); }); it('renders metrics, downloads, and recent requests', async () => { swrState.set('/api/admin/metrics', { data: { totalRequests: 12, activeDownloads: 2, completedLast30Days: 8, failedLast30Days: 1, totalUsers: 4, systemHealth: { status: 'healthy', issues: [] }, }, }); swrState.set('/api/admin/downloads/active', { data: { downloads: [ { requestId: 'r1', title: 'Active Book', author: 'Author One', progress: 55, speed: 1024, eta: 1200, user: 'Zach', startedAt: new Date('2024-01-01T00:00:00Z'), }, ], }, }); // RecentRequestsTable fetches from /api/admin/requests with query params swrState.set('/api/admin/requests?page=1&pageSize=25&search=&status=all&userId=&sortBy=createdAt&sortOrder=desc', { data: { requests: [ { requestId: 'req-1', title: 'Recent Book', author: 'Author Two', status: 'pending', userId: 'user-1', user: 'Sam', createdAt: new Date('2024-01-02T00:00:00Z'), completedAt: null, errorMessage: null, }, ], total: 1, page: 1, pageSize: 25, totalPages: 1, }, }); swrState.set('/api/admin/users', { data: { users: [] } }); swrState.set('/api/admin/requests/pending-approval', { data: { requests: [] } }); swrState.set('/api/admin/settings', { data: { ebook: { enabled: false } } }); render(); expect(await screen.findByText('Admin Dashboard')).toBeInTheDocument(); expect(screen.getByText('Total Requests')).toBeInTheDocument(); expect(screen.getByText('Active Book')).toBeInTheDocument(); expect(screen.getByText('Recent Book')).toBeInTheDocument(); }); it('approves a pending request and refreshes caches', async () => { swrState.set('/api/admin/metrics', { data: { totalRequests: 1, activeDownloads: 0, completedLast30Days: 0, failedLast30Days: 0, totalUsers: 1, systemHealth: { status: 'healthy', issues: [] }, }, }); swrState.set('/api/admin/downloads/active', { data: { downloads: [] } }); // RecentRequestsTable fetches from /api/admin/requests with query params swrState.set('/api/admin/requests?page=1&pageSize=25&search=&status=all&userId=&sortBy=createdAt&sortOrder=desc', { data: { requests: [], total: 0, page: 1, pageSize: 25, totalPages: 0 }, }); swrState.set('/api/admin/users', { data: { users: [] } }); swrState.set('/api/admin/settings', { data: { ebook: { enabled: false } } }); swrState.set('/api/admin/requests/pending-approval', { data: { requests: [ { id: 'pending-1', createdAt: new Date().toISOString(), audiobook: { title: 'Awaiting', author: 'Author', coverArtUrl: null }, user: { id: 'u1', plexUsername: 'User', avatarUrl: null }, }, ], }, }); fetchJSONMock.mockResolvedValue({ success: true }); render(); fireEvent.click(await screen.findByRole('button', { name: 'Approve' })); await waitFor(() => { expect(fetchJSONMock).toHaveBeenCalledWith('/api/admin/requests/pending-1/approve', { method: 'POST', body: JSON.stringify({ action: 'approve' }), }); expect(toastMock.success).toHaveBeenCalledWith('Request approved'); }); expect(mutateMock).toHaveBeenCalledWith('/api/admin/requests/pending-approval'); expect(mutateMock).toHaveBeenCalledWith('/api/admin/requests/recent'); expect(mutateMock).toHaveBeenCalledWith('/api/admin/metrics'); }); it('shows an error message when dashboard data fails to load', async () => { swrState.set('/api/admin/metrics', { error: new Error('Metrics unavailable') }); render(); expect(await screen.findByText('Error Loading Dashboard')).toBeInTheDocument(); expect(screen.getByText('Metrics unavailable')).toBeInTheDocument(); }); });