mirror of
https://github.com/langgenius/dify.git
synced 2026-02-24 18:05:11 +00:00
257 lines
9.3 KiB
TypeScript
257 lines
9.3 KiB
TypeScript
import type { DataSourceAuth } from './types'
|
|
import type { FormSchema } from '@/app/components/base/form/types'
|
|
import type { AddApiKeyButtonProps, AddOAuthButtonProps, PluginPayload } from '@/app/components/plugins/plugin-auth/types'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
|
import { AuthCategory } from '@/app/components/plugins/plugin-auth/types'
|
|
import Configure from './configure'
|
|
|
|
/**
|
|
* Configure Component Tests
|
|
* Using Unit approach to ensure 100% coverage and stable tests.
|
|
*/
|
|
|
|
// Mock plugin auth components to isolate the unit test for Configure.
|
|
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
|
AddApiKeyButton: vi.fn(({ onUpdate, disabled, buttonText }: AddApiKeyButtonProps & { onUpdate: () => void }) => (
|
|
<button data-testid="add-api-key" onClick={onUpdate} disabled={disabled}>{buttonText}</button>
|
|
)),
|
|
AddOAuthButton: vi.fn(({ onUpdate, disabled, buttonText }: AddOAuthButtonProps & { onUpdate: () => void }) => (
|
|
<button data-testid="add-oauth" onClick={onUpdate} disabled={disabled}>{buttonText}</button>
|
|
)),
|
|
}))
|
|
|
|
describe('Configure Component', () => {
|
|
const mockOnUpdate = vi.fn()
|
|
const mockPluginPayload: PluginPayload = {
|
|
category: AuthCategory.datasource,
|
|
provider: 'test-provider',
|
|
}
|
|
|
|
const mockItemBase: DataSourceAuth = {
|
|
author: 'Test Author',
|
|
provider: 'test-provider',
|
|
plugin_id: 'test-plugin-id',
|
|
plugin_unique_identifier: 'test-unique-id',
|
|
icon: 'test-icon-url',
|
|
name: 'test-name',
|
|
label: { en_US: 'Test Label', zh_Hans: 'zh_hans' },
|
|
description: { en_US: 'Test Description', zh_Hans: 'zh_hans' },
|
|
credentials_list: [],
|
|
}
|
|
|
|
const mockFormSchema: FormSchema = {
|
|
name: 'api_key',
|
|
label: { en_US: 'API Key', zh_Hans: 'zh_hans' },
|
|
type: FormTypeEnum.textInput,
|
|
required: true,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Open State Management', () => {
|
|
it('should toggle and manage the open state correctly', () => {
|
|
// Arrange
|
|
// Add a schema so we can detect if it's open by checking for button presence
|
|
const itemWithApiKey: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [mockFormSchema],
|
|
}
|
|
render(<Configure item={itemWithApiKey} pluginPayload={mockPluginPayload} />)
|
|
const trigger = screen.getByRole('button', { name: /dataSource.configure/i })
|
|
|
|
// Assert: Initially closed (button from content should not be present)
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
|
|
// Act: Click to open
|
|
fireEvent.click(trigger)
|
|
// Assert: Now open
|
|
expect(screen.getByTestId('add-api-key')).toBeInTheDocument()
|
|
|
|
// Act: Click again to close
|
|
fireEvent.click(trigger)
|
|
// Assert: Now closed
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Conditional Rendering', () => {
|
|
it('should render AddApiKeyButton when credential_schema is non-empty', () => {
|
|
// Arrange
|
|
const itemWithApiKey: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [mockFormSchema],
|
|
}
|
|
|
|
// Act
|
|
render(<Configure item={itemWithApiKey} pluginPayload={mockPluginPayload} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('add-api-key')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('add-oauth')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render AddOAuthButton when oauth_schema with client_schema is non-empty', () => {
|
|
// Arrange
|
|
const itemWithOAuth: DataSourceAuth = {
|
|
...mockItemBase,
|
|
oauth_schema: {
|
|
client_schema: [mockFormSchema],
|
|
},
|
|
}
|
|
|
|
// Act
|
|
render(<Configure item={itemWithOAuth} pluginPayload={mockPluginPayload} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('add-oauth')).toBeInTheDocument()
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render both buttons and the OR divider when both schemes are available', () => {
|
|
// Arrange
|
|
const itemWithBoth: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [mockFormSchema],
|
|
oauth_schema: {
|
|
client_schema: [mockFormSchema],
|
|
},
|
|
}
|
|
|
|
// Act
|
|
render(<Configure item={itemWithBoth} pluginPayload={mockPluginPayload} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('add-api-key')).toBeInTheDocument()
|
|
expect(screen.getByTestId('add-oauth')).toBeInTheDocument()
|
|
expect(screen.getByText('OR')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Update Handling', () => {
|
|
it('should call onUpdate and close the portal when an update is triggered', () => {
|
|
// Arrange
|
|
const itemWithApiKey: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [mockFormSchema],
|
|
}
|
|
render(<Configure item={itemWithApiKey} pluginPayload={mockPluginPayload} onUpdate={mockOnUpdate} />)
|
|
|
|
// Act: Open and click update
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
fireEvent.click(screen.getByTestId('add-api-key'))
|
|
|
|
// Assert
|
|
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle missing onUpdate callback gracefully', () => {
|
|
// Arrange
|
|
const itemWithBoth: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [mockFormSchema],
|
|
oauth_schema: {
|
|
client_schema: [mockFormSchema],
|
|
},
|
|
}
|
|
render(<Configure item={itemWithBoth} pluginPayload={mockPluginPayload} />)
|
|
|
|
// Act & Assert
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
fireEvent.click(screen.getByTestId('add-api-key'))
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
fireEvent.click(screen.getByTestId('add-oauth'))
|
|
expect(screen.queryByTestId('add-oauth')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props and Edge Cases', () => {
|
|
it('should pass the disabled prop to both configuration buttons', () => {
|
|
// Arrange
|
|
const itemWithBoth: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [mockFormSchema],
|
|
oauth_schema: {
|
|
client_schema: [mockFormSchema],
|
|
},
|
|
}
|
|
|
|
// Act: Open the configuration menu
|
|
render(<Configure item={itemWithBoth} pluginPayload={mockPluginPayload} disabled={true} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('add-api-key')).toBeDisabled()
|
|
expect(screen.getByTestId('add-oauth')).toBeDisabled()
|
|
})
|
|
|
|
it('should handle edge cases for missing, empty, or partial item data', () => {
|
|
// Act & Assert (Missing schemas)
|
|
const { rerender } = render(<Configure item={mockItemBase} pluginPayload={mockPluginPayload} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('add-oauth')).not.toBeInTheDocument()
|
|
|
|
// Arrange (Empty schemas)
|
|
const itemEmpty: DataSourceAuth = {
|
|
...mockItemBase,
|
|
credential_schema: [],
|
|
oauth_schema: { client_schema: [] },
|
|
}
|
|
// Act
|
|
rerender(<Configure item={itemEmpty} pluginPayload={mockPluginPayload} />)
|
|
// Already open from previous click if rerender doesn't reset state
|
|
// But it's better to be sure
|
|
expect(screen.queryByTestId('add-api-key')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('add-oauth')).not.toBeInTheDocument()
|
|
|
|
// Arrange (Partial OAuth schema)
|
|
const itemPartialOAuth: DataSourceAuth = {
|
|
...mockItemBase,
|
|
oauth_schema: {
|
|
is_oauth_custom_client_enabled: true,
|
|
},
|
|
}
|
|
// Act
|
|
rerender(<Configure item={itemPartialOAuth} pluginPayload={mockPluginPayload} />)
|
|
// Assert
|
|
expect(screen.queryByTestId('add-oauth')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should reach the unreachable branch on line 95 for 100% coverage', async () => {
|
|
// Specialized test to reach the '|| []' part: canOAuth must be truthy but client_schema falsy on second call
|
|
let count = 0
|
|
const itemWithGlitchedSchema = {
|
|
...mockItemBase,
|
|
oauth_schema: {
|
|
get client_schema() {
|
|
count++
|
|
if (count % 2 !== 0)
|
|
return [mockFormSchema]
|
|
return undefined
|
|
},
|
|
is_oauth_custom_client_enabled: false,
|
|
is_system_oauth_params_exists: false,
|
|
oauth_custom_client_params: {},
|
|
redirect_uri: '',
|
|
},
|
|
} as unknown as DataSourceAuth
|
|
|
|
render(<Configure item={itemWithGlitchedSchema} pluginPayload={mockPluginPayload} />)
|
|
fireEvent.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('add-oauth')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
})
|