import type { CreateExternalAPIReq } from '../declarations' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' // Import mocked service import { createExternalAPI } from '@/service/datasets' import AddExternalAPIModal from './index' // Mock API service vi.mock('@/service/datasets', () => ({ createExternalAPI: vi.fn(), })) // Mock toast context const mockNotify = vi.fn() vi.mock('@/app/components/base/toast', () => ({ useToastContext: () => ({ notify: mockNotify, }), })) describe('AddExternalAPIModal', () => { const defaultProps = { onSave: vi.fn(), onCancel: vi.fn(), isEditMode: false, } const initialData: CreateExternalAPIReq = { name: 'Test API', settings: { endpoint: 'https://api.example.com', api_key: 'test-key-12345', }, } beforeEach(() => { vi.clearAllMocks() }) describe('Rendering', () => { it('should render without crashing', () => { render() expect(screen.getByText('dataset.createExternalAPI')).toBeInTheDocument() }) it('should render create title when not in edit mode', () => { render() expect(screen.getByText('dataset.createExternalAPI')).toBeInTheDocument() }) it('should render edit title when in edit mode', () => { render() expect(screen.getByText('dataset.editExternalAPIFormTitle')).toBeInTheDocument() }) it('should render form fields', () => { render() expect(screen.getByLabelText(/name/i)).toBeInTheDocument() expect(screen.getByLabelText(/api endpoint/i)).toBeInTheDocument() expect(screen.getByLabelText(/api key/i)).toBeInTheDocument() }) it('should render cancel and save buttons', () => { render() expect(screen.getByText('dataset.externalAPIForm.cancel')).toBeInTheDocument() expect(screen.getByText('dataset.externalAPIForm.save')).toBeInTheDocument() }) it('should render encryption notice', () => { render() expect(screen.getByText('PKCS1_OAEP')).toBeInTheDocument() }) it('should render close button', () => { render() // Close button is rendered in a portal const closeButton = document.body.querySelector('.action-btn') expect(closeButton).toBeInTheDocument() }) }) describe('Edit Mode with Dataset Bindings', () => { it('should show warning when editing with dataset bindings', () => { const datasetBindings = [ { id: 'ds-1', name: 'Dataset 1' }, { id: 'ds-2', name: 'Dataset 2' }, ] render( , ) expect(screen.getByText('dataset.editExternalAPIFormWarning.front')).toBeInTheDocument() // Verify the count is displayed in the warning section const warningElement = screen.getByText('dataset.editExternalAPIFormWarning.front').parentElement expect(warningElement?.textContent).toContain('2') }) it('should not show warning when no dataset bindings', () => { render( , ) expect(screen.queryByText('dataset.editExternalAPIFormWarning.front')).not.toBeInTheDocument() }) }) describe('Form Interactions', () => { it('should update form values when input changes', () => { render() const nameInput = screen.getByLabelText(/name/i) fireEvent.change(nameInput, { target: { value: 'New API Name' } }) expect(nameInput).toHaveValue('New API Name') }) it('should initialize form with data in edit mode', () => { render() expect(screen.getByLabelText(/name/i)).toHaveValue('Test API') expect(screen.getByLabelText(/api endpoint/i)).toHaveValue('https://api.example.com') expect(screen.getByLabelText(/api key/i)).toHaveValue('test-key-12345') }) it('should disable save button when form has empty inputs', () => { render() const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button') expect(saveButton).toBeDisabled() }) it('should enable save button when all fields are filled', () => { render() const nameInput = screen.getByLabelText(/name/i) const endpointInput = screen.getByLabelText(/api endpoint/i) const apiKeyInput = screen.getByLabelText(/api key/i) fireEvent.change(nameInput, { target: { value: 'Test' } }) fireEvent.change(endpointInput, { target: { value: 'https://test.com' } }) fireEvent.change(apiKeyInput, { target: { value: 'key12345' } }) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button') expect(saveButton).not.toBeDisabled() }) }) describe('Create Mode - Save', () => { it('should create API and call onSave on success', async () => { const mockResponse = { id: 'new-api-123', tenant_id: 'tenant-1', name: 'Test', description: '', settings: { endpoint: 'https://test.com', api_key: 'key12345' }, dataset_bindings: [], created_by: 'user-1', created_at: '2021-01-01T00:00:00Z', } vi.mocked(createExternalAPI).mockResolvedValue(mockResponse) const onSave = vi.fn() const onCancel = vi.fn() render() const nameInput = screen.getByLabelText(/name/i) const endpointInput = screen.getByLabelText(/api endpoint/i) const apiKeyInput = screen.getByLabelText(/api key/i) fireEvent.change(nameInput, { target: { value: 'Test' } }) fireEvent.change(endpointInput, { target: { value: 'https://test.com' } }) fireEvent.change(apiKeyInput, { target: { value: 'key12345' } }) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { expect(createExternalAPI).toHaveBeenCalledWith({ body: { name: 'Test', settings: { endpoint: 'https://test.com', api_key: 'key12345' }, }, }) expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'External API saved successfully', }) expect(onSave).toHaveBeenCalledWith(mockResponse) expect(onCancel).toHaveBeenCalled() }) }) it('should show error notification when API key is too short', async () => { render() const nameInput = screen.getByLabelText(/name/i) const endpointInput = screen.getByLabelText(/api endpoint/i) const apiKeyInput = screen.getByLabelText(/api key/i) fireEvent.change(nameInput, { target: { value: 'Test' } }) fireEvent.change(endpointInput, { target: { value: 'https://test.com' } }) fireEvent.change(apiKeyInput, { target: { value: 'key' } }) // Less than 5 characters const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'common.apiBasedExtension.modal.apiKey.lengthError', }) }) }) it('should handle create API error', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) vi.mocked(createExternalAPI).mockRejectedValue(new Error('Create failed')) render() const nameInput = screen.getByLabelText(/name/i) const endpointInput = screen.getByLabelText(/api endpoint/i) const apiKeyInput = screen.getByLabelText(/api key/i) fireEvent.change(nameInput, { target: { value: 'Test' } }) fireEvent.change(endpointInput, { target: { value: 'https://test.com' } }) fireEvent.change(apiKeyInput, { target: { value: 'key12345' } }) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'Failed to save/update External API', }) }) consoleSpy.mockRestore() }) }) describe('Edit Mode - Save', () => { it('should call onEdit directly when editing without dataset bindings', async () => { const onEdit = vi.fn().mockResolvedValue(undefined) const onCancel = vi.fn() render( , ) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { // When no datasetBindings, onEdit is called directly with original form data expect(onEdit).toHaveBeenCalledWith({ name: 'Test API', settings: { endpoint: 'https://api.example.com', api_key: 'test-key-12345', }, }) }) }) it('should show confirm dialog when editing with dataset bindings', async () => { const datasetBindings = [{ id: 'ds-1', name: 'Dataset 1' }] const onEdit = vi.fn().mockResolvedValue(undefined) render( , ) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument() }) }) it('should proceed with save after confirming in edit mode with bindings', async () => { vi.mocked(createExternalAPI).mockResolvedValue({ id: 'api-123', tenant_id: 'tenant-1', name: 'Test API', description: '', settings: { endpoint: 'https://api.example.com', api_key: 'test-key-12345' }, dataset_bindings: [], created_by: 'user-1', created_at: '2021-01-01T00:00:00Z', }) const datasetBindings = [{ id: 'ds-1', name: 'Dataset 1' }] const onCancel = vi.fn() render( , ) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument() }) const confirmButton = screen.getByRole('button', { name: /confirm/i }) fireEvent.click(confirmButton) await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ type: 'success' }), ) }) }) it('should close confirm dialog when cancel is clicked', async () => { const datasetBindings = [{ id: 'ds-1', name: 'Dataset 1' }] render( , ) const saveButton = screen.getByText('dataset.externalAPIForm.save').closest('button')! fireEvent.click(saveButton) await waitFor(() => { expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument() }) // There are multiple cancel buttons, find the one in the confirm dialog const cancelButtons = screen.getAllByRole('button', { name: /cancel/i }) const confirmDialogCancelButton = cancelButtons[cancelButtons.length - 1] fireEvent.click(confirmDialogCancelButton) await waitFor(() => { // Confirm button should be gone after canceling expect(screen.queryAllByRole('button', { name: /confirm/i })).toHaveLength(0) }) }) }) describe('Cancel', () => { it('should call onCancel when cancel button is clicked', () => { const onCancel = vi.fn() render() const cancelButton = screen.getByText('dataset.externalAPIForm.cancel').closest('button')! fireEvent.click(cancelButton) expect(onCancel).toHaveBeenCalledTimes(1) }) it('should call onCancel when close button is clicked', () => { const onCancel = vi.fn() render() // Close button is rendered in a portal const closeButton = document.body.querySelector('.action-btn')! fireEvent.click(closeButton) expect(onCancel).toHaveBeenCalledTimes(1) }) }) describe('Edge Cases', () => { it('should handle undefined data in edit mode', () => { render() expect(screen.getByLabelText(/name/i)).toHaveValue('') }) it('should handle null datasetBindings', () => { render( , ) expect(screen.queryByText('dataset.editExternalAPIFormWarning.front')).not.toBeInTheDocument() }) it('should render documentation link in encryption notice', () => { render() const link = screen.getByRole('link', { name: 'PKCS1_OAEP' }) expect(link).toHaveAttribute('href', 'https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html') expect(link).toHaveAttribute('target', '_blank') }) }) })