mirror of
https://github.com/langgenius/dify.git
synced 2026-02-24 18:05:11 +00:00
test(web): Added test for model-auth files in header folder (#32358)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import type { CustomModel, ModelCredential, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { ConfigurationMethodEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import AddCredentialInLoadBalancing from './add-credential-in-load-balancing'
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
|
||||
Authorized: ({
|
||||
renderTrigger,
|
||||
authParams,
|
||||
items,
|
||||
onItemClick,
|
||||
}: {
|
||||
renderTrigger: (open?: boolean) => React.ReactNode
|
||||
authParams?: { onUpdate?: (payload?: unknown, formValues?: Record<string, unknown>) => void }
|
||||
items: Array<{ credentials: Array<{ credential_id: string, credential_name: string }> }>
|
||||
onItemClick?: (credential: { credential_id: string, credential_name: string }) => void
|
||||
}) => (
|
||||
<div>
|
||||
{renderTrigger(false)}
|
||||
<button onClick={() => authParams?.onUpdate?.({ provider: 'x' }, { key: 'value' })}>Run update</button>
|
||||
<button onClick={() => onItemClick?.(items[0].credentials[0])}>Select first</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('AddCredentialInLoadBalancing', () => {
|
||||
const provider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
} as ModelProvider
|
||||
|
||||
const model = {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
} as CustomModel
|
||||
|
||||
const modelCredential = {
|
||||
available_credentials: [
|
||||
{ credential_id: 'cred-1', credential_name: 'Key 1' },
|
||||
],
|
||||
credentials: {},
|
||||
load_balancing: { enabled: false, configs: [] },
|
||||
} as ModelCredential
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render add credential label', () => {
|
||||
render(
|
||||
<AddCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
model={model}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
modelCredential={modelCredential}
|
||||
onSelectCredential={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.addCredential/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward update payload when update action happens', () => {
|
||||
const onUpdate = vi.fn()
|
||||
|
||||
render(
|
||||
<AddCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
model={model}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
modelCredential={modelCredential}
|
||||
onSelectCredential={vi.fn()}
|
||||
onUpdate={onUpdate}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Run update' }))
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith({ provider: 'x' }, { key: 'value' })
|
||||
})
|
||||
|
||||
it('should call onSelectCredential when user picks a credential', () => {
|
||||
const onSelectCredential = vi.fn()
|
||||
|
||||
render(
|
||||
<AddCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
model={model}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
modelCredential={modelCredential}
|
||||
onSelectCredential={onSelectCredential}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Select first' }))
|
||||
|
||||
expect(onSelectCredential).toHaveBeenCalledWith(modelCredential.available_credentials[0])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,165 @@
|
||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import AddCustomModel from './add-custom-model'
|
||||
|
||||
// Mock hooks
|
||||
const mockHandleOpenModalForAddNewCustomModel = vi.fn()
|
||||
const mockHandleOpenModalForAddCustomModelToModelList = vi.fn()
|
||||
|
||||
vi.mock('./hooks/use-auth', () => ({
|
||||
useAuth: (_provider: unknown, _configMethod: unknown, _fixedFields: unknown, options: { mode: string }) => {
|
||||
if (options.mode === 'config-custom-model') {
|
||||
return { handleOpenModal: mockHandleOpenModalForAddNewCustomModel }
|
||||
}
|
||||
if (options.mode === 'add-custom-model-to-model-list') {
|
||||
return { handleOpenModal: mockHandleOpenModalForAddCustomModelToModelList }
|
||||
}
|
||||
return { handleOpenModal: vi.fn() }
|
||||
},
|
||||
}))
|
||||
|
||||
let mockCanAddedModels: { model: string, model_type: string }[] = []
|
||||
vi.mock('./hooks/use-custom-models', () => ({
|
||||
useCanAddedModels: () => mockCanAddedModels,
|
||||
}))
|
||||
|
||||
// Mock components
|
||||
vi.mock('../model-icon', () => ({
|
||||
default: () => <div data-testid="model-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiAddCircleFill: () => <div data-testid="add-circle-icon" />,
|
||||
RiAddLine: () => <div data-testid="add-line-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip-mock">
|
||||
{children}
|
||||
<div>{popupContent}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock portal components to avoid async/jsdom issues (consistent with sibling tests)
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean, onOpenChange: (open: boolean) => void }) => (
|
||||
<div data-testid="portal" data-open={open}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick}>{children}</div>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children }: { children: React.ReactNode, open?: boolean }) => {
|
||||
// In many tests, we need to find elements inside the content even if "closed" in state
|
||||
// but not yet "removed" from DOM. However, to avoid multiple elements issues,
|
||||
// we should be careful.
|
||||
// For AddCustomModel, we need the content to be present when we click a model.
|
||||
return <div data-testid="portal-content" style={{ display: 'block' }}>{children}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
describe('AddCustomModel', () => {
|
||||
const mockProvider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
} as unknown as ModelProvider
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCanAddedModels = []
|
||||
})
|
||||
|
||||
it('should render the add model button', () => {
|
||||
render(
|
||||
<AddCustomModel
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/modelProvider.addModel/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('add-circle-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call handleOpenModal directly when no models available and allowed', () => {
|
||||
mockCanAddedModels = []
|
||||
render(
|
||||
<AddCustomModel
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show models list when models are available', () => {
|
||||
mockCanAddedModels = [{ model: 'gpt-4', model_type: 'llm' }]
|
||||
render(
|
||||
<AddCustomModel
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
|
||||
// The portal should be "open"
|
||||
expect(screen.getByTestId('portal')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getByText('gpt-4')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call handleOpenModalForAddCustomModelToModelList when clicking a model', () => {
|
||||
const model = { model: 'gpt-4', model_type: 'llm' }
|
||||
mockCanAddedModels = [model]
|
||||
render(
|
||||
<AddCustomModel
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByText('gpt-4'))
|
||||
|
||||
expect(mockHandleOpenModalForAddCustomModelToModelList).toHaveBeenCalledWith(undefined, model)
|
||||
})
|
||||
|
||||
it('should call handleOpenModalForAddNewCustomModel when clicking "Add New Model" in list', () => {
|
||||
mockCanAddedModels = [{ model: 'gpt-4', model_type: 'llm' }]
|
||||
render(
|
||||
<AddCustomModel
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByText(/modelProvider.auth.addNewModel/))
|
||||
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show tooltip when no models and custom tokens not allowed', () => {
|
||||
const restrictedProvider = { ...mockProvider, allow_custom_token: false }
|
||||
mockCanAddedModels = []
|
||||
render(
|
||||
<AddCustomModel
|
||||
provider={restrictedProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip-mock')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.auth.credentialUnavailable')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
expect(mockHandleOpenModalForAddNewCustomModel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { Credential, CustomModelCredential, ModelProvider } from '../../declarations'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { ModelTypeEnum } from '../../declarations'
|
||||
import { AuthorizedItem } from './authorized-item'
|
||||
|
||||
vi.mock('../../model-icon', () => ({
|
||||
default: ({ modelName }: { modelName: string }) => <div data-testid="model-icon">{modelName}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./credential-item', () => ({
|
||||
default: ({ credential, onEdit, onDelete, onItemClick }: {
|
||||
credential: Credential
|
||||
onEdit?: (credential: Credential) => void
|
||||
onDelete?: (credential: Credential) => void
|
||||
onItemClick?: (credential: Credential) => void
|
||||
}) => (
|
||||
<div data-testid={`credential-item-${credential.credential_id}`}>
|
||||
{credential.credential_name}
|
||||
<button onClick={() => onEdit?.(credential)}>Edit</button>
|
||||
<button onClick={() => onDelete?.(credential)}>Delete</button>
|
||||
<button onClick={() => onItemClick?.(credential)}>Click</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('AuthorizedItem', () => {
|
||||
const mockProvider: ModelProvider = {
|
||||
provider: 'openai',
|
||||
} as ModelProvider
|
||||
|
||||
const mockCredentials: Credential[] = [
|
||||
{ credential_id: 'cred-1', credential_name: 'API Key 1' },
|
||||
{ credential_id: 'cred-2', credential_name: 'API Key 2' },
|
||||
]
|
||||
|
||||
const mockModel: CustomModelCredential = {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render credentials list', () => {
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('credential-item-cred-1')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('credential-item-cred-2')).toBeInTheDocument()
|
||||
expect(screen.getByText('API Key 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('API Key 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render model title when showModelTitle is true', () => {
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
model={mockModel}
|
||||
showModelTitle
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('model-icon')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('gpt-4')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should not render model title when showModelTitle is false', () => {
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
model={mockModel}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('model-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom title instead of model name', () => {
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
model={mockModel}
|
||||
title="Custom Title"
|
||||
showModelTitle
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Custom Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty credentials array', () => {
|
||||
const { container } = render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={[]}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(container.querySelector('[data-testid^="credential-item-"]')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Callback Propagation', () => {
|
||||
it('should pass onEdit callback to credential items', () => {
|
||||
const onEdit = vi.fn()
|
||||
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
model={mockModel}
|
||||
onEdit={onEdit}
|
||||
/>,
|
||||
)
|
||||
|
||||
screen.getAllByText('Edit')[0].click()
|
||||
|
||||
expect(onEdit).toHaveBeenCalledWith(mockCredentials[0], mockModel)
|
||||
})
|
||||
|
||||
it('should pass onDelete callback to credential items', () => {
|
||||
const onDelete = vi.fn()
|
||||
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
model={mockModel}
|
||||
onDelete={onDelete}
|
||||
/>,
|
||||
)
|
||||
|
||||
screen.getAllByText('Delete')[0].click()
|
||||
|
||||
expect(onDelete).toHaveBeenCalledWith(mockCredentials[0], mockModel)
|
||||
})
|
||||
|
||||
it('should pass onItemClick callback to credential items', () => {
|
||||
const onItemClick = vi.fn()
|
||||
|
||||
render(
|
||||
<AuthorizedItem
|
||||
provider={mockProvider}
|
||||
credentials={mockCredentials}
|
||||
model={mockModel}
|
||||
onItemClick={onItemClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
screen.getAllByText('Click')[0].click()
|
||||
|
||||
expect(onItemClick).toHaveBeenCalledWith(mockCredentials[0], mockModel)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { Credential } from '../../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import CredentialItem from './credential-item'
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiCheckLine: () => <div data-testid="check-icon" />,
|
||||
RiDeleteBinLine: () => <div data-testid="delete-icon" />,
|
||||
RiEqualizer2Line: () => <div data-testid="edit-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: () => <div data-testid="indicator" />,
|
||||
}))
|
||||
|
||||
describe('CredentialItem', () => {
|
||||
const credential: Credential = {
|
||||
credential_id: 'cred-1',
|
||||
credential_name: 'Test API Key',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render credential text and indicator', () => {
|
||||
render(<CredentialItem credential={credential} />)
|
||||
|
||||
expect(screen.getByText('Test API Key')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render enterprise badge for enterprise credential', () => {
|
||||
render(<CredentialItem credential={{ ...credential, from_enterprise: true }} />)
|
||||
|
||||
expect(screen.getByText('Enterprise')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onItemClick when list item is clicked', () => {
|
||||
const onItemClick = vi.fn()
|
||||
|
||||
render(<CredentialItem credential={credential} onItemClick={onItemClick} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Test API Key'))
|
||||
|
||||
expect(onItemClick).toHaveBeenCalledWith(credential)
|
||||
})
|
||||
|
||||
it('should not call onItemClick when credential is unavailable', () => {
|
||||
const onItemClick = vi.fn()
|
||||
|
||||
render(<CredentialItem credential={{ ...credential, not_allowed_to_use: true }} onItemClick={onItemClick} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Test API Key'))
|
||||
|
||||
expect(onItemClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onEdit and onDelete from action buttons', () => {
|
||||
const onEdit = vi.fn()
|
||||
const onDelete = vi.fn()
|
||||
|
||||
render(<CredentialItem credential={credential} onEdit={onEdit} onDelete={onDelete} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-icon').closest('button') as HTMLButtonElement)
|
||||
fireEvent.click(screen.getByTestId('delete-icon').closest('button') as HTMLButtonElement)
|
||||
|
||||
expect(onEdit).toHaveBeenCalledWith(credential)
|
||||
expect(onDelete).toHaveBeenCalledWith(credential)
|
||||
})
|
||||
|
||||
it('should block delete action for the currently selected credential when delete is disabled', () => {
|
||||
const onDelete = vi.fn()
|
||||
|
||||
render(
|
||||
<CredentialItem
|
||||
credential={credential}
|
||||
onDelete={onDelete}
|
||||
disableDeleteButShowAction
|
||||
selectedCredentialId="cred-1"
|
||||
disableDeleteTip="Cannot remove selected"
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('delete-icon').closest('button') as HTMLButtonElement)
|
||||
|
||||
expect(onDelete).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,486 @@
|
||||
import type { Credential, CustomModel, ModelProvider } from '../../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { ConfigurationMethodEnum, ModelTypeEnum } from '../../declarations'
|
||||
import Authorized from './index'
|
||||
|
||||
const mockHandleOpenModal = vi.fn()
|
||||
const mockHandleActiveCredential = vi.fn()
|
||||
const mockOpenConfirmDelete = vi.fn()
|
||||
const mockCloseConfirmDelete = vi.fn()
|
||||
const mockHandleConfirmDelete = vi.fn()
|
||||
|
||||
let mockDeleteCredentialId: string | null = null
|
||||
let mockDoingAction = false
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
useAuth: () => ({
|
||||
openConfirmDelete: mockOpenConfirmDelete,
|
||||
closeConfirmDelete: mockCloseConfirmDelete,
|
||||
doingAction: mockDoingAction,
|
||||
handleActiveCredential: mockHandleActiveCredential,
|
||||
handleConfirmDelete: mockHandleConfirmDelete,
|
||||
deleteCredentialId: mockDeleteCredentialId,
|
||||
handleOpenModal: mockHandleOpenModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
let mockPortalOpen = false
|
||||
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
|
||||
mockPortalOpen = open
|
||||
return <div data-testid="portal" data-open={open}>{children}</div>
|
||||
},
|
||||
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick}>{children}</div>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => {
|
||||
if (!mockPortalOpen)
|
||||
return null
|
||||
return <div data-testid="portal-content">{children}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/confirm', () => ({
|
||||
default: ({ isShow, onCancel, onConfirm }: { isShow: boolean, onCancel: () => void, onConfirm: () => void }) => {
|
||||
if (!isShow)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="confirm-dialog">
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
<button onClick={onConfirm}>Confirm</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./authorized-item', () => ({
|
||||
default: ({ credentials, model, onEdit, onDelete, onItemClick }: {
|
||||
credentials: Credential[]
|
||||
model?: CustomModel
|
||||
onEdit?: (credential: Credential, model?: CustomModel) => void
|
||||
onDelete?: (credential: Credential, model?: CustomModel) => void
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
}) => (
|
||||
<div data-testid="authorized-item">
|
||||
{credentials.map((cred: Credential) => (
|
||||
<div key={cred.credential_id}>
|
||||
<span>{cred.credential_name}</span>
|
||||
<button onClick={() => onEdit?.(cred, model)}>Edit</button>
|
||||
<button onClick={() => onDelete?.(cred, model)}>Delete</button>
|
||||
<button onClick={() => onItemClick?.(cred, model)}>Select</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Authorized', () => {
|
||||
const mockProvider: ModelProvider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
} as ModelProvider
|
||||
|
||||
const mockCredentials: Credential[] = [
|
||||
{ credential_id: 'cred-1', credential_name: 'API Key 1' },
|
||||
{ credential_id: 'cred-2', credential_name: 'API Key 2' },
|
||||
]
|
||||
|
||||
const mockItems = [
|
||||
{
|
||||
model: {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
},
|
||||
credentials: mockCredentials,
|
||||
},
|
||||
]
|
||||
|
||||
const mockRenderTrigger = (open?: boolean) => (
|
||||
<button>
|
||||
Trigger
|
||||
{open ? 'Open' : 'Closed'}
|
||||
</button>
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPortalOpen = false
|
||||
mockDeleteCredentialId = null
|
||||
mockDoingAction = false
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render trigger button', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Trigger/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render portal content when open', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('authorized-item')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render portal content when closed', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Add API Key button when not model credential', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/addApiKey/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Add Model Credential button when is model credential', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
authParams={{ isModelCredential: true }}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/addModelCredential/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render add action when hideAddAction is true', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
hideAddAction
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText(/addApiKey/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render popup title when provided', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
popupTitle="Select Credential"
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Select Credential')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onOpenChange when trigger is clicked in controlled mode', () => {
|
||||
const onOpenChange = vi.fn()
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen={false}
|
||||
onOpenChange={onOpenChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should toggle portal on trigger click', () => {
|
||||
const { rerender } = render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
|
||||
rerender(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open modal when triggerOnlyOpenModal is true', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
triggerOnlyOpenModal
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
|
||||
expect(mockHandleOpenModal).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call handleOpenModal when Add API Key is clicked', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText(/addApiKey/))
|
||||
|
||||
expect(mockHandleOpenModal).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call handleOpenModal with credential and model when edit is clicked', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getAllByText('Edit')[0])
|
||||
|
||||
expect(mockHandleOpenModal).toHaveBeenCalledWith(
|
||||
mockCredentials[0],
|
||||
mockItems[0].model,
|
||||
)
|
||||
})
|
||||
|
||||
it('should pass current model fields when adding model credential', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
authParams={{ isModelCredential: true }}
|
||||
currentCustomConfigurationModelFixedFields={{
|
||||
__model_name: 'gpt-4',
|
||||
__model_type: ModelTypeEnum.textGeneration,
|
||||
}}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText(/addModelCredential/))
|
||||
|
||||
expect(mockHandleOpenModal).toHaveBeenCalledWith(undefined, {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onItemClick when credential is selected', () => {
|
||||
const onItemClick = vi.fn()
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
onItemClick={onItemClick}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getAllByText('Select')[0])
|
||||
|
||||
expect(onItemClick).toHaveBeenCalledWith(mockCredentials[0], mockItems[0].model)
|
||||
})
|
||||
|
||||
it('should call handleActiveCredential when onItemClick is not provided', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getAllByText('Select')[0])
|
||||
|
||||
expect(mockHandleActiveCredential).toHaveBeenCalledWith(mockCredentials[0], mockItems[0].model)
|
||||
})
|
||||
|
||||
it('should not call onItemClick when disableItemClick is true', () => {
|
||||
const onItemClick = vi.fn()
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
onItemClick={onItemClick}
|
||||
disableItemClick
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getAllByText('Select')[0])
|
||||
|
||||
expect(onItemClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Confirmation', () => {
|
||||
it('should show confirm dialog when deleteCredentialId is set', () => {
|
||||
mockDeleteCredentialId = 'cred-1'
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('confirm-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show confirm dialog when deleteCredentialId is null', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call closeConfirmDelete when cancel is clicked', () => {
|
||||
mockDeleteCredentialId = 'cred-1'
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('Cancel'))
|
||||
|
||||
expect(mockCloseConfirmDelete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call handleConfirmDelete when confirm is clicked', () => {
|
||||
mockDeleteCredentialId = 'cred-1'
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('Confirm'))
|
||||
|
||||
expect(mockHandleConfirmDelete).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty items array', () => {
|
||||
render(
|
||||
<Authorized
|
||||
provider={mockProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={[]}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('authorized-item')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render add action when provider does not allow custom token', () => {
|
||||
const restrictedProvider = { ...mockProvider, allow_custom_token: false }
|
||||
|
||||
render(
|
||||
<Authorized
|
||||
provider={restrictedProvider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={mockItems}
|
||||
renderTrigger={mockRenderTrigger}
|
||||
isOpen
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText(/addApiKey/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import ConfigModel from './config-model'
|
||||
|
||||
// Mock icons
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiEqualizer2Line: () => <div data-testid="config-icon" />,
|
||||
RiScales3Line: () => <div data-testid="scales-icon" />,
|
||||
}))
|
||||
|
||||
// Mock Indicator
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div data-testid={`indicator-${color}`} />,
|
||||
}))
|
||||
|
||||
describe('ConfigModel', () => {
|
||||
it('should render authorization error when loadBalancingInvalid is true', () => {
|
||||
const onClick = vi.fn()
|
||||
render(<ConfigModel loadBalancingInvalid onClick={onClick} />)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.authorizationError/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('scales-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-orange')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText(/modelProvider.auth.authorizationError/))
|
||||
expect(onClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render credential removed message when credentialRemoved is true', () => {
|
||||
render(<ConfigModel credentialRemoved />)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.credentialRemoved/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-red')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render standard config message when no flags enabled', () => {
|
||||
render(<ConfigModel />)
|
||||
|
||||
expect(screen.getByText(/operation.config/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('config-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render config load balancing when loadBalancingEnabled is true', () => {
|
||||
render(<ConfigModel loadBalancingEnabled />)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.configLoadBalancing/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('scales-icon')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import ConfigProvider from './config-provider'
|
||||
|
||||
const mockUseCredentialStatus = vi.fn()
|
||||
|
||||
vi.mock('./hooks', () => ({
|
||||
useCredentialStatus: () => mockUseCredentialStatus(),
|
||||
}))
|
||||
|
||||
vi.mock('./authorized', () => ({
|
||||
default: ({ renderTrigger }: { renderTrigger: () => React.ReactNode }) => (
|
||||
<div>
|
||||
{renderTrigger()}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('ConfigProvider', () => {
|
||||
const baseProvider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
} as ModelProvider
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should show setup label when no credential exists', () => {
|
||||
mockUseCredentialStatus.mockReturnValue({
|
||||
hasCredential: false,
|
||||
authorized: true,
|
||||
current_credential_id: '',
|
||||
current_credential_name: '',
|
||||
available_credentials: [],
|
||||
})
|
||||
|
||||
render(<ConfigProvider provider={baseProvider} />)
|
||||
|
||||
expect(screen.getByText(/operation.setup/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show config label when credential exists', () => {
|
||||
mockUseCredentialStatus.mockReturnValue({
|
||||
hasCredential: true,
|
||||
authorized: true,
|
||||
current_credential_id: 'cred-1',
|
||||
current_credential_name: 'Key 1',
|
||||
available_credentials: [],
|
||||
})
|
||||
|
||||
render(<ConfigProvider provider={baseProvider} />)
|
||||
|
||||
expect(screen.getByText(/operation.config/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should still render setup label when custom credentials are not allowed', () => {
|
||||
mockUseCredentialStatus.mockReturnValue({
|
||||
hasCredential: false,
|
||||
authorized: false,
|
||||
current_credential_id: '',
|
||||
current_credential_name: '',
|
||||
available_credentials: [],
|
||||
})
|
||||
|
||||
render(<ConfigProvider provider={{ ...baseProvider, allow_custom_token: false }} />)
|
||||
|
||||
expect(screen.getByText(/operation.setup/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import CredentialSelector from './credential-selector'
|
||||
|
||||
// Mock components
|
||||
vi.mock('./authorized/credential-item', () => ({
|
||||
default: ({ credential, onItemClick }: { credential: { credential_name: string }, onItemClick: (c: unknown) => void }) => (
|
||||
<div data-testid="credential-item" onClick={() => onItemClick(credential)}>
|
||||
{credential.credential_name}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: () => <div data-testid="indicator" />,
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiAddLine: () => <div data-testid="add-icon" />,
|
||||
RiArrowDownSLine: () => <div data-testid="arrow-icon" />,
|
||||
}))
|
||||
|
||||
// Mock portal components
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => (
|
||||
<div data-testid="portal" data-open={open}>{children}</div>
|
||||
),
|
||||
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick}>{children}</div>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children }: { children: React.ReactNode, open?: boolean }) => {
|
||||
// We should only render children if open or if we want to test they are hidden
|
||||
// The real component might handle this with CSS or conditional rendering.
|
||||
// Let's use conditional rendering in the mock to avoid "multiple elements" errors.
|
||||
return <div data-testid="portal-content">{children}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
describe('CredentialSelector', () => {
|
||||
const mockCredentials = [
|
||||
{ credential_id: 'cred-1', credential_name: 'Key 1' },
|
||||
{ credential_id: 'cred-2', credential_name: 'Key 2' },
|
||||
]
|
||||
const mockOnSelect = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render selected credential name', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
selectedCredential={mockCredentials[0]}
|
||||
credentials={mockCredentials}
|
||||
onSelect={mockOnSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Use getAllByText and take the first one (the one in the trigger)
|
||||
expect(screen.getAllByText('Key 1')[0]).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render placeholder when no credential selected', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
credentials={mockCredentials}
|
||||
onSelect={mockOnSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.selectModelCredential/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open portal on click', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
credentials={mockCredentials}
|
||||
onSelect={mockOnSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
expect(screen.getByTestId('portal')).toHaveAttribute('data-open', 'true')
|
||||
expect(screen.getAllByTestId('credential-item')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should call onSelect when a credential is clicked', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
credentials={mockCredentials}
|
||||
onSelect={mockOnSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByText('Key 2'))
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(mockCredentials[1])
|
||||
})
|
||||
|
||||
it('should call onSelect with add new credential data when clicking add button', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
credentials={mockCredentials}
|
||||
onSelect={mockOnSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
fireEvent.click(screen.getByText(/modelProvider.auth.addNewModelCredential/))
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
credential_id: '__add_new_credential',
|
||||
addNewCredential: true,
|
||||
}))
|
||||
})
|
||||
|
||||
it('should not open portal when disabled', () => {
|
||||
render(
|
||||
<CredentialSelector
|
||||
disabled
|
||||
credentials={mockCredentials}
|
||||
onSelect={mockOnSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
expect(screen.getByTestId('portal')).toHaveAttribute('data-open', 'false')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { CustomModel } from '../../declarations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { ModelTypeEnum } from '../../declarations'
|
||||
import { useAuthService, useGetCredential } from './use-auth-service'
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useGetProviderCredential: vi.fn(),
|
||||
useGetModelCredential: vi.fn(),
|
||||
useAddProviderCredential: vi.fn(),
|
||||
useEditProviderCredential: vi.fn(),
|
||||
useDeleteProviderCredential: vi.fn(),
|
||||
useActiveProviderCredential: vi.fn(),
|
||||
useAddModelCredential: vi.fn(),
|
||||
useEditModelCredential: vi.fn(),
|
||||
useDeleteModelCredential: vi.fn(),
|
||||
useActiveModelCredential: vi.fn(),
|
||||
}))
|
||||
|
||||
const {
|
||||
useGetProviderCredential,
|
||||
useGetModelCredential,
|
||||
useAddProviderCredential,
|
||||
useEditProviderCredential,
|
||||
useDeleteProviderCredential,
|
||||
useActiveProviderCredential,
|
||||
useAddModelCredential,
|
||||
useEditModelCredential,
|
||||
useDeleteModelCredential,
|
||||
useActiveModelCredential,
|
||||
} = await import('@/service/use-models')
|
||||
|
||||
describe('useAuthService hooks', () => {
|
||||
let queryClient: QueryClient
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
|
||||
const mockMutationReturn = { mutateAsync: vi.fn() }
|
||||
vi.mocked(useAddProviderCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useAddProviderCredential>)
|
||||
vi.mocked(useEditProviderCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useEditProviderCredential>)
|
||||
vi.mocked(useDeleteProviderCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useDeleteProviderCredential>)
|
||||
vi.mocked(useActiveProviderCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useActiveProviderCredential>)
|
||||
vi.mocked(useAddModelCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useAddModelCredential>)
|
||||
vi.mocked(useEditModelCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useEditModelCredential>)
|
||||
vi.mocked(useDeleteModelCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useDeleteModelCredential>)
|
||||
vi.mocked(useActiveModelCredential).mockReturnValue(mockMutationReturn as unknown as ReturnType<typeof useActiveModelCredential>)
|
||||
})
|
||||
|
||||
it('useGetCredential selects correct source and params', () => {
|
||||
const mockData = { data: 'test' }
|
||||
vi.mocked(useGetProviderCredential).mockReturnValue(mockData as unknown as ReturnType<typeof useGetProviderCredential>)
|
||||
vi.mocked(useGetModelCredential).mockReturnValue(mockData as unknown as ReturnType<typeof useGetModelCredential>)
|
||||
|
||||
// Provider case
|
||||
const { result: providerRes } = renderHook(() => useGetCredential('openai', false, 'cred-123'), { wrapper })
|
||||
expect(useGetProviderCredential).toHaveBeenCalledWith(true, 'openai', 'cred-123')
|
||||
expect(providerRes.current).toBe(mockData)
|
||||
|
||||
// Model case
|
||||
const mockModel = { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration } as CustomModel
|
||||
const { result: modelRes } = renderHook(() => useGetCredential('openai', true, 'cred-123', mockModel, 'src'), { wrapper })
|
||||
expect(useGetModelCredential).toHaveBeenCalledWith(true, 'openai', 'cred-123', 'gpt-4', ModelTypeEnum.textGeneration, 'src')
|
||||
expect(modelRes.current).toBe(mockData)
|
||||
|
||||
// Early return cases
|
||||
renderHook(() => useGetCredential('openai', false), { wrapper })
|
||||
expect(useGetProviderCredential).toHaveBeenCalledWith(false, 'openai', undefined)
|
||||
|
||||
// Branch: isModelCredential true but no id/model
|
||||
renderHook(() => useGetCredential('openai', true), { wrapper })
|
||||
expect(useGetModelCredential).toHaveBeenCalledWith(false, 'openai', undefined, undefined, undefined, undefined)
|
||||
})
|
||||
|
||||
it('useAuthService provides correct services for provider and model', () => {
|
||||
const { result } = renderHook(() => useAuthService('openai'), { wrapper })
|
||||
|
||||
// Provider services
|
||||
expect(result.current.getAddCredentialService(false)).toBe(vi.mocked(useAddProviderCredential).mock.results[0].value.mutateAsync)
|
||||
expect(result.current.getEditCredentialService(false)).toBe(vi.mocked(useEditProviderCredential).mock.results[0].value.mutateAsync)
|
||||
expect(result.current.getDeleteCredentialService(false)).toBe(vi.mocked(useDeleteProviderCredential).mock.results[0].value.mutateAsync)
|
||||
expect(result.current.getActiveCredentialService(false)).toBe(vi.mocked(useActiveProviderCredential).mock.results[0].value.mutateAsync)
|
||||
|
||||
// Model services
|
||||
expect(result.current.getAddCredentialService(true)).toBe(vi.mocked(useAddModelCredential).mock.results[0].value.mutateAsync)
|
||||
expect(result.current.getEditCredentialService(true)).toBe(vi.mocked(useEditModelCredential).mock.results[0].value.mutateAsync)
|
||||
expect(result.current.getDeleteCredentialService(true)).toBe(vi.mocked(useDeleteModelCredential).mock.results[0].value.mutateAsync)
|
||||
expect(result.current.getActiveCredentialService(true)).toBe(vi.mocked(useActiveModelCredential).mock.results[0].value.mutateAsync)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,247 @@
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { ConfigurationMethodEnum, ModelModalModeEnum, ModelTypeEnum } from '../../declarations'
|
||||
import { useAuth } from './use-auth'
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
const mockHandleRefreshModel = vi.fn()
|
||||
const mockOpenModelModal = vi.fn()
|
||||
const mockDeleteModelService = vi.fn()
|
||||
const mockDeleteProviderCredential = vi.fn()
|
||||
const mockDeleteModelCredential = vi.fn()
|
||||
const mockActiveProviderCredential = vi.fn()
|
||||
const mockActiveModelCredential = vi.fn()
|
||||
const mockAddProviderCredential = vi.fn()
|
||||
const mockAddModelCredential = vi.fn()
|
||||
const mockEditProviderCredential = vi.fn()
|
||||
const mockEditModelCredential = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelModalHandler: () => mockOpenModelModal,
|
||||
useRefreshModel: () => ({ handleRefreshModel: mockHandleRefreshModel }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useDeleteModel: () => ({ mutateAsync: mockDeleteModelService }),
|
||||
}))
|
||||
|
||||
vi.mock('./use-auth-service', () => ({
|
||||
useAuthService: () => ({
|
||||
getDeleteCredentialService: (isModel: boolean) => (isModel ? mockDeleteModelCredential : mockDeleteProviderCredential),
|
||||
getActiveCredentialService: (isModel: boolean) => (isModel ? mockActiveModelCredential : mockActiveProviderCredential),
|
||||
getEditCredentialService: (isModel: boolean) => (isModel ? mockEditModelCredential : mockEditProviderCredential),
|
||||
getAddCredentialService: (isModel: boolean) => (isModel ? mockAddModelCredential : mockAddProviderCredential),
|
||||
}),
|
||||
}))
|
||||
|
||||
const createDeferred = <T,>() => {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('useAuth', () => {
|
||||
const provider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
} as ModelProvider
|
||||
|
||||
const credential: Credential = {
|
||||
credential_id: 'cred-1',
|
||||
credential_name: 'Primary key',
|
||||
}
|
||||
|
||||
const model: CustomModel = {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDeleteModelService.mockResolvedValue({ result: 'success' })
|
||||
mockDeleteProviderCredential.mockResolvedValue({ result: 'success' })
|
||||
mockDeleteModelCredential.mockResolvedValue({ result: 'success' })
|
||||
mockActiveProviderCredential.mockResolvedValue({ result: 'success' })
|
||||
mockActiveModelCredential.mockResolvedValue({ result: 'success' })
|
||||
mockAddProviderCredential.mockResolvedValue({ result: 'success' })
|
||||
mockAddModelCredential.mockResolvedValue({ result: 'success' })
|
||||
mockEditProviderCredential.mockResolvedValue({ result: 'success' })
|
||||
mockEditModelCredential.mockResolvedValue({ result: 'success' })
|
||||
})
|
||||
|
||||
it('should open and close delete confirmation state', () => {
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.predefinedModel))
|
||||
|
||||
act(() => {
|
||||
result.current.openConfirmDelete(credential, model)
|
||||
})
|
||||
|
||||
expect(result.current.deleteCredentialId).toBe('cred-1')
|
||||
expect(result.current.deleteModel).toEqual(model)
|
||||
expect(result.current.pendingOperationCredentialId.current).toBe('cred-1')
|
||||
expect(result.current.pendingOperationModel.current).toEqual(model)
|
||||
|
||||
act(() => {
|
||||
result.current.closeConfirmDelete()
|
||||
})
|
||||
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
expect(result.current.deleteModel).toBeNull()
|
||||
})
|
||||
|
||||
it('should activate credential, notify success, and refresh models', async () => {
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.customizableModel))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleActiveCredential(credential, model)
|
||||
})
|
||||
|
||||
expect(mockActiveModelCredential).toHaveBeenCalledWith({
|
||||
credential_id: 'cred-1',
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
})
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
message: 'common.api.actionSuccess',
|
||||
}))
|
||||
expect(mockHandleRefreshModel).toHaveBeenCalledWith(provider, undefined, true)
|
||||
expect(result.current.doingAction).toBe(false)
|
||||
})
|
||||
|
||||
it('should close delete dialog without calling services when nothing is pending', async () => {
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.predefinedModel))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleConfirmDelete()
|
||||
})
|
||||
|
||||
expect(mockDeleteProviderCredential).not.toHaveBeenCalled()
|
||||
expect(mockDeleteModelService).not.toHaveBeenCalled()
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
expect(result.current.deleteModel).toBeNull()
|
||||
})
|
||||
|
||||
it('should delete credential and call onRemove callback', async () => {
|
||||
const onRemove = vi.fn()
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.predefinedModel, undefined, {
|
||||
isModelCredential: false,
|
||||
onRemove,
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.openConfirmDelete(credential, model)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleConfirmDelete()
|
||||
})
|
||||
|
||||
expect(mockDeleteProviderCredential).toHaveBeenCalledWith({
|
||||
credential_id: 'cred-1',
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
})
|
||||
expect(mockDeleteModelService).not.toHaveBeenCalled()
|
||||
expect(onRemove).toHaveBeenCalledWith('cred-1')
|
||||
expect(result.current.deleteCredentialId).toBeNull()
|
||||
})
|
||||
|
||||
it('should delete model when pending operation has no credential id', async () => {
|
||||
const onRemove = vi.fn()
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.customizableModel, undefined, {
|
||||
onRemove,
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.openConfirmDelete(undefined, model)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleConfirmDelete()
|
||||
})
|
||||
|
||||
expect(mockDeleteModelService).toHaveBeenCalledWith({
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
})
|
||||
expect(onRemove).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should add or edit credentials and refresh on successful save', async () => {
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.predefinedModel))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSaveCredential({ api_key: 'new-key' })
|
||||
})
|
||||
|
||||
expect(mockAddProviderCredential).toHaveBeenCalledWith({ api_key: 'new-key' })
|
||||
expect(mockHandleRefreshModel).toHaveBeenCalledWith(provider, undefined, true)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSaveCredential({ credential_id: 'cred-1', api_key: 'updated-key' })
|
||||
})
|
||||
|
||||
expect(mockEditProviderCredential).toHaveBeenCalledWith({ credential_id: 'cred-1', api_key: 'updated-key' })
|
||||
expect(mockHandleRefreshModel).toHaveBeenCalledWith(provider, undefined, false)
|
||||
})
|
||||
|
||||
it('should ignore duplicate save requests while an action is in progress', async () => {
|
||||
const deferred = createDeferred<{ result: string }>()
|
||||
mockAddProviderCredential.mockReturnValueOnce(deferred.promise)
|
||||
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.predefinedModel))
|
||||
|
||||
let first!: Promise<void>
|
||||
let second!: Promise<void>
|
||||
|
||||
await act(async () => {
|
||||
first = result.current.handleSaveCredential({ api_key: 'first' })
|
||||
second = result.current.handleSaveCredential({ api_key: 'second' })
|
||||
deferred.resolve({ result: 'success' })
|
||||
await Promise.all([first, second])
|
||||
})
|
||||
|
||||
expect(mockAddProviderCredential).toHaveBeenCalledTimes(1)
|
||||
expect(mockAddProviderCredential).toHaveBeenCalledWith({ api_key: 'first' })
|
||||
})
|
||||
|
||||
it('should forward modal open arguments', () => {
|
||||
const onUpdate = vi.fn()
|
||||
const fixedFields = {
|
||||
__model_name: 'gpt-4',
|
||||
__model_type: ModelTypeEnum.textGeneration,
|
||||
}
|
||||
const { result } = renderHook(() => useAuth(provider, ConfigurationMethodEnum.customizableModel, fixedFields, {
|
||||
isModelCredential: true,
|
||||
onUpdate,
|
||||
mode: ModelModalModeEnum.configModelCredential,
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleOpenModal(credential, model)
|
||||
})
|
||||
|
||||
expect(mockOpenModelModal).toHaveBeenCalledWith(
|
||||
provider,
|
||||
ConfigurationMethodEnum.customizableModel,
|
||||
fixedFields,
|
||||
expect.objectContaining({
|
||||
isModelCredential: true,
|
||||
credential,
|
||||
model,
|
||||
onUpdate,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Credential, CustomModelCredential, ModelProvider } from '../../declarations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useCredentialData } from './use-credential-data'
|
||||
|
||||
vi.mock('./use-auth-service', () => ({
|
||||
useGetCredential: vi.fn(),
|
||||
}))
|
||||
|
||||
const { useGetCredential } = await import('./use-auth-service')
|
||||
|
||||
describe('useCredentialData', () => {
|
||||
let queryClient: QueryClient
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
})
|
||||
|
||||
it('determines correct config source and parameters', () => {
|
||||
vi.mocked(useGetCredential).mockReturnValue({ isLoading: false, data: {} } as unknown as ReturnType<typeof useGetCredential>)
|
||||
const mockProvider = { provider: 'openai' } as unknown as ModelProvider
|
||||
|
||||
// Predefined source
|
||||
renderHook(() => useCredentialData(mockProvider, true), { wrapper })
|
||||
expect(useGetCredential).toHaveBeenCalledWith('openai', undefined, undefined, undefined, 'predefined-model')
|
||||
|
||||
// Custom source
|
||||
renderHook(() => useCredentialData(mockProvider, false), { wrapper })
|
||||
expect(useGetCredential).toHaveBeenCalledWith('openai', undefined, undefined, undefined, 'custom-model')
|
||||
})
|
||||
|
||||
it('returns appropriate loading and data states', () => {
|
||||
const mockData = { api_key: 'test' }
|
||||
vi.mocked(useGetCredential).mockReturnValue({ isLoading: true, data: undefined } as unknown as ReturnType<typeof useGetCredential>)
|
||||
const mockProvider = { provider: 'openai' } as unknown as ModelProvider
|
||||
|
||||
const { result: loadingRes } = renderHook(() => useCredentialData(mockProvider, true), { wrapper })
|
||||
expect(loadingRes.current.isLoading).toBe(true)
|
||||
expect(loadingRes.current.credentialData).toEqual({})
|
||||
|
||||
vi.mocked(useGetCredential).mockReturnValue({ isLoading: false, data: mockData } as unknown as ReturnType<typeof useGetCredential>)
|
||||
const { result: dataRes } = renderHook(() => useCredentialData(mockProvider, true), { wrapper })
|
||||
expect(dataRes.current.isLoading).toBe(false)
|
||||
expect(dataRes.current.credentialData).toBe(mockData)
|
||||
})
|
||||
|
||||
it('passes credential and model identifier correctly', () => {
|
||||
vi.mocked(useGetCredential).mockReturnValue({ isLoading: false, data: {} } as unknown as ReturnType<typeof useGetCredential>)
|
||||
const mockProvider = { provider: 'openai' } as unknown as ModelProvider
|
||||
const mockCredential = { credential_id: 'cred-123' } as unknown as Credential
|
||||
const mockModel = { model: 'gpt-4' } as unknown as CustomModelCredential
|
||||
|
||||
renderHook(() => useCredentialData(mockProvider, true, true, mockCredential, mockModel), { wrapper })
|
||||
expect(useGetCredential).toHaveBeenCalledWith('openai', true, 'cred-123', mockModel, 'predefined-model')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ModelProvider } from '../../declarations'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useCredentialStatus } from './use-credential-status'
|
||||
|
||||
describe('useCredentialStatus', () => {
|
||||
it('computes authorized and authRemoved status correctly', () => {
|
||||
// Authorized case
|
||||
const authProvider = {
|
||||
custom_configuration: {
|
||||
current_credential_id: '123',
|
||||
current_credential_name: 'Key',
|
||||
available_credentials: [{ credential_id: '123', credential_name: 'Key' }],
|
||||
},
|
||||
} as unknown as ModelProvider
|
||||
const { result: authRes } = renderHook(() => useCredentialStatus(authProvider))
|
||||
expect(authRes.current.authorized).toBeTruthy()
|
||||
expect(authRes.current.authRemoved).toBe(false)
|
||||
|
||||
// AuthRemoved case (found but not selected)
|
||||
const removedProvider = {
|
||||
custom_configuration: {
|
||||
current_credential_id: '',
|
||||
current_credential_name: '',
|
||||
available_credentials: [{ credential_id: '123' }],
|
||||
},
|
||||
} as unknown as ModelProvider
|
||||
const { result: removedRes } = renderHook(() => useCredentialStatus(removedProvider))
|
||||
expect(removedRes.current.authRemoved).toBe(true)
|
||||
expect(removedRes.current.authorized).toBeFalsy()
|
||||
})
|
||||
|
||||
it('handles empty or restricted credentials', () => {
|
||||
// Empty case
|
||||
const emptyProvider = {
|
||||
custom_configuration: { available_credentials: [] },
|
||||
} as unknown as ModelProvider
|
||||
const { result: emptyRes } = renderHook(() => useCredentialStatus(emptyProvider))
|
||||
expect(emptyRes.current.hasCredential).toBe(false)
|
||||
|
||||
// Restricted case
|
||||
const restrictedProvider = {
|
||||
custom_configuration: {
|
||||
current_credential_id: '123',
|
||||
available_credentials: [{ credential_id: '123', not_allowed_to_use: true }],
|
||||
},
|
||||
} as unknown as ModelProvider
|
||||
const { result: restrictedRes } = renderHook(() => useCredentialStatus(restrictedProvider))
|
||||
expect(restrictedRes.current.notAllowedToUse).toBe(true)
|
||||
})
|
||||
|
||||
it('handles undefined custom configuration gracefully', () => {
|
||||
const { result } = renderHook(() => useCredentialStatus({ custom_configuration: {} } as ModelProvider))
|
||||
expect(result.current.hasCredential).toBe(false)
|
||||
expect(result.current.available_credentials).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ModelProvider } from '../../declarations'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useCanAddedModels, useCustomModels } from './use-custom-models'
|
||||
|
||||
describe('useCustomModels and useCanAddedModels', () => {
|
||||
it('extracts custom models from provider correctly', () => {
|
||||
const mockProvider = {
|
||||
custom_configuration: {
|
||||
custom_models: [
|
||||
{ model: 'gpt-4', model_type: 'text-generation' },
|
||||
{ model: 'gpt-3.5', model_type: 'text-generation' },
|
||||
],
|
||||
},
|
||||
} as unknown as ModelProvider
|
||||
|
||||
const { result } = renderHook(() => useCustomModels(mockProvider))
|
||||
expect(result.current).toHaveLength(2)
|
||||
expect(result.current[0].model).toBe('gpt-4')
|
||||
|
||||
const { result: emptyRes } = renderHook(() => useCustomModels({ custom_configuration: {} } as unknown as ModelProvider))
|
||||
expect(emptyRes.current).toEqual([])
|
||||
})
|
||||
|
||||
it('extracts can_added_models from provider correctly', () => {
|
||||
const mockProvider = {
|
||||
custom_configuration: {
|
||||
can_added_models: [{ model: 'gpt-4-turbo', model_type: 'text-generation' }],
|
||||
},
|
||||
} as unknown as ModelProvider
|
||||
|
||||
const { result } = renderHook(() => useCanAddedModels(mockProvider))
|
||||
expect(result.current).toHaveLength(1)
|
||||
expect(result.current[0].model).toBe('gpt-4-turbo')
|
||||
|
||||
const { result: emptyRes } = renderHook(() => useCanAddedModels({ custom_configuration: {} } as unknown as ModelProvider))
|
||||
expect(emptyRes.current).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { useModelFormSchemas } from './use-model-form-schemas'
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
genModelNameFormSchema: vi.fn(() => ({
|
||||
type: FormTypeEnum.textInput,
|
||||
variable: '__model_name',
|
||||
label: 'Model Name',
|
||||
required: true,
|
||||
})),
|
||||
genModelTypeFormSchema: vi.fn(() => ({
|
||||
type: FormTypeEnum.select,
|
||||
variable: '__model_type',
|
||||
label: 'Model Type',
|
||||
required: true,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('useModelFormSchemas', () => {
|
||||
const mockProvider = {
|
||||
provider: 'openai',
|
||||
provider_credential_schema: {
|
||||
credential_form_schemas: [
|
||||
{ type: FormTypeEnum.textInput, variable: 'api_key', label: 'API Key', required: true },
|
||||
],
|
||||
},
|
||||
model_credential_schema: {
|
||||
credential_form_schemas: [
|
||||
{ type: FormTypeEnum.textInput, variable: 'model_key', label: 'Model Key', required: true },
|
||||
],
|
||||
},
|
||||
supported_model_types: ['text-generation'],
|
||||
} as unknown as ModelProvider
|
||||
|
||||
it('selects correct form schemas based on providerFormSchemaPredefined', () => {
|
||||
const { result: providerResult } = renderHook(() => useModelFormSchemas(mockProvider, true))
|
||||
expect(providerResult.current.formSchemas.some(s => s.variable === 'api_key')).toBe(true)
|
||||
|
||||
const { result: modelResult } = renderHook(() => useModelFormSchemas(mockProvider, false))
|
||||
expect(modelResult.current.formSchemas.some(s => s.variable === 'model_key')).toBe(true)
|
||||
|
||||
const { result: emptyResult } = renderHook(() => useModelFormSchemas({} as unknown as ModelProvider, true))
|
||||
expect(emptyResult.current.formSchemas).toHaveLength(1) // only __authorization_name__
|
||||
})
|
||||
|
||||
it('computes form values correctly for credentials and models', () => {
|
||||
const mockCredential = { credential_name: 'Test' } as unknown as Credential
|
||||
const mockModel = { model: 'gpt-4', model_type: 'text-generation' } as unknown as CustomModelCredential
|
||||
const { result } = renderHook(() => useModelFormSchemas(mockProvider, true, { api_key: 'val' }, mockCredential, mockModel))
|
||||
expect((result.current.formValues as Record<string, unknown>).api_key).toBe('val')
|
||||
expect((result.current.formValues as Record<string, unknown>).__authorization_name__).toBe('Test')
|
||||
expect((result.current.formValues as Record<string, unknown>).__model_name).toBe('gpt-4')
|
||||
|
||||
// Branch: credential present but credentials (param) missing
|
||||
const { result: emptyCredsRes } = renderHook(() => useModelFormSchemas(mockProvider, true, undefined, mockCredential))
|
||||
expect((emptyCredsRes.current.formValues as Record<string, unknown>).__authorization_name__).toBe('Test')
|
||||
})
|
||||
|
||||
it('handles model name and type schemas for custom models', () => {
|
||||
const { result: predefined } = renderHook(() => useModelFormSchemas(mockProvider, true))
|
||||
expect(predefined.current.modelNameAndTypeFormSchemas).toHaveLength(0)
|
||||
|
||||
const { result: custom } = renderHook(() => useModelFormSchemas(mockProvider, false))
|
||||
expect(custom.current.modelNameAndTypeFormSchemas).toHaveLength(2)
|
||||
expect(custom.current.modelNameAndTypeFormSchemas[0].variable).toBe('__model_name')
|
||||
|
||||
const mockModel = { model: 'custom', model_type: 'text' } as unknown as CustomModelCredential
|
||||
const { result: customWithVal } = renderHook(() => useModelFormSchemas(mockProvider, false, undefined, undefined, mockModel))
|
||||
expect((customWithVal.current.modelNameAndTypeFormValues as Record<string, unknown>).__model_name).toBe('custom')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import ManageCustomModelCredentials from './manage-custom-model-credentials'
|
||||
|
||||
// Mock hooks
|
||||
const mockUseCustomModels = vi.fn()
|
||||
vi.mock('./hooks', () => ({
|
||||
useCustomModels: () => mockUseCustomModels(),
|
||||
useAuth: () => ({
|
||||
handleOpenModal: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Authorized
|
||||
vi.mock('./authorized', () => ({
|
||||
default: ({ renderTrigger, items, popupTitle }: { renderTrigger: (o?: boolean) => React.ReactNode, items: { length: number }, popupTitle: string }) => (
|
||||
<div data-testid="authorized-mock">
|
||||
<div data-testid="trigger-container">{renderTrigger()}</div>
|
||||
<div data-testid="popup-title">{popupTitle}</div>
|
||||
<div data-testid="items-count">{items.length}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('ManageCustomModelCredentials', () => {
|
||||
const mockProvider = {
|
||||
provider: 'openai',
|
||||
} as unknown as ModelProvider
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return null when no custom models exist', () => {
|
||||
mockUseCustomModels.mockReturnValue([])
|
||||
const { container } = render(<ManageCustomModelCredentials provider={mockProvider} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render authorized component when custom models exist', () => {
|
||||
const mockModels = [
|
||||
{
|
||||
model: 'gpt-4',
|
||||
available_model_credentials: [{ credential_id: 'c1', credential_name: 'Key 1' }],
|
||||
current_credential_id: 'c1',
|
||||
current_credential_name: 'Key 1',
|
||||
},
|
||||
{
|
||||
model: 'gpt-3.5',
|
||||
// testing undefined credentials branch
|
||||
},
|
||||
]
|
||||
mockUseCustomModels.mockReturnValue(mockModels)
|
||||
|
||||
render(<ManageCustomModelCredentials provider={mockProvider} />)
|
||||
|
||||
expect(screen.getByTestId('authorized-mock')).toBeInTheDocument()
|
||||
expect(screen.getByText(/modelProvider.auth.manageCredentials/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('items-count')).toHaveTextContent('2')
|
||||
expect(screen.getByTestId('popup-title')).toHaveTextContent('modelProvider.auth.customModelCredentials')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { CustomModel, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import SwitchCredentialInLoadBalancing from './switch-credential-in-load-balancing'
|
||||
|
||||
// Mock components
|
||||
vi.mock('./authorized', () => ({
|
||||
default: ({ renderTrigger, onItemClick, items }: { renderTrigger: () => React.ReactNode, onItemClick: (c: unknown) => void, items: { credentials: unknown[] }[] }) => (
|
||||
<div data-testid="authorized-mock">
|
||||
<div data-testid="trigger-container" onClick={() => onItemClick(items[0].credentials[0])}>
|
||||
{renderTrigger()}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <div data-testid={`indicator-${color}`} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip-mock">
|
||||
{children}
|
||||
<div>{popupContent}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiArrowDownSLine: () => <div data-testid="arrow-icon" />,
|
||||
}))
|
||||
|
||||
describe('SwitchCredentialInLoadBalancing', () => {
|
||||
const mockProvider = {
|
||||
provider: 'openai',
|
||||
allow_custom_token: true,
|
||||
} as unknown as ModelProvider
|
||||
|
||||
const mockModel = {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
} as unknown as CustomModel
|
||||
|
||||
const mockCredentials = [
|
||||
{ credential_id: 'cred-1', credential_name: 'Key 1' },
|
||||
{ credential_id: 'cred-2', credential_name: 'Key 2' },
|
||||
]
|
||||
|
||||
const mockSetCustomModelCredential = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render selected credential name correctly', () => {
|
||||
render(
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={mockProvider}
|
||||
model={mockModel}
|
||||
credentials={mockCredentials}
|
||||
customModelCredential={mockCredentials[0]}
|
||||
setCustomModelCredential={mockSetCustomModelCredential}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Key 1')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-green')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render auth removed status when selected credential is not in list', () => {
|
||||
render(
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={mockProvider}
|
||||
model={mockModel}
|
||||
credentials={mockCredentials}
|
||||
customModelCredential={{ credential_id: 'dead-cred', credential_name: 'Dead Key' }}
|
||||
setCustomModelCredential={mockSetCustomModelCredential}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/modelProvider.auth.authRemoved/)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('indicator-red')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render unavailable status when credentials list is empty', () => {
|
||||
render(
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={mockProvider}
|
||||
model={mockModel}
|
||||
credentials={[]}
|
||||
customModelCredential={undefined}
|
||||
setCustomModelCredential={mockSetCustomModelCredential}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/auth.credentialUnavailableInButton/)).toBeInTheDocument()
|
||||
expect(screen.queryByTestId(/indicator-/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call setCustomModelCredential when an item is selected in Authorized', () => {
|
||||
render(
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={mockProvider}
|
||||
model={mockModel}
|
||||
credentials={mockCredentials}
|
||||
customModelCredential={mockCredentials[0]}
|
||||
setCustomModelCredential={mockSetCustomModelCredential}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('trigger-container'))
|
||||
expect(mockSetCustomModelCredential).toHaveBeenCalledWith(mockCredentials[0])
|
||||
})
|
||||
|
||||
it('should show tooltip when empty and custom credentials not allowed', () => {
|
||||
const restrictedProvider = { ...mockProvider, allow_custom_token: false }
|
||||
render(
|
||||
<SwitchCredentialInLoadBalancing
|
||||
provider={restrictedProvider}
|
||||
model={mockModel}
|
||||
credentials={[]}
|
||||
customModelCredential={undefined}
|
||||
setCustomModelCredential={mockSetCustomModelCredential}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('plugin.auth.credentialUnavailable')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user