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:
Coding On Star
2026-03-16 18:17:21 +08:00
committed by GitHub
parent c3ee83645f
commit 6da802eb2a
140 changed files with 1782 additions and 1486 deletions

View File

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

View File

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

View File

@ -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,
})
})
})
})