test: add tests for dataset document detail (#31274)

Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-01-27 15:43:27 +08:00
committed by GitHub
parent eca26a9b9b
commit c8abe1c306
105 changed files with 28225 additions and 686 deletions

View File

@ -1,5 +1,6 @@
import type { Credential } from '@/app/components/tools/types'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
import ConfigCredential from './config-credentials'
@ -14,47 +15,472 @@ describe('ConfigCredential', () => {
vi.clearAllMocks()
})
it('renders and calls onHide when cancel is pressed', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
// Tests for basic rendering
describe('Rendering', () => {
it('should render without crashing', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
expect(screen.getByText('tools.createTool.authMethod.title')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('common.operation.cancel'))
it('should render all three auth type options', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
expect(mockOnHide).toHaveBeenCalledTimes(1)
expect(mockOnChange).not.toHaveBeenCalled()
expect(screen.getByText('tools.createTool.authMethod.types.none')).toBeInTheDocument()
expect(screen.getByText('tools.createTool.authMethod.types.api_key_header')).toBeInTheDocument()
expect(screen.getByText('tools.createTool.authMethod.types.api_key_query')).toBeInTheDocument()
})
it('should render with positionCenter prop', async () => {
await act(async () => {
render(
<ConfigCredential
positionCenter
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
expect(screen.getByText('tools.createTool.authMethod.title')).toBeInTheDocument()
})
})
it('allows selecting apiKeyHeader and submits the new credential', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
// Tests for cancel and save buttons
describe('Cancel and Save Actions', () => {
it('should call onHide when cancel is pressed', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('common.operation.cancel'))
expect(mockOnHide).toHaveBeenCalledTimes(1)
expect(mockOnChange).not.toHaveBeenCalled()
})
it('should call both onChange and onHide when save is pressed', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledTimes(1)
expect(mockOnHide).toHaveBeenCalledTimes(1)
})
})
// Tests for "none" auth type selection
describe('None Auth Type', () => {
it('should select none auth type and save', async () => {
const credentialWithApiKey: Credential = {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'X-Api-Key',
api_key_value: 'test-value',
api_key_header_prefix: AuthHeaderPrefix.bearer,
}
await act(async () => {
render(
<ConfigCredential
credential={credentialWithApiKey}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
// Switch to none auth type
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.none'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith({
auth_type: AuthType.none,
})
})
})
// Tests for API Key Header auth type
describe('API Key Header Auth Type', () => {
it('should select apiKeyHeader and show header prefix options', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
// Header prefix options should appear
expect(screen.getByText('tools.createTool.authHeaderPrefix.types.basic')).toBeInTheDocument()
expect(screen.getByText('tools.createTool.authHeaderPrefix.types.bearer')).toBeInTheDocument()
expect(screen.getByText('tools.createTool.authHeaderPrefix.types.custom')).toBeInTheDocument()
})
it('should submit apiKeyHeader credential with default values', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
const headerInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiKeyPlaceholder')
const valueInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiValuePlaceholder')
fireEvent.change(headerInput, { target: { value: 'X-Auth' } })
fireEvent.change(valueInput, { target: { value: 'sEcReT' } })
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith({
auth_type: AuthType.apiKeyHeader,
api_key_header: 'X-Auth',
api_key_header_prefix: AuthHeaderPrefix.custom,
api_key_value: 'sEcReT',
})
expect(mockOnHide).toHaveBeenCalled()
})
it('should select basic header prefix', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
fireEvent.click(screen.getByText('tools.createTool.authHeaderPrefix.types.basic'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
auth_type: AuthType.apiKeyHeader,
api_key_header_prefix: AuthHeaderPrefix.basic,
}),
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
const headerInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiKeyPlaceholder')
const valueInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiValuePlaceholder')
fireEvent.change(headerInput, { target: { value: 'X-Auth' } })
fireEvent.change(valueInput, { target: { value: 'sEcReT' } })
fireEvent.click(screen.getByText('common.operation.save'))
it('should select bearer header prefix', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
expect(mockOnChange).toHaveBeenCalledWith({
auth_type: AuthType.apiKeyHeader,
api_key_header: 'X-Auth',
api_key_header_prefix: AuthHeaderPrefix.custom,
api_key_value: 'sEcReT',
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
fireEvent.click(screen.getByText('tools.createTool.authHeaderPrefix.types.bearer'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
auth_type: AuthType.apiKeyHeader,
api_key_header_prefix: AuthHeaderPrefix.bearer,
}),
)
})
it('should select custom header prefix', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
// Start with none, switch to apiKeyHeader (which defaults to custom)
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
// Select bearer first, then custom to test switching
fireEvent.click(screen.getByText('tools.createTool.authHeaderPrefix.types.bearer'))
fireEvent.click(screen.getByText('tools.createTool.authHeaderPrefix.types.custom'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
auth_type: AuthType.apiKeyHeader,
api_key_header_prefix: AuthHeaderPrefix.custom,
}),
)
})
it('should preserve existing values when switching to apiKeyHeader', async () => {
const existingCredential: Credential = {
auth_type: AuthType.none,
api_key_header: 'Existing-Header',
api_key_value: 'existing-value',
api_key_header_prefix: AuthHeaderPrefix.bearer,
}
await act(async () => {
render(
<ConfigCredential
credential={existingCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_header'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
auth_type: AuthType.apiKeyHeader,
api_key_header: 'Existing-Header',
api_key_value: 'existing-value',
api_key_header_prefix: AuthHeaderPrefix.bearer,
}),
)
})
})
// Tests for API Key Query auth type
describe('API Key Query Auth Type', () => {
it('should select apiKeyQuery and show query param input', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_query'))
// Query param input should appear
expect(screen.getByPlaceholderText('tools.createTool.authMethod.types.queryParamPlaceholder')).toBeInTheDocument()
})
it('should submit apiKeyQuery credential with default values', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_query'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith({
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'key',
api_key_value: '',
})
})
it('should edit query param name and value', async () => {
await act(async () => {
render(
<ConfigCredential
credential={baseCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_query'))
const queryParamInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.queryParamPlaceholder')
const valueInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiValuePlaceholder')
fireEvent.change(queryParamInput, { target: { value: 'api_key' } })
fireEvent.change(valueInput, { target: { value: 'my-secret-key' } })
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith({
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'api_key',
api_key_value: 'my-secret-key',
})
})
it('should preserve existing values when switching to apiKeyQuery', async () => {
const existingCredential: Credential = {
auth_type: AuthType.none,
api_key_query_param: 'existing_param',
api_key_value: 'existing-value',
}
await act(async () => {
render(
<ConfigCredential
credential={existingCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_query'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'existing_param',
api_key_value: 'existing-value',
}),
)
})
})
// Tests for switching between auth types
describe('Switching Auth Types', () => {
it('should switch from apiKeyHeader to apiKeyQuery', async () => {
const headerCredential: Credential = {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'Authorization',
api_key_value: 'Bearer token',
api_key_header_prefix: AuthHeaderPrefix.bearer,
}
await act(async () => {
render(
<ConfigCredential
credential={headerCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
// Switch to query
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.api_key_query'))
// Header prefix options should disappear
expect(screen.queryByText('tools.createTool.authHeaderPrefix.types.basic')).not.toBeInTheDocument()
// Query param input should appear
expect(screen.getByPlaceholderText('tools.createTool.authMethod.types.queryParamPlaceholder')).toBeInTheDocument()
})
it('should switch from apiKeyQuery to none', async () => {
const queryCredential: Credential = {
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'key',
api_key_value: 'value',
}
await act(async () => {
render(
<ConfigCredential
credential={queryCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
// Switch to none
fireEvent.click(screen.getByText('tools.createTool.authMethod.types.none'))
fireEvent.click(screen.getByText('common.operation.save'))
expect(mockOnChange).toHaveBeenCalledWith({
auth_type: AuthType.none,
})
})
})
// Tests for initial credential state
describe('Initial Credential State', () => {
it('should show apiKeyHeader fields when initial auth type is apiKeyHeader', async () => {
const headerCredential: Credential = {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'X-Custom-Header',
api_key_value: 'secret123',
api_key_header_prefix: AuthHeaderPrefix.bearer,
}
await act(async () => {
render(
<ConfigCredential
credential={headerCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
// Header inputs should be visible with initial values
const headerInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.apiKeyPlaceholder')
expect(headerInput).toHaveValue('X-Custom-Header')
})
it('should show apiKeyQuery fields when initial auth type is apiKeyQuery', async () => {
const queryCredential: Credential = {
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'apikey',
api_key_value: 'queryvalue',
}
await act(async () => {
render(
<ConfigCredential
credential={queryCredential}
onChange={mockOnChange}
onHide={mockOnHide}
/>,
)
})
// Query param input should be visible with initial value
const queryParamInput = screen.getByPlaceholderText('tools.createTool.authMethod.types.queryParamPlaceholder')
expect(queryParamInput).toHaveValue('apikey')
})
expect(mockOnHide).toHaveBeenCalled()
})
})

View File

@ -1,8 +1,10 @@
import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { Plan } from '@/app/components/billing/type'
import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
import { parseParamsSchema } from '@/service/tools'
import EditCustomCollectionModal from './index'
@ -52,6 +54,18 @@ vi.mock('@/context/i18n', async () => {
}
})
// Mock EmojiPicker
vi.mock('@/app/components/base/emoji-picker', () => ({
default: ({ onSelect, onClose }: { onSelect: (icon: string, background: string) => void, onClose: () => void }) => {
return (
<div data-testid="emoji-picker">
<button data-testid="select-emoji" onClick={() => onSelect('🚀', '#FF0000')}>Select Emoji</button>
<button data-testid="close-emoji-picker" onClick={onClose}>Close</button>
</div>
)
},
}))
describe('EditCustomCollectionModal', () => {
const mockOnHide = vi.fn()
const mockOnAdd = vi.fn()
@ -75,80 +89,490 @@ describe('EditCustomCollectionModal', () => {
} as ProviderContextState)
})
const renderModal = () => render(
const renderModal = (props?: {
payload?: {
provider: string
credentials: { auth_type: AuthType, api_key_header?: string, api_key_header_prefix?: AuthHeaderPrefix, api_key_value?: string }
schema_type: string
schema: string
icon: { content: string, background: string }
privacy_policy?: string
custom_disclaimer?: string
labels?: string[]
tools?: Array<{ operation_id: string, summary: string, method: string, server_url: string, parameters: Array<{ name: string, label: { en_US: string, zh_Hans: string } }> }>
}
positionLeft?: boolean
dialogClassName?: string
}) => render(
<EditCustomCollectionModal
payload={undefined}
payload={props?.payload}
onHide={mockOnHide}
onAdd={mockOnAdd}
onEdit={mockOnEdit}
onRemove={mockOnRemove}
positionLeft={props?.positionLeft}
dialogClassName={props?.dialogClassName}
/>,
)
it('shows an error when the provider name is missing', async () => {
renderModal()
// Tests for Add mode (no payload)
describe('Add Mode', () => {
it('should render add mode title when no payload', () => {
renderModal()
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '{}' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
expect(screen.getByText('tools.createTool.title')).toBeInTheDocument()
})
fireEvent.click(screen.getByText('common.operation.save'))
it('should show error when provider name is missing', async () => {
renderModal()
await waitFor(() => {
expect(toastNotifySpy).toHaveBeenCalledWith(expect.objectContaining({
message: 'common.errorMsg.fieldRequired:{"field":"tools.createTool.name"}',
type: 'error',
}))
})
expect(mockOnAdd).not.toHaveBeenCalled()
})
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '{}' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
})
it('shows an error when the schema is missing', async () => {
renderModal()
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
fireEvent.change(providerInput, { target: { value: 'provider' } })
fireEvent.click(screen.getByText('common.operation.save'))
await waitFor(() => {
expect(toastNotifySpy).toHaveBeenCalledWith(expect.objectContaining({
message: 'common.errorMsg.fieldRequired:{"field":"tools.createTool.schema"}',
type: 'error',
}))
})
expect(mockOnAdd).not.toHaveBeenCalled()
})
it('saves a valid custom collection', async () => {
renderModal()
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
fireEvent.change(providerInput, { target: { value: 'provider' } })
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '{}' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
})
await act(async () => {
fireEvent.click(screen.getByText('common.operation.save'))
await waitFor(() => {
expect(toastNotifySpy).toHaveBeenCalledWith(expect.objectContaining({
message: 'common.errorMsg.fieldRequired:{"field":"tools.createTool.name"}',
type: 'error',
}))
})
expect(mockOnAdd).not.toHaveBeenCalled()
})
await waitFor(() => {
expect(mockOnAdd).toHaveBeenCalledWith(expect.objectContaining({
provider: 'provider',
schema: '{}',
schema_type: 'openapi',
it('should show error when schema is missing', async () => {
renderModal()
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
fireEvent.change(providerInput, { target: { value: 'provider' } })
fireEvent.click(screen.getByText('common.operation.save'))
await waitFor(() => {
expect(toastNotifySpy).toHaveBeenCalledWith(expect.objectContaining({
message: 'common.errorMsg.fieldRequired:{"field":"tools.createTool.schema"}',
type: 'error',
}))
})
expect(mockOnAdd).not.toHaveBeenCalled()
})
it('should save a valid custom collection', async () => {
renderModal()
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
fireEvent.change(providerInput, { target: { value: 'provider' } })
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '{}' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
})
await act(async () => {
fireEvent.click(screen.getByText('common.operation.save'))
})
await waitFor(() => {
expect(mockOnAdd).toHaveBeenCalledWith(expect.objectContaining({
provider: 'provider',
schema: '{}',
schema_type: 'openapi',
credentials: {
auth_type: 'none',
},
labels: [],
}))
expect(toastNotifySpy).not.toHaveBeenCalled()
})
})
it('should call onHide when cancel is clicked', () => {
renderModal()
fireEvent.click(screen.getByText('common.operation.cancel'))
expect(mockOnHide).toHaveBeenCalled()
})
})
// Tests for Edit mode (with payload)
describe('Edit Mode', () => {
const editPayload = {
provider: 'existing-provider',
credentials: {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'X-Api-Key',
api_key_header_prefix: AuthHeaderPrefix.bearer,
api_key_value: 'secret-key',
},
schema_type: 'openapi',
schema: '{"openapi": "3.0.0"}',
icon: { content: '🔧', background: '#FFCC00' },
privacy_policy: 'https://example.com/privacy',
custom_disclaimer: 'Use at your own risk',
labels: ['api', 'tools'],
tools: [{
operation_id: 'getUsers',
summary: 'Get all users',
method: 'GET',
server_url: 'https://api.example.com/users',
parameters: [{
name: 'limit',
label: { en_US: 'Limit', zh_Hans: '限制' },
}],
}],
}
it('should render edit mode title when payload is provided', () => {
renderModal({ payload: editPayload })
expect(screen.getByText('tools.createTool.editTitle')).toBeInTheDocument()
})
it('should show delete button in edit mode', () => {
renderModal({ payload: editPayload })
expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
})
it('should call onRemove when delete button is clicked', () => {
renderModal({ payload: editPayload })
fireEvent.click(screen.getByText('common.operation.delete'))
expect(mockOnRemove).toHaveBeenCalled()
})
it('should call onEdit with original_provider when saving in edit mode', async () => {
renderModal({ payload: editPayload })
// Change the provider name
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
fireEvent.change(providerInput, { target: { value: 'updated-provider' } })
await act(async () => {
fireEvent.click(screen.getByText('common.operation.save'))
})
await waitFor(() => {
expect(mockOnEdit).toHaveBeenCalledWith(expect.objectContaining({
provider: 'updated-provider',
original_provider: 'existing-provider',
}))
})
})
it('should display existing provider name', () => {
renderModal({ payload: editPayload })
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
expect(providerInput).toHaveValue('existing-provider')
})
it('should display existing schema', () => {
renderModal({ payload: editPayload })
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
expect(schemaInput).toHaveValue('{"openapi": "3.0.0"}')
})
it('should display available tools table', () => {
renderModal({ payload: editPayload })
expect(screen.getByText('getUsers')).toBeInTheDocument()
expect(screen.getByText('Get all users')).toBeInTheDocument()
expect(screen.getByText('GET')).toBeInTheDocument()
})
it('should strip credential fields when auth_type is none on save', async () => {
const payloadWithNoneAuth = {
...editPayload,
credentials: {
auth_type: 'none',
auth_type: AuthType.none,
api_key_header: 'should-be-removed',
api_key_header_prefix: AuthHeaderPrefix.bearer,
api_key_value: 'should-be-removed',
},
labels: [],
}))
expect(toastNotifySpy).not.toHaveBeenCalled()
}
renderModal({ payload: payloadWithNoneAuth })
await act(async () => {
fireEvent.click(screen.getByText('common.operation.save'))
})
await waitFor(() => {
expect(mockOnEdit).toHaveBeenCalledWith(expect.objectContaining({
credentials: {
auth_type: AuthType.none,
},
}))
// These fields should NOT be present
const callArg = mockOnEdit.mock.calls[0][0]
expect(callArg.credentials.api_key_header).toBeUndefined()
expect(callArg.credentials.api_key_header_prefix).toBeUndefined()
expect(callArg.credentials.api_key_value).toBeUndefined()
})
})
})
// Tests for Schema parsing
describe('Schema Parsing', () => {
it('should parse schema and update params when schema changes', async () => {
parseParamsSchemaMock.mockResolvedValueOnce({
parameters_schema: [{
operation_id: 'newOp',
summary: 'New operation',
method: 'POST',
server_url: 'https://api.example.com/new',
parameters: [],
}],
schema_type: 'swagger',
})
renderModal()
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '{"swagger": "2.0"}' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{"swagger": "2.0"}')
})
await waitFor(() => {
expect(screen.getByText('newOp')).toBeInTheDocument()
})
})
it('should handle schema parse error and reset params', async () => {
parseParamsSchemaMock.mockRejectedValueOnce(new Error('Parse error'))
renderModal()
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: 'invalid schema' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('invalid schema')
})
// The table should still be visible but empty (no tools)
expect(screen.getByText('tools.createTool.availableTools.title')).toBeInTheDocument()
})
it('should not parse schema when empty', async () => {
renderModal()
// Clear any calls from initial render
parseParamsSchemaMock.mockClear()
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '' } })
// Wait a bit and check that parseParamsSchema was not called with empty string
await new Promise(resolve => setTimeout(resolve, 100))
expect(parseParamsSchemaMock).not.toHaveBeenCalledWith('')
})
})
// Tests for Icon Section
describe('Icon Section', () => {
it('should render icon section', () => {
renderModal()
// The name input should be present
const nameInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
expect(nameInput).toBeInTheDocument()
})
it('should render name input section', () => {
renderModal()
// Name label should be present
expect(screen.getByText('tools.createTool.name')).toBeInTheDocument()
})
})
// Tests for Credentials Modal
describe('Credentials Modal', () => {
it('should show auth method section title', () => {
renderModal()
expect(screen.getByText('tools.createTool.authMethod.title')).toBeInTheDocument()
})
it('should display current auth type', () => {
renderModal()
// The default auth type is 'none'
expect(screen.getByText('tools.createTool.authMethod.types.none')).toBeInTheDocument()
})
})
// Tests for Test API Modal
describe('Test API Modal', () => {
const payloadWithTools = {
provider: 'test-provider',
credentials: { auth_type: AuthType.none },
schema_type: 'openapi',
schema: '{}',
icon: { content: '🔧', background: '#FFCC00' },
tools: [{
operation_id: 'testOp',
summary: 'Test operation',
method: 'POST',
server_url: 'https://api.example.com/test',
parameters: [],
}],
}
it('should render test button in available tools table', () => {
renderModal({ payload: payloadWithTools })
// Find the test button
const testButton = screen.getByText('tools.createTool.availableTools.test')
expect(testButton).toBeInTheDocument()
})
it('should display tool information in the table', () => {
renderModal({ payload: payloadWithTools })
expect(screen.getByText('testOp')).toBeInTheDocument()
expect(screen.getByText('Test operation')).toBeInTheDocument()
expect(screen.getByText('POST')).toBeInTheDocument()
})
})
// Tests for Privacy Policy and Custom Disclaimer
describe('Privacy Policy and Custom Disclaimer', () => {
it('should update privacy policy input', () => {
renderModal()
const privacyInput = screen.getByPlaceholderText('tools.createTool.privacyPolicyPlaceholder')
fireEvent.change(privacyInput, { target: { value: 'https://example.com/privacy' } })
expect(privacyInput).toHaveValue('https://example.com/privacy')
})
it('should update custom disclaimer input', () => {
renderModal()
const disclaimerInput = screen.getByPlaceholderText('tools.createTool.customDisclaimerPlaceholder')
fireEvent.change(disclaimerInput, { target: { value: 'Custom disclaimer text' } })
expect(disclaimerInput).toHaveValue('Custom disclaimer text')
})
it('should include privacy policy and custom disclaimer in save payload', async () => {
renderModal()
const providerInput = screen.getByPlaceholderText('tools.createTool.toolNamePlaceHolder')
fireEvent.change(providerInput, { target: { value: 'test-provider' } })
const schemaInput = screen.getByPlaceholderText('tools.createTool.schemaPlaceHolder')
fireEvent.change(schemaInput, { target: { value: '{}' } })
const privacyInput = screen.getByPlaceholderText('tools.createTool.privacyPolicyPlaceholder')
fireEvent.change(privacyInput, { target: { value: 'https://privacy.example.com' } })
const disclaimerInput = screen.getByPlaceholderText('tools.createTool.customDisclaimerPlaceholder')
fireEvent.change(disclaimerInput, { target: { value: 'My disclaimer' } })
await waitFor(() => {
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
})
await act(async () => {
fireEvent.click(screen.getByText('common.operation.save'))
})
await waitFor(() => {
expect(mockOnAdd).toHaveBeenCalledWith(expect.objectContaining({
privacy_policy: 'https://privacy.example.com',
custom_disclaimer: 'My disclaimer',
}))
})
})
})
// Tests for Props
describe('Props', () => {
it('should render with positionLeft prop', () => {
renderModal({ positionLeft: true })
expect(screen.getByText('tools.createTool.title')).toBeInTheDocument()
})
it('should render with dialogClassName prop', () => {
renderModal({ dialogClassName: 'custom-dialog-class' })
expect(screen.getByText('tools.createTool.title')).toBeInTheDocument()
})
})
// Tests for getPath helper function
describe('URL Path Extraction', () => {
const payloadWithVariousUrls = (serverUrl: string) => ({
provider: 'test-provider',
credentials: { auth_type: AuthType.none },
schema_type: 'openapi',
schema: '{}',
icon: { content: '🔧', background: '#FFCC00' },
tools: [{
operation_id: 'testOp',
summary: 'Test',
method: 'GET',
server_url: serverUrl,
parameters: [],
}],
})
it('should extract path from full URL', () => {
renderModal({ payload: payloadWithVariousUrls('https://api.example.com/users/list') })
expect(screen.getByText('/users/list')).toBeInTheDocument()
})
it('should handle URL with encoded characters', () => {
renderModal({ payload: payloadWithVariousUrls('https://api.example.com/users%20list') })
expect(screen.getByText('/users list')).toBeInTheDocument()
})
it('should handle empty URL', () => {
renderModal({ payload: payloadWithVariousUrls('') })
// Should not crash and show the row
expect(screen.getByText('testOp')).toBeInTheDocument()
})
it('should handle invalid URL by returning the original string', () => {
renderModal({ payload: payloadWithVariousUrls('not-a-valid-url') })
// Should show the original string
expect(screen.getByText('not-a-valid-url')).toBeInTheDocument()
})
it('should handle URL with only domain', () => {
renderModal({ payload: payloadWithVariousUrls('https://api.example.com') })
// Path would be empty or "/"
expect(screen.getByText('testOp')).toBeInTheDocument()
})
})
// Tests for Schema spec link
describe('Schema Spec Link', () => {
it('should render swagger spec link', () => {
renderModal()
const link = screen.getByText('tools.createTool.viewSchemaSpec')
expect(link.closest('a')).toHaveAttribute('href', 'https://swagger.io/specification/')
expect(link.closest('a')).toHaveAttribute('target', '_blank')
})
})
})

View File

@ -1,6 +1,7 @@
import type { CustomCollectionBackend, CustomParamSchema } from '@/app/components/tools/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { AuthType } from '@/app/components/tools/types'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types'
import { testAPIAvailable } from '@/service/tools'
import TestApi from './test-api'
@ -28,6 +29,7 @@ describe('TestApi', () => {
id: 'test-id',
labels: [],
}
const tool: CustomParamSchema = {
operation_id: 'testOp',
summary: 'summary',
@ -39,46 +41,305 @@ describe('TestApi', () => {
en_US: 'Limit',
zh_Hans: '限制',
},
// eslint-disable-next-line ts/no-explicit-any
} as any],
} as CustomParamSchema['parameters'][0]],
}
const renderTestApi = () => {
const mockOnHide = vi.fn()
const renderTestApi = (props?: {
customCollection?: CustomCollectionBackend
tool?: CustomParamSchema
positionCenter?: boolean
}) => {
return render(
<TestApi
customCollection={customCollection}
tool={tool}
onHide={vi.fn()}
customCollection={props?.customCollection ?? customCollection}
tool={props?.tool ?? tool}
onHide={props ? mockOnHide : vi.fn()}
positionCenter={props?.positionCenter}
/>,
)
}
beforeEach(() => {
vi.clearAllMocks()
testAPIAvailableMock.mockReset()
})
it('renders parameters and runs the API test', async () => {
testAPIAvailableMock.mockResolvedValueOnce({ result: 'ok' })
renderTestApi()
// Tests for basic rendering
describe('Rendering', () => {
it('should render without crashing', async () => {
await act(async () => {
renderTestApi()
})
const parameterInput = screen.getAllByRole('textbox')[0]
fireEvent.change(parameterInput, { target: { value: '5' } })
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
expect(screen.getByText('tools.test.testResult')).toBeInTheDocument()
})
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith({
provider_name: customCollection.provider,
tool_name: tool.operation_id,
it('should display tool name in the title', async () => {
await act(async () => {
renderTestApi()
})
expect(screen.getByText(/testOp/)).toBeInTheDocument()
})
it('should render parameters table', async () => {
await act(async () => {
renderTestApi()
})
expect(screen.getByText('tools.test.parameters')).toBeInTheDocument()
expect(screen.getByText('tools.test.value')).toBeInTheDocument()
expect(screen.getByText('Limit')).toBeInTheDocument()
})
it('should render test result placeholder', async () => {
await act(async () => {
renderTestApi()
})
expect(screen.getByText('tools.test.testResultPlaceholder')).toBeInTheDocument()
})
it('should render with positionCenter prop', async () => {
await act(async () => {
renderTestApi({ positionCenter: true })
})
expect(screen.getByText('tools.test.testResult')).toBeInTheDocument()
})
})
// Tests for API test execution
describe('API Test Execution', () => {
it('should run API test with parameters and show result', async () => {
testAPIAvailableMock.mockResolvedValueOnce({ result: 'ok' })
renderTestApi()
const parameterInput = screen.getAllByRole('textbox')[0]
fireEvent.change(parameterInput, { target: { value: '5' } })
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith({
provider_name: customCollection.provider,
tool_name: tool.operation_id,
credentials: {
auth_type: AuthType.none,
},
schema_type: customCollection.schema_type,
schema: customCollection.schema,
parameters: {
limit: '5',
},
})
expect(screen.getByText('ok')).toBeInTheDocument()
})
})
it('should display error result when API returns error', async () => {
testAPIAvailableMock.mockResolvedValueOnce({ error: 'API Error occurred' })
renderTestApi()
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(screen.getByText('API Error occurred')).toBeInTheDocument()
})
})
it('should call API when test button is clicked', async () => {
testAPIAvailableMock.mockResolvedValueOnce({ result: 'test completed' })
await act(async () => {
renderTestApi()
})
// Click test button
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
})
// API should have been called
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledTimes(1)
expect(screen.getByText('test completed')).toBeInTheDocument()
})
})
it('should strip extra credential fields when auth_type is none', async () => {
const collectionWithExtraFields: CustomCollectionBackend = {
...customCollection,
credentials: {
auth_type: AuthType.none,
api_key_header: 'X-Api-Key',
api_key_header_prefix: AuthHeaderPrefix.bearer,
api_key_value: 'secret',
},
schema_type: customCollection.schema_type,
schema: customCollection.schema,
parameters: {
limit: '5',
},
}
testAPIAvailableMock.mockResolvedValueOnce({ result: 'success' })
renderTestApi({ customCollection: collectionWithExtraFields })
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith(
expect.objectContaining({
credentials: {
auth_type: AuthType.none,
},
}),
)
})
})
})
// Tests for credentials modal
describe('Credentials Modal', () => {
it('should show auth method display text', async () => {
await act(async () => {
renderTestApi()
})
// Check that the auth method is displayed
expect(screen.getByText('tools.createTool.authMethod.types.none')).toBeInTheDocument()
})
it('should display current auth type in the button', async () => {
const collectionWithHeader: CustomCollectionBackend = {
...customCollection,
credentials: {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'X-Api-Key',
api_key_header_prefix: AuthHeaderPrefix.bearer,
api_key_value: 'token',
},
}
await act(async () => {
renderTestApi({ customCollection: collectionWithHeader })
})
// Check that the auth method display shows the correct type
expect(screen.getByText('tools.createTool.authMethod.types.api_key_header')).toBeInTheDocument()
})
})
// Tests for multiple parameters
describe('Multiple Parameters', () => {
it('should handle multiple parameters', async () => {
const toolWithMultipleParams: CustomParamSchema = {
...tool,
parameters: [
{
name: 'limit',
label: { en_US: 'Limit', zh_Hans: '限制' },
} as CustomParamSchema['parameters'][0],
{
name: 'offset',
label: { en_US: 'Offset', zh_Hans: '偏移' },
} as CustomParamSchema['parameters'][0],
],
}
testAPIAvailableMock.mockResolvedValueOnce({ result: 'multi-param success' })
renderTestApi({ tool: toolWithMultipleParams })
const inputs = screen.getAllByRole('textbox')
fireEvent.change(inputs[0], { target: { value: '10' } })
fireEvent.change(inputs[1], { target: { value: '20' } })
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith(
expect.objectContaining({
parameters: {
limit: '10',
offset: '20',
},
}),
)
})
})
it('should handle empty parameters', async () => {
testAPIAvailableMock.mockResolvedValueOnce({ result: 'empty params success' })
renderTestApi()
// Don't fill in any parameters
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith(
expect.objectContaining({
parameters: {},
}),
)
})
})
})
// Tests for different auth types
describe('Different Auth Types', () => {
it('should pass apiKeyHeader credentials to API', async () => {
const collectionWithHeader: CustomCollectionBackend = {
...customCollection,
credentials: {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'Authorization',
api_key_header_prefix: AuthHeaderPrefix.bearer,
api_key_value: 'test-token',
},
}
testAPIAvailableMock.mockResolvedValueOnce({ result: 'header auth success' })
renderTestApi({ customCollection: collectionWithHeader })
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith(
expect.objectContaining({
credentials: {
auth_type: AuthType.apiKeyHeader,
api_key_header: 'Authorization',
api_key_header_prefix: AuthHeaderPrefix.bearer,
api_key_value: 'test-token',
},
}),
)
})
})
it('should pass apiKeyQuery credentials to API', async () => {
const collectionWithQuery: CustomCollectionBackend = {
...customCollection,
credentials: {
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'api_key',
api_key_value: 'query-token',
},
}
testAPIAvailableMock.mockResolvedValueOnce({ result: 'query auth success' })
renderTestApi({ customCollection: collectionWithQuery })
fireEvent.click(screen.getByRole('button', { name: 'tools.test.title' }))
await waitFor(() => {
expect(testAPIAvailableMock).toHaveBeenCalledWith(
expect.objectContaining({
credentials: {
auth_type: AuthType.apiKeyQuery,
api_key_query_param: 'api_key',
api_key_value: 'query-token',
},
}),
)
})
expect(screen.getByText('ok')).toBeInTheDocument()
})
})
})