import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import ApiTokensSection from '../ApiTokensSection'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, defaultValue?: string) => defaultValue || key, }), })); // Mock the hooks const mockTokens = [ { id: '1', name: 'Test Token', key_prefix: 'abc123', scopes: ['read:appointments', 'write:appointments'], is_active: true, created_at: '2024-01-01T00:00:00Z', last_used_at: '2024-01-02T00:00:00Z', expires_at: null, created_by: { full_name: 'John Doe', username: 'john' }, }, { id: '2', name: 'Revoked Token', key_prefix: 'xyz789', scopes: ['read:resources'], is_active: false, created_at: '2024-01-01T00:00:00Z', last_used_at: null, expires_at: null, created_by: null, }, ]; const mockUseApiTokens = vi.fn(); const mockUseCreateApiToken = vi.fn(); const mockUseRevokeApiToken = vi.fn(); const mockUseUpdateApiToken = vi.fn(); vi.mock('../../hooks/useApiTokens', () => ({ useApiTokens: () => mockUseApiTokens(), useCreateApiToken: () => mockUseCreateApiToken(), useRevokeApiToken: () => mockUseRevokeApiToken(), useUpdateApiToken: () => mockUseUpdateApiToken(), API_SCOPES: [ { value: 'read:appointments', label: 'Read Appointments', description: 'View appointments' }, { value: 'write:appointments', label: 'Write Appointments', description: 'Create/edit appointments' }, { value: 'read:resources', label: 'Read Resources', description: 'View resources' }, ], SCOPE_PRESETS: { read_only: { label: 'Read Only', description: 'View data only', scopes: ['read:appointments', 'read:resources'] }, read_write: { label: 'Read & Write', description: 'Full access', scopes: ['read:appointments', 'write:appointments', 'read:resources'] }, custom: { label: 'Custom', description: 'Select individual permissions', scopes: [] }, }, })); const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); return ({ children }: { children: React.ReactNode }) => ( {children} ); }; describe('ApiTokensSection', () => { beforeEach(() => { vi.clearAllMocks(); mockUseCreateApiToken.mockReturnValue({ mutateAsync: vi.fn(), isPending: false }); mockUseRevokeApiToken.mockReturnValue({ mutateAsync: vi.fn(), isPending: false }); mockUseUpdateApiToken.mockReturnValue({ mutateAsync: vi.fn(), isPending: false }); }); it('renders loading state', () => { mockUseApiTokens.mockReturnValue({ data: undefined, isLoading: true, error: null }); render(, { wrapper: createWrapper() }); expect(document.querySelector('.animate-spin')).toBeInTheDocument(); }); it('renders error state', () => { mockUseApiTokens.mockReturnValue({ data: undefined, isLoading: false, error: new Error('Failed') }); render(, { wrapper: createWrapper() }); expect(screen.getByText(/Failed to load API tokens/)).toBeInTheDocument(); }); it('renders empty state when no tokens', () => { mockUseApiTokens.mockReturnValue({ data: [], isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('No API tokens yet')).toBeInTheDocument(); }); it('renders tokens list', () => { mockUseApiTokens.mockReturnValue({ data: mockTokens, isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('Test Token')).toBeInTheDocument(); expect(screen.getByText('Revoked Token')).toBeInTheDocument(); }); it('renders section title', () => { mockUseApiTokens.mockReturnValue({ data: [], isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('API Tokens')).toBeInTheDocument(); }); it('renders New Token button', () => { mockUseApiTokens.mockReturnValue({ data: [], isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('New Token')).toBeInTheDocument(); }); it('renders API Docs link', () => { mockUseApiTokens.mockReturnValue({ data: [], isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('API Docs')).toBeInTheDocument(); }); it('opens new token modal when button clicked', () => { mockUseApiTokens.mockReturnValue({ data: mockTokens, isLoading: false, error: null }); render(, { wrapper: createWrapper() }); fireEvent.click(screen.getByText('New Token')); // Modal title should appear expect(screen.getByRole('heading', { name: 'Create API Token' })).toBeInTheDocument(); }); it('shows active tokens count', () => { mockUseApiTokens.mockReturnValue({ data: mockTokens, isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText(/Active Tokens \(1\)/)).toBeInTheDocument(); }); it('shows revoked tokens count', () => { mockUseApiTokens.mockReturnValue({ data: mockTokens, isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText(/Revoked Tokens \(1\)/)).toBeInTheDocument(); }); it('shows token key prefix', () => { mockUseApiTokens.mockReturnValue({ data: mockTokens, isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText(/abc123••••••••/)).toBeInTheDocument(); }); it('shows revoked badge for inactive tokens', () => { mockUseApiTokens.mockReturnValue({ data: mockTokens, isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('Revoked')).toBeInTheDocument(); }); it('renders description text', () => { mockUseApiTokens.mockReturnValue({ data: [], isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText(/Create and manage API tokens/)).toBeInTheDocument(); }); it('renders create button in empty state', () => { mockUseApiTokens.mockReturnValue({ data: [], isLoading: false, error: null }); render(, { wrapper: createWrapper() }); expect(screen.getByText('Create API Token')).toBeInTheDocument(); }); });