mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
refactor(custom): reorganize web app brand module and raise coverage threshold (#33531)
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,210 @@
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import SerpapiPlugin from '../SerpapiPlugin'
|
||||
import { updatePluginKey, validatePluginKey } from '../utils'
|
||||
|
||||
const mockEventEmitter = vi.hoisted(() => {
|
||||
let subscriber: ((value: string) => void) | undefined
|
||||
return {
|
||||
useSubscription: vi.fn((callback: (value: string) => void) => {
|
||||
subscriber = callback
|
||||
}),
|
||||
emit: vi.fn((value: string) => {
|
||||
subscriber?.(value)
|
||||
}),
|
||||
reset: () => {
|
||||
subscriber = undefined
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/toast/context')>()
|
||||
return {
|
||||
...actual,
|
||||
useToastContext: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../utils', () => ({
|
||||
updatePluginKey: vi.fn(),
|
||||
validatePluginKey: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: vi.fn(() => ({
|
||||
eventEmitter: mockEventEmitter,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('SerpapiPlugin', () => {
|
||||
const mockOnUpdate = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockUpdatePluginKey = updatePluginKey as ReturnType<typeof vi.fn>
|
||||
const mockValidatePluginKey = validatePluginKey as ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEventEmitter.reset()
|
||||
const mockUseAppContext = useAppContext as ReturnType<typeof vi.fn>
|
||||
const mockUseToastContext = useToastContext as ReturnType<typeof vi.fn>
|
||||
mockUseAppContext.mockReturnValue({
|
||||
isCurrentWorkspaceManager: true,
|
||||
})
|
||||
mockUseToastContext.mockReturnValue({
|
||||
notify: mockNotify,
|
||||
})
|
||||
mockValidatePluginKey.mockResolvedValue({ status: 'success' })
|
||||
mockUpdatePluginKey.mockResolvedValue({ status: 'success' })
|
||||
})
|
||||
|
||||
it('should show key input when manager clicks edit key', () => {
|
||||
const mockPlugin: PluginProvider = {
|
||||
tool_name: 'serpapi',
|
||||
credentials: {
|
||||
api_key: 'existing-key',
|
||||
},
|
||||
} as PluginProvider
|
||||
|
||||
render(<SerpapiPlugin plugin={mockPlugin} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||
expect(screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should clear existing key on focus and show validation error for invalid key', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
mockValidatePluginKey.mockResolvedValue({ status: 'error', message: 'Invalid API key' })
|
||||
|
||||
const mockPlugin: PluginProvider = {
|
||||
tool_name: 'serpapi',
|
||||
credentials: {
|
||||
api_key: 'existing-key',
|
||||
},
|
||||
} as PluginProvider
|
||||
|
||||
render(<SerpapiPlugin plugin={mockPlugin} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||
const input = screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder')
|
||||
|
||||
expect(input).toHaveValue('existing-key')
|
||||
fireEvent.focus(input)
|
||||
expect(input).toHaveValue('')
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'invalid-key' },
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
})
|
||||
|
||||
expect(screen.getByText(/Invalid API key/)).toBeInTheDocument()
|
||||
|
||||
fireEvent.focus(input)
|
||||
expect(input).toHaveValue('invalid-key')
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '' },
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000)
|
||||
})
|
||||
|
||||
expect(screen.queryByText(/Invalid API key/)).toBeNull()
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('should not open key input when user is not workspace manager', () => {
|
||||
const mockUseAppContext = useAppContext as ReturnType<typeof vi.fn>
|
||||
mockUseAppContext.mockReturnValue({
|
||||
isCurrentWorkspaceManager: false,
|
||||
})
|
||||
|
||||
const mockPlugin = {
|
||||
tool_name: 'serpapi',
|
||||
is_enabled: true,
|
||||
credentials: null,
|
||||
} satisfies PluginProvider
|
||||
|
||||
render(<SerpapiPlugin plugin={mockPlugin} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||
|
||||
expect(screen.queryByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder')).toBeNull()
|
||||
})
|
||||
|
||||
it('should save changed key and trigger success feedback', async () => {
|
||||
const mockPlugin: PluginProvider = {
|
||||
tool_name: 'serpapi',
|
||||
credentials: {
|
||||
api_key: 'existing-key',
|
||||
},
|
||||
} as PluginProvider
|
||||
|
||||
render(<SerpapiPlugin plugin={mockPlugin} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||
fireEvent.change(screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder'), {
|
||||
target: { value: 'new-key' },
|
||||
})
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep editor open when save request fails', async () => {
|
||||
mockUpdatePluginKey.mockResolvedValue({ status: 'error', message: 'update failed' })
|
||||
|
||||
const mockPlugin: PluginProvider = {
|
||||
tool_name: 'serpapi',
|
||||
credentials: {
|
||||
api_key: 'existing-key',
|
||||
},
|
||||
} as PluginProvider
|
||||
|
||||
render(<SerpapiPlugin plugin={mockPlugin} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||
fireEvent.change(screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder'), {
|
||||
target: { value: 'new-key' },
|
||||
})
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep editor open when key value is unchanged', async () => {
|
||||
const mockPlugin: PluginProvider = {
|
||||
tool_name: 'serpapi',
|
||||
credentials: {
|
||||
api_key: 'existing-key',
|
||||
},
|
||||
} as PluginProvider
|
||||
|
||||
render(<SerpapiPlugin plugin={mockPlugin} onUpdate={mockOnUpdate} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,122 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useState } from 'react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import PluginPage from '../index'
|
||||
import { updatePluginKey, validatePluginKey } from '../utils'
|
||||
|
||||
const mockUsePluginProviders = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
usePluginProviders: mockUsePluginProviders,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast/context', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/toast/context')>()
|
||||
return {
|
||||
...actual,
|
||||
useToastContext: () => ({
|
||||
notify: vi.fn(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
emit: vi.fn(),
|
||||
useSubscription: vi.fn(),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../utils', () => ({
|
||||
updatePluginKey: vi.fn(),
|
||||
validatePluginKey: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('PluginPage', () => {
|
||||
const mockUpdatePluginKey = updatePluginKey as ReturnType<typeof vi.fn>
|
||||
const mockValidatePluginKey = validatePluginKey as ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const mockUseAppContext = useAppContext as ReturnType<typeof vi.fn>
|
||||
mockUseAppContext.mockReturnValue({
|
||||
isCurrentWorkspaceManager: true,
|
||||
})
|
||||
mockValidatePluginKey.mockResolvedValue({ status: 'success' })
|
||||
mockUpdatePluginKey.mockResolvedValue({ status: 'success' })
|
||||
})
|
||||
|
||||
it('should render plugin settings with edit action when serpapi key exists', () => {
|
||||
mockUsePluginProviders.mockReturnValue({
|
||||
data: [
|
||||
{ tool_name: 'serpapi', credentials: { api_key: 'test-key' } },
|
||||
],
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
render(<PluginPage />)
|
||||
expect(screen.getByText('common.provider.editKey')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin settings with add action when serpapi key is missing', () => {
|
||||
mockUsePluginProviders.mockReturnValue({
|
||||
data: [
|
||||
{ tool_name: 'serpapi', credentials: null },
|
||||
],
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
render(<PluginPage />)
|
||||
expect(screen.getByText('common.provider.addKey')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display encryption notice with PKCS1_OAEP link', () => {
|
||||
mockUsePluginProviders.mockReturnValue({
|
||||
data: [],
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
render(<PluginPage />)
|
||||
expect(screen.getByText(/common\.provider\.encrypted\.front/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/common\.provider\.encrypted\.back/)).toBeInTheDocument()
|
||||
const link = screen.getByRole('link', { name: 'PKCS1_OAEP' })
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('href', 'https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html')
|
||||
})
|
||||
|
||||
it('should show reload state after saving key', async () => {
|
||||
let showReloadedState = () => {}
|
||||
const Wrapper = () => {
|
||||
const [reloaded, setReloaded] = useState(false)
|
||||
showReloadedState = () => setReloaded(true)
|
||||
return (
|
||||
<>
|
||||
<PluginPage />
|
||||
{reloaded && <div>providers-reloaded</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
mockUsePluginProviders.mockImplementation(() => ({
|
||||
data: [{ tool_name: 'serpapi', credentials: { api_key: 'existing-key' } }],
|
||||
refetch: () => showReloadedState(),
|
||||
}))
|
||||
|
||||
render(<Wrapper />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||
fireEvent.change(screen.getByPlaceholderText('common.plugin.serpapi.apiKeyPlaceholder'), {
|
||||
target: { value: 'new-key' },
|
||||
})
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('providers-reloaded')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,73 @@
|
||||
import { updatePluginProviderAIKey, validatePluginProviderKey } from '@/service/common'
|
||||
import { ValidatedStatus } from '../../key-validator/declarations'
|
||||
import { updatePluginKey, validatePluginKey } from '../utils'
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
validatePluginProviderKey: vi.fn(),
|
||||
updatePluginProviderAIKey: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockValidatePluginProviderKey = validatePluginProviderKey as ReturnType<typeof vi.fn>
|
||||
const mockUpdatePluginProviderAIKey = updatePluginProviderAIKey as ReturnType<typeof vi.fn>
|
||||
|
||||
describe('Plugin Utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe.each([
|
||||
{
|
||||
name: 'validatePluginKey',
|
||||
utilFn: validatePluginKey,
|
||||
serviceMock: mockValidatePluginProviderKey,
|
||||
successBody: { credentials: { api_key: 'test-key' } },
|
||||
failureBody: { credentials: { api_key: 'invalid' } },
|
||||
exceptionBody: { credentials: { api_key: 'test' } },
|
||||
serviceErrorMessage: 'Invalid API key',
|
||||
thrownErrorMessage: 'Network error',
|
||||
},
|
||||
{
|
||||
name: 'updatePluginKey',
|
||||
utilFn: updatePluginKey,
|
||||
serviceMock: mockUpdatePluginProviderAIKey,
|
||||
successBody: { credentials: { api_key: 'new-key' } },
|
||||
failureBody: { credentials: { api_key: 'test' } },
|
||||
exceptionBody: { credentials: { api_key: 'test' } },
|
||||
serviceErrorMessage: 'Update failed',
|
||||
thrownErrorMessage: 'Request failed',
|
||||
},
|
||||
])('$name', ({ utilFn, serviceMock, successBody, failureBody, exceptionBody, serviceErrorMessage, thrownErrorMessage }) => {
|
||||
it('should return success status when service succeeds', async () => {
|
||||
serviceMock.mockResolvedValue({ result: 'success' })
|
||||
|
||||
const result = await utilFn('serpapi', successBody)
|
||||
|
||||
expect(result.status).toBe(ValidatedStatus.Success)
|
||||
})
|
||||
|
||||
it('should return error status with message when service returns an error', async () => {
|
||||
serviceMock.mockResolvedValue({
|
||||
result: 'error',
|
||||
error: serviceErrorMessage,
|
||||
})
|
||||
|
||||
const result = await utilFn('serpapi', failureBody)
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: ValidatedStatus.Error,
|
||||
message: serviceErrorMessage,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error status when service throws exception', async () => {
|
||||
serviceMock.mockRejectedValue(new Error(thrownErrorMessage))
|
||||
|
||||
const result = await utilFn('serpapi', exceptionBody)
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: ValidatedStatus.Error,
|
||||
message: thrownErrorMessage,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user