test(web): Added test for model-auth files in header folder (#32358)

This commit is contained in:
akashseth-ifp
2026-02-23 10:27:00 +05:30
committed by GitHub
parent e4ddf07194
commit d0bb642fc5
16 changed files with 2015 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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