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();
});
});