mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 12:37:20 +08:00
test: tighten user-visible specs and raise coverage for key-validator… (#32281)
This commit is contained in:
parent
8141e3af99
commit
aad980f267
@ -0,0 +1,106 @@
|
|||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ValidatedStatus } from './declarations'
|
||||||
|
import KeyInput from './KeyInput'
|
||||||
|
|
||||||
|
type Props = ComponentProps<typeof KeyInput>
|
||||||
|
|
||||||
|
const createProps = (overrides: Partial<Props> = {}): Props => ({
|
||||||
|
name: 'API key',
|
||||||
|
placeholder: 'Enter API key',
|
||||||
|
value: 'initial-value',
|
||||||
|
onChange: vi.fn(),
|
||||||
|
onFocus: undefined,
|
||||||
|
validating: false,
|
||||||
|
validatedStatusState: {},
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('KeyInput', () => {
|
||||||
|
it('shows the label and placeholder value', () => {
|
||||||
|
const props = createProps()
|
||||||
|
render(<KeyInput {...props} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('API key')).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText('Enter API key')).toHaveValue('initial-value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates the visible input value when user types', () => {
|
||||||
|
const ControlledKeyInput = () => {
|
||||||
|
const [value, setValue] = useState('initial-value')
|
||||||
|
return (
|
||||||
|
<KeyInput
|
||||||
|
{...createProps({
|
||||||
|
value,
|
||||||
|
onChange: setValue,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(<ControlledKeyInput />)
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Enter API key'), { target: { value: 'updated' } })
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText('Enter API key')).toHaveValue('updated')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cycles through validating and error messaging', () => {
|
||||||
|
const props = createProps()
|
||||||
|
const { rerender } = render(
|
||||||
|
<KeyInput {...props} validating validatedStatusState={{}} />,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.validating')).toBeInTheDocument()
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<KeyInput
|
||||||
|
{...props}
|
||||||
|
validating={false}
|
||||||
|
validatedStatusState={{ status: ValidatedStatus.Error, message: 'bad-request' }}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.validatedErrorbad-request')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show an error tip for exceed status', () => {
|
||||||
|
render(
|
||||||
|
<KeyInput
|
||||||
|
{...createProps({
|
||||||
|
validating: false,
|
||||||
|
validatedStatusState: { status: ValidatedStatus.Exceed, message: 'quota' },
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText(/common\.provider\.validatedError/i)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show validating or error text for success status', () => {
|
||||||
|
render(
|
||||||
|
<KeyInput
|
||||||
|
{...createProps({
|
||||||
|
validating: false,
|
||||||
|
validatedStatusState: { status: ValidatedStatus.Success },
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.provider.validating')).toBeNull()
|
||||||
|
expect(screen.queryByText(/common\.provider\.validatedError/i)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows fallback error text when error message is missing', () => {
|
||||||
|
render(
|
||||||
|
<KeyInput
|
||||||
|
{...createProps({
|
||||||
|
validating: false,
|
||||||
|
validatedStatusState: { status: ValidatedStatus.Error },
|
||||||
|
})}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.validatedError')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import Operate from './Operate'
|
||||||
|
|
||||||
|
describe('Operate', () => {
|
||||||
|
it('renders cancel and save when editing', () => {
|
||||||
|
render(
|
||||||
|
<Operate
|
||||||
|
isOpen
|
||||||
|
status="add"
|
||||||
|
onAdd={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onSave={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('common.operation.save')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows add key prompt when closed', () => {
|
||||||
|
render(
|
||||||
|
<Operate
|
||||||
|
isOpen={false}
|
||||||
|
status="add"
|
||||||
|
onAdd={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onSave={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.addKey')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows invalid state indicator and edit prompt when status is fail', () => {
|
||||||
|
render(
|
||||||
|
<Operate
|
||||||
|
isOpen={false}
|
||||||
|
status="fail"
|
||||||
|
onAdd={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onSave={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.invalidApiKey')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('common.provider.editKey')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows edit prompt without error text when status is success', () => {
|
||||||
|
render(
|
||||||
|
<Operate
|
||||||
|
isOpen={false}
|
||||||
|
status="success"
|
||||||
|
onAdd={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onSave={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.editKey')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('common.provider.invalidApiKey')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows no actions for unsupported status', () => {
|
||||||
|
render(
|
||||||
|
<Operate
|
||||||
|
isOpen={false}
|
||||||
|
status={'unknown' as never}
|
||||||
|
onAdd={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
onEdit={vi.fn()}
|
||||||
|
onSave={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.provider.addKey')).toBeNull()
|
||||||
|
expect(screen.queryByText('common.provider.editKey')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import {
|
||||||
|
ValidatedErrorIcon,
|
||||||
|
ValidatedErrorMessage,
|
||||||
|
ValidatedSuccessIcon,
|
||||||
|
ValidatingTip,
|
||||||
|
} from './ValidateStatus'
|
||||||
|
|
||||||
|
describe('ValidateStatus', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show validating text while validation is running', () => {
|
||||||
|
render(<ValidatingTip />)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.validating')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show translated error text with the backend message', () => {
|
||||||
|
render(<ValidatedErrorMessage errorMessage="invalid-token" />)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.provider.validatedErrorinvalid-token')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render decorative icon for success and error states', () => {
|
||||||
|
const { container, rerender } = render(<ValidatedSuccessIcon />)
|
||||||
|
|
||||||
|
expect(container.firstElementChild).toBeTruthy()
|
||||||
|
|
||||||
|
rerender(<ValidatedErrorIcon />)
|
||||||
|
|
||||||
|
expect(container.firstElementChild).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { ValidatedStatus } from './declarations'
|
||||||
|
|
||||||
|
describe('declarations', () => {
|
||||||
|
describe('ValidatedStatus', () => {
|
||||||
|
it('should expose expected status values', () => {
|
||||||
|
expect(ValidatedStatus.Success).toBe('success')
|
||||||
|
expect(ValidatedStatus.Error).toBe('error')
|
||||||
|
expect(ValidatedStatus.Exceed).toBe('exceed')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import { ValidatedStatus } from './declarations'
|
||||||
|
import { useValidate } from './hooks'
|
||||||
|
|
||||||
|
describe('useValidate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear validation state when before returns false', async () => {
|
||||||
|
const { result } = renderHook(() => useValidate({ apiKey: 'value' }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[0]({ before: () => false })
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current[1]).toBe(false)
|
||||||
|
expect(result.current[2]).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should expose success status after a successful validation', async () => {
|
||||||
|
const run = vi.fn().mockResolvedValue({ status: ValidatedStatus.Success })
|
||||||
|
const { result } = renderHook(() => useValidate({ apiKey: 'value' }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[0]({
|
||||||
|
before: () => true,
|
||||||
|
run,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current[1]).toBe(false)
|
||||||
|
expect(result.current[2]).toEqual({ status: ValidatedStatus.Success })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should expose error status and message when validation fails', async () => {
|
||||||
|
const run = vi.fn().mockResolvedValue({ status: ValidatedStatus.Error, message: 'bad-key' })
|
||||||
|
const { result } = renderHook(() => useValidate({ apiKey: 'value' }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[0]({
|
||||||
|
before: () => true,
|
||||||
|
run,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current[1]).toBe(false)
|
||||||
|
expect(result.current[2]).toEqual({ status: ValidatedStatus.Error, message: 'bad-key' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep validating state true when run is not provided', async () => {
|
||||||
|
const { result } = renderHook(() => useValidate({ apiKey: 'value' }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[0]({ before: () => true })
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current[1]).toBe(true)
|
||||||
|
expect(result.current[2]).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
import type { ComponentProps } from 'react'
|
||||||
|
import type { Form } from './declarations'
|
||||||
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import KeyValidator from './index'
|
||||||
|
|
||||||
|
let subscriptionCallback: ((value: string) => void) | null = null
|
||||||
|
const mockEmit = vi.fn((value: string) => {
|
||||||
|
subscriptionCallback?.(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('@/context/event-emitter', () => ({
|
||||||
|
useEventEmitterContextContext: () => ({
|
||||||
|
eventEmitter: {
|
||||||
|
emit: mockEmit,
|
||||||
|
useSubscription: (cb: (value: string) => void) => {
|
||||||
|
subscriptionCallback = cb
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockValidate = vi.fn()
|
||||||
|
const mockUseValidate = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('./hooks', () => ({
|
||||||
|
useValidate: (...args: unknown[]) => mockUseValidate(...args),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('KeyValidator', () => {
|
||||||
|
const formValidate = {
|
||||||
|
before: () => true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const forms: Form[] = [
|
||||||
|
{
|
||||||
|
key: 'apiKey',
|
||||||
|
title: 'API key',
|
||||||
|
placeholder: 'Enter API key',
|
||||||
|
value: 'initial-key',
|
||||||
|
validate: formValidate,
|
||||||
|
handleFocus: (_value, setValue) => {
|
||||||
|
setValue(prev => ({ ...prev, apiKey: 'focused-key' }))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const createProps = (overrides: Partial<ComponentProps<typeof KeyValidator>> = {}) => ({
|
||||||
|
type: 'test-provider',
|
||||||
|
title: <div>Provider key</div>,
|
||||||
|
status: 'add' as const,
|
||||||
|
forms,
|
||||||
|
keyFrom: {
|
||||||
|
text: 'Get key',
|
||||||
|
link: 'https://example.com/key',
|
||||||
|
},
|
||||||
|
onSave: vi.fn().mockResolvedValue(true),
|
||||||
|
disabled: false,
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
subscriptionCallback = null
|
||||||
|
mockValidate.mockImplementation((config?: { before?: () => boolean }) => config?.before?.())
|
||||||
|
mockUseValidate.mockReturnValue([mockValidate, false, {}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open and close the editor from add and cancel actions', () => {
|
||||||
|
render(<KeyValidator {...createProps()} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText('Enter API key')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('link', { name: 'Get key' })).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||||
|
|
||||||
|
expect(screen.queryByPlaceholderText('Enter API key')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should submit the updated value when save is clicked', async () => {
|
||||||
|
render(<KeyValidator {...createProps()} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||||
|
const input = screen.getByPlaceholderText('Enter API key')
|
||||||
|
|
||||||
|
fireEvent.focus(input)
|
||||||
|
expect(input).toHaveValue('focused-key')
|
||||||
|
|
||||||
|
fireEvent.change(input, {
|
||||||
|
target: { value: 'updated-key' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('common.operation.save'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByPlaceholderText('Enter API key')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep the editor open when save does not succeed', async () => {
|
||||||
|
const formsWithoutValidation: Form[] = [
|
||||||
|
{
|
||||||
|
key: 'apiKey',
|
||||||
|
title: 'API key',
|
||||||
|
placeholder: 'Enter API key',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const props = createProps({
|
||||||
|
forms: formsWithoutValidation,
|
||||||
|
onSave: vi.fn().mockResolvedValue(false),
|
||||||
|
})
|
||||||
|
render(<KeyValidator {...props} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||||
|
const input = screen.getByPlaceholderText('Enter API key')
|
||||||
|
|
||||||
|
expect(input).toHaveValue('')
|
||||||
|
|
||||||
|
fireEvent.focus(input)
|
||||||
|
fireEvent.change(input, {
|
||||||
|
target: { value: 'typed-without-validator' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('common.operation.save'))
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText('Enter API key')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should close and reset edited values when another validator emits a trigger', () => {
|
||||||
|
render(<KeyValidator {...createProps()} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Enter API key'), {
|
||||||
|
target: { value: 'changed' },
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
subscriptionCallback?.('plugins/another-provider')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByPlaceholderText('Enter API key')).toBeNull()
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText('Enter API key')).toHaveValue('initial-key')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent opening key editor when disabled', () => {
|
||||||
|
render(<KeyValidator {...createProps()} disabled />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.addKey'))
|
||||||
|
|
||||||
|
expect(screen.queryByPlaceholderText('Enter API key')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open the editor from edit action when validator is in success state', () => {
|
||||||
|
render(<KeyValidator {...createProps({ status: 'success' })} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('common.provider.editKey'))
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText('Enter API key')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
import type { UserProfileResponse } from '@/models/common'
|
||||||
|
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||||
|
import { ToastProvider } from '@/app/components/base/toast'
|
||||||
|
import { languages } from '@/i18n-config/language'
|
||||||
|
import { updateUserProfile } from '@/service/common'
|
||||||
|
import { timezones } from '@/utils/timezone'
|
||||||
|
import LanguagePage from './index'
|
||||||
|
|
||||||
|
const mockRefresh = vi.fn()
|
||||||
|
const mockMutateUserProfile = vi.fn()
|
||||||
|
let mockLocale: string | undefined = 'en-US'
|
||||||
|
let mockUserProfile: UserProfileResponse
|
||||||
|
|
||||||
|
vi.mock('@/app/components/base/select', async () => {
|
||||||
|
const React = await import('react')
|
||||||
|
|
||||||
|
return {
|
||||||
|
SimpleSelect: ({
|
||||||
|
items = [],
|
||||||
|
defaultValue,
|
||||||
|
onSelect,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
items?: Array<{ value: string | number, name: string }>
|
||||||
|
defaultValue?: string | number
|
||||||
|
onSelect: (item: { value: string | number, name: string }) => void
|
||||||
|
disabled?: boolean
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
|
const [selectedValue, setSelectedValue] = React.useState<string | number | undefined>(defaultValue)
|
||||||
|
const selected = items.find(item => item.value === selectedValue)
|
||||||
|
?? items.find(item => item.value === defaultValue)
|
||||||
|
?? null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button type="button" disabled={disabled} onClick={() => setOpen(prev => !prev)}>
|
||||||
|
{selected?.name ?? ''}
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div>
|
||||||
|
{items.map(item => (
|
||||||
|
<button
|
||||||
|
key={item.value}
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedValue(item.value)
|
||||||
|
onSelect(item)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({ refresh: mockRefresh }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context', () => ({
|
||||||
|
useAppContext: () => ({
|
||||||
|
userProfile: mockUserProfile,
|
||||||
|
mutateUserProfile: mockMutateUserProfile,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/i18n', () => ({
|
||||||
|
useLocale: () => mockLocale,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/service/common', () => ({
|
||||||
|
updateUserProfile: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/i18n-config', () => ({
|
||||||
|
setLocaleOnClient: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const updateUserProfileMock = vi.mocked(updateUserProfile)
|
||||||
|
|
||||||
|
const createUserProfile = (overrides: Partial<UserProfileResponse> = {}): UserProfileResponse => ({
|
||||||
|
id: 'user-id',
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
avatar: '',
|
||||||
|
avatar_url: null,
|
||||||
|
is_password_set: false,
|
||||||
|
interface_language: 'en-US',
|
||||||
|
timezone: 'Pacific/Niue',
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderPage = () => {
|
||||||
|
render(
|
||||||
|
<ToastProvider>
|
||||||
|
<LanguagePage />
|
||||||
|
</ToastProvider>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSectionByLabel = (sectionLabel: string) => {
|
||||||
|
const label = screen.getByText(sectionLabel)
|
||||||
|
const section = label.closest('div')?.parentElement
|
||||||
|
if (!section)
|
||||||
|
throw new Error(`Missing select section: ${sectionLabel}`)
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOption = async (sectionLabel: string, optionName: string) => {
|
||||||
|
const section = getSectionByLabel(sectionLabel)
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(within(section).getByRole('button'))
|
||||||
|
})
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(await within(section).findByRole('option', { name: optionName }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLanguageOption = (value: string) => {
|
||||||
|
const option = languages.find(item => item.value === value)
|
||||||
|
if (!option)
|
||||||
|
throw new Error(`Missing language option: ${value}`)
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimezoneOption = (value: string) => {
|
||||||
|
const option = timezones.find(item => item.value === value)
|
||||||
|
if (!option)
|
||||||
|
throw new Error(`Missing timezone option: ${value}`)
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockLocale = 'en-US'
|
||||||
|
mockUserProfile = createUserProfile()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
describe('LanguagePage - Rendering', () => {
|
||||||
|
it('should render default language and timezone labels', () => {
|
||||||
|
const english = getLanguageOption('en-US')
|
||||||
|
const niueTimezone = getTimezoneOption('Pacific/Niue')
|
||||||
|
mockLocale = undefined
|
||||||
|
mockUserProfile = createUserProfile({
|
||||||
|
interface_language: english.value.toString(),
|
||||||
|
timezone: niueTimezone.value.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
expect(screen.getByText('common.language.displayLanguage')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('common.language.timezone')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: english.name })).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: niueTimezone.name })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Interactions
|
||||||
|
describe('LanguagePage - Interactions', () => {
|
||||||
|
it('should show success toast when language updates', async () => {
|
||||||
|
const chinese = getLanguageOption('zh-Hans')
|
||||||
|
mockUserProfile = createUserProfile({ interface_language: 'en-US' })
|
||||||
|
updateUserProfileMock.mockResolvedValueOnce({ result: 'success' })
|
||||||
|
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
await selectOption('common.language.displayLanguage', chinese.name)
|
||||||
|
|
||||||
|
expect(await screen.findByText('common.actionMsg.modifiedSuccessfully')).toBeInTheDocument()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateUserProfileMock).toHaveBeenCalledWith({
|
||||||
|
url: '/account/interface-language',
|
||||||
|
body: { interface_language: chinese.value },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error toast when language update fails', async () => {
|
||||||
|
const chinese = getLanguageOption('zh-Hans')
|
||||||
|
updateUserProfileMock.mockRejectedValueOnce(new Error('Update failed'))
|
||||||
|
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
await selectOption('common.language.displayLanguage', chinese.name)
|
||||||
|
|
||||||
|
expect(await screen.findByText('Update failed')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show success toast when timezone updates', async () => {
|
||||||
|
const midwayTimezone = getTimezoneOption('Pacific/Midway')
|
||||||
|
updateUserProfileMock.mockResolvedValueOnce({ result: 'success' })
|
||||||
|
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
await selectOption('common.language.timezone', midwayTimezone.name)
|
||||||
|
|
||||||
|
expect(await screen.findByText('common.actionMsg.modifiedSuccessfully')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: midwayTimezone.name })).toBeInTheDocument()
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
it('should show error toast when timezone update fails', async () => {
|
||||||
|
const midwayTimezone = getTimezoneOption('Pacific/Midway')
|
||||||
|
updateUserProfileMock.mockRejectedValueOnce(new Error('Timezone failed'))
|
||||||
|
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
await selectOption('common.language.timezone', midwayTimezone.name)
|
||||||
|
|
||||||
|
expect(await screen.findByText('Timezone failed')).toBeInTheDocument()
|
||||||
|
}, 15000)
|
||||||
|
})
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
import type { PluginProvider } from '@/models/common'
|
||||||
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
|
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', () => ({
|
||||||
|
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,118 @@
|
|||||||
|
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', () => ({
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user