test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-12 10:04:56 +08:00
committed by GitHub
parent 10f85074e8
commit d6b025e91e
195 changed files with 12219 additions and 7840 deletions

View File

@ -0,0 +1,186 @@
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '../../types'
import {
useAddPluginCredentialHook,
useDeletePluginCredentialHook,
useDeletePluginOAuthCustomClientHook,
useGetPluginCredentialInfoHook,
useGetPluginCredentialSchemaHook,
useGetPluginOAuthClientSchemaHook,
useGetPluginOAuthUrlHook,
useInvalidPluginCredentialInfoHook,
useInvalidPluginOAuthClientSchemaHook,
useSetPluginDefaultCredentialHook,
useSetPluginOAuthCustomClientHook,
useUpdatePluginCredentialHook,
} from '../use-credential'
// Mock service hooks
const mockUseGetPluginCredentialInfo = vi.fn().mockReturnValue({ data: null, isLoading: false })
const mockUseDeletePluginCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockUseInvalidPluginCredentialInfo = vi.fn().mockReturnValue(vi.fn())
const mockUseSetPluginDefaultCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockUseGetPluginCredentialSchema = vi.fn().mockReturnValue({ data: [], isLoading: false })
const mockUseAddPluginCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockUseUpdatePluginCredential = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockUseGetPluginOAuthUrl = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockUseGetPluginOAuthClientSchema = vi.fn().mockReturnValue({ data: null, isLoading: false })
const mockUseInvalidPluginOAuthClientSchema = vi.fn().mockReturnValue(vi.fn())
const mockUseSetPluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockUseDeletePluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() })
const mockInvalidToolsByType = vi.fn()
vi.mock('@/service/use-plugins-auth', () => ({
useGetPluginCredentialInfo: (...args: unknown[]) => mockUseGetPluginCredentialInfo(...args),
useDeletePluginCredential: (...args: unknown[]) => mockUseDeletePluginCredential(...args),
useInvalidPluginCredentialInfo: (...args: unknown[]) => mockUseInvalidPluginCredentialInfo(...args),
useSetPluginDefaultCredential: (...args: unknown[]) => mockUseSetPluginDefaultCredential(...args),
useGetPluginCredentialSchema: (...args: unknown[]) => mockUseGetPluginCredentialSchema(...args),
useAddPluginCredential: (...args: unknown[]) => mockUseAddPluginCredential(...args),
useUpdatePluginCredential: (...args: unknown[]) => mockUseUpdatePluginCredential(...args),
useGetPluginOAuthUrl: (...args: unknown[]) => mockUseGetPluginOAuthUrl(...args),
useGetPluginOAuthClientSchema: (...args: unknown[]) => mockUseGetPluginOAuthClientSchema(...args),
useInvalidPluginOAuthClientSchema: (...args: unknown[]) => mockUseInvalidPluginOAuthClientSchema(...args),
useSetPluginOAuthCustomClient: (...args: unknown[]) => mockUseSetPluginOAuthCustomClient(...args),
useDeletePluginOAuthCustomClient: (...args: unknown[]) => mockUseDeletePluginOAuthCustomClient(...args),
}))
vi.mock('@/service/use-tools', () => ({
useInvalidToolsByType: () => mockInvalidToolsByType,
}))
const toolPayload = {
category: AuthCategory.tool,
provider: 'test-provider',
providerType: 'builtin',
}
describe('use-credential hooks', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('useGetPluginCredentialInfoHook', () => {
it('should call service with correct URL when enabled', () => {
renderHook(() => useGetPluginCredentialInfoHook(toolPayload, true))
expect(mockUseGetPluginCredentialInfo).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/credential/info`,
)
})
it('should pass empty string when disabled', () => {
renderHook(() => useGetPluginCredentialInfoHook(toolPayload, false))
expect(mockUseGetPluginCredentialInfo).toHaveBeenCalledWith('')
})
})
describe('useDeletePluginCredentialHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useDeletePluginCredentialHook(toolPayload))
expect(mockUseDeletePluginCredential).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/delete`,
)
})
})
describe('useInvalidPluginCredentialInfoHook', () => {
it('should return a function that invalidates both credential info and tools', () => {
const { result } = renderHook(() => useInvalidPluginCredentialInfoHook(toolPayload))
result.current()
const invalidFn = mockUseInvalidPluginCredentialInfo.mock.results[0].value
expect(invalidFn).toHaveBeenCalled()
expect(mockInvalidToolsByType).toHaveBeenCalled()
})
})
describe('useSetPluginDefaultCredentialHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useSetPluginDefaultCredentialHook(toolPayload))
expect(mockUseSetPluginDefaultCredential).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/default-credential`,
)
})
})
describe('useGetPluginCredentialSchemaHook', () => {
it('should call service with correct schema URL for API_KEY', () => {
renderHook(() => useGetPluginCredentialSchemaHook(toolPayload, CredentialTypeEnum.API_KEY))
expect(mockUseGetPluginCredentialSchema).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/credential/schema/${CredentialTypeEnum.API_KEY}`,
)
})
it('should call service with correct schema URL for OAUTH2', () => {
renderHook(() => useGetPluginCredentialSchemaHook(toolPayload, CredentialTypeEnum.OAUTH2))
expect(mockUseGetPluginCredentialSchema).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/credential/schema/${CredentialTypeEnum.OAUTH2}`,
)
})
})
describe('useAddPluginCredentialHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useAddPluginCredentialHook(toolPayload))
expect(mockUseAddPluginCredential).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/add`,
)
})
})
describe('useUpdatePluginCredentialHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useUpdatePluginCredentialHook(toolPayload))
expect(mockUseUpdatePluginCredential).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/update`,
)
})
})
describe('useGetPluginOAuthUrlHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useGetPluginOAuthUrlHook(toolPayload))
expect(mockUseGetPluginOAuthUrl).toHaveBeenCalledWith(
`/oauth/plugin/${toolPayload.provider}/tool/authorization-url`,
)
})
})
describe('useGetPluginOAuthClientSchemaHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useGetPluginOAuthClientSchemaHook(toolPayload))
expect(mockUseGetPluginOAuthClientSchema).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/client-schema`,
)
})
})
describe('useInvalidPluginOAuthClientSchemaHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useInvalidPluginOAuthClientSchemaHook(toolPayload))
expect(mockUseInvalidPluginOAuthClientSchema).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/client-schema`,
)
})
})
describe('useSetPluginOAuthCustomClientHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useSetPluginOAuthCustomClientHook(toolPayload))
expect(mockUseSetPluginOAuthCustomClient).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/custom-client`,
)
})
})
describe('useDeletePluginOAuthCustomClientHook', () => {
it('should call service with correct URL', () => {
renderHook(() => useDeletePluginOAuthCustomClientHook(toolPayload))
expect(mockUseDeletePluginOAuthCustomClient).toHaveBeenCalledWith(
`/workspaces/current/tool-provider/builtin/${toolPayload.provider}/oauth/custom-client`,
)
})
})
})

View File

@ -0,0 +1,80 @@
import { describe, expect, it } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '../../types'
import { useGetApi } from '../use-get-api'
describe('useGetApi', () => {
const provider = 'test-provider'
describe('tool category', () => {
it('returns correct API paths for tool category', () => {
const api = useGetApi({ category: AuthCategory.tool, provider })
expect(api.getCredentialInfo).toBe(`/workspaces/current/tool-provider/builtin/${provider}/credential/info`)
expect(api.setDefaultCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/default-credential`)
expect(api.getCredentials).toBe(`/workspaces/current/tool-provider/builtin/${provider}/credentials`)
expect(api.addCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/add`)
expect(api.updateCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/update`)
expect(api.deleteCredential).toBe(`/workspaces/current/tool-provider/builtin/${provider}/delete`)
expect(api.getOauthUrl).toBe(`/oauth/plugin/${provider}/tool/authorization-url`)
})
it('returns a function for getCredentialSchema', () => {
const api = useGetApi({ category: AuthCategory.tool, provider })
expect(typeof api.getCredentialSchema).toBe('function')
const schemaUrl = api.getCredentialSchema('api-key' as never)
expect(schemaUrl).toBe(`/workspaces/current/tool-provider/builtin/${provider}/credential/schema/api-key`)
})
it('includes OAuth client endpoints', () => {
const api = useGetApi({ category: AuthCategory.tool, provider })
expect(api.getOauthClientSchema).toBe(`/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`)
expect(api.setCustomOauthClient).toBe(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`)
})
})
describe('datasource category', () => {
it('returns correct API paths for datasource category', () => {
const api = useGetApi({ category: AuthCategory.datasource, provider })
expect(api.getCredentials).toBe(`/auth/plugin/datasource/${provider}`)
expect(api.addCredential).toBe(`/auth/plugin/datasource/${provider}`)
expect(api.updateCredential).toBe(`/auth/plugin/datasource/${provider}/update`)
expect(api.deleteCredential).toBe(`/auth/plugin/datasource/${provider}/delete`)
expect(api.setDefaultCredential).toBe(`/auth/plugin/datasource/${provider}/default`)
expect(api.getOauthUrl).toBe(`/oauth/plugin/${provider}/datasource/get-authorization-url`)
})
it('returns empty string for getCredentialInfo', () => {
const api = useGetApi({ category: AuthCategory.datasource, provider })
expect(api.getCredentialInfo).toBe('')
})
it('returns a function for getCredentialSchema that returns empty string', () => {
const api = useGetApi({ category: AuthCategory.datasource, provider })
expect(api.getCredentialSchema(CredentialTypeEnum.API_KEY)).toBe('')
})
})
describe('other categories', () => {
it('returns empty strings as fallback for unsupported category', () => {
const api = useGetApi({ category: AuthCategory.model, provider })
expect(api.getCredentialInfo).toBe('')
expect(api.setDefaultCredential).toBe('')
expect(api.getCredentials).toBe('')
expect(api.addCredential).toBe('')
expect(api.updateCredential).toBe('')
expect(api.deleteCredential).toBe('')
expect(api.getOauthUrl).toBe('')
})
it('returns a function for getCredentialSchema that returns empty string', () => {
const api = useGetApi({ category: AuthCategory.model, provider })
expect(api.getCredentialSchema(CredentialTypeEnum.API_KEY)).toBe('')
})
})
describe('default category', () => {
it('defaults to tool category when category is not specified', () => {
const api = useGetApi({ provider } as { category: AuthCategory, provider: string })
expect(api.getCredentialInfo).toContain('tool-provider')
})
})
})

View File

@ -0,0 +1,191 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { usePluginAuthAction } from '../../hooks/use-plugin-auth-action'
import { AuthCategory } from '../../types'
const mockDeletePluginCredential = vi.fn().mockResolvedValue({})
const mockSetPluginDefaultCredential = vi.fn().mockResolvedValue({})
const mockUpdatePluginCredential = vi.fn().mockResolvedValue({})
const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('../../hooks/use-credential', () => ({
useDeletePluginCredentialHook: () => ({
mutateAsync: mockDeletePluginCredential,
}),
useSetPluginDefaultCredentialHook: () => ({
mutateAsync: mockSetPluginDefaultCredential,
}),
useUpdatePluginCredentialHook: () => ({
mutateAsync: mockUpdatePluginCredential,
}),
}))
const pluginPayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
return function Wrapper({ children }: { children: ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children)
}
}
describe('usePluginAuthAction', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should initialize with default state', () => {
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
wrapper: createWrapper(),
})
expect(result.current.doingAction).toBe(false)
expect(result.current.deleteCredentialId).toBeNull()
expect(result.current.editValues).toBeNull()
})
it('should open and close confirm dialog', () => {
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
wrapper: createWrapper(),
})
act(() => {
result.current.openConfirm('cred-1')
})
expect(result.current.deleteCredentialId).toBe('cred-1')
act(() => {
result.current.closeConfirm()
})
expect(result.current.deleteCredentialId).toBeNull()
})
it('should handle edit action', () => {
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
wrapper: createWrapper(),
})
const editVals = { key: 'value' }
act(() => {
result.current.handleEdit('cred-1', editVals)
})
expect(result.current.editValues).toEqual(editVals)
})
it('should handle remove action by setting deleteCredentialId', () => {
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
wrapper: createWrapper(),
})
act(() => {
result.current.handleEdit('cred-1', { key: 'value' })
})
act(() => {
result.current.handleRemove()
})
expect(result.current.deleteCredentialId).toBe('cred-1')
})
it('should handle confirm delete', async () => {
const mockOnUpdate = vi.fn()
const { result } = renderHook(() => usePluginAuthAction(pluginPayload, mockOnUpdate), {
wrapper: createWrapper(),
})
act(() => {
result.current.openConfirm('cred-1')
})
await act(async () => {
await result.current.handleConfirm()
})
expect(mockDeletePluginCredential).toHaveBeenCalledWith({ credential_id: 'cred-1' })
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }))
expect(mockOnUpdate).toHaveBeenCalled()
expect(result.current.deleteCredentialId).toBeNull()
})
it('should handle set default credential', async () => {
const mockOnUpdate = vi.fn()
const { result } = renderHook(() => usePluginAuthAction(pluginPayload, mockOnUpdate), {
wrapper: createWrapper(),
})
await act(async () => {
await result.current.handleSetDefault('cred-1')
})
expect(mockSetPluginDefaultCredential).toHaveBeenCalledWith('cred-1')
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }))
expect(mockOnUpdate).toHaveBeenCalled()
})
it('should handle rename credential', async () => {
const mockOnUpdate = vi.fn()
const { result } = renderHook(() => usePluginAuthAction(pluginPayload, mockOnUpdate), {
wrapper: createWrapper(),
})
await act(async () => {
await result.current.handleRename({
credential_id: 'cred-1',
name: 'New Name',
})
})
expect(mockUpdatePluginCredential).toHaveBeenCalledWith({
credential_id: 'cred-1',
name: 'New Name',
})
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }))
expect(mockOnUpdate).toHaveBeenCalled()
})
it('should prevent concurrent actions during doingAction', async () => {
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
wrapper: createWrapper(),
})
act(() => {
result.current.handleSetDoingAction(true)
})
expect(result.current.doingAction).toBe(true)
act(() => {
result.current.openConfirm('cred-1')
})
await act(async () => {
await result.current.handleConfirm()
})
expect(mockDeletePluginCredential).not.toHaveBeenCalled()
})
it('should handle confirm without pending credential ID', async () => {
const { result } = renderHook(() => usePluginAuthAction(pluginPayload), {
wrapper: createWrapper(),
})
await act(async () => {
await result.current.handleConfirm()
})
expect(mockDeletePluginCredential).not.toHaveBeenCalled()
expect(result.current.deleteCredentialId).toBeNull()
})
})

View File

@ -0,0 +1,110 @@
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '../../types'
import { usePluginAuth } from '../use-plugin-auth'
// Mock dependencies
const mockCredentials = [
{ id: '1', credential_type: CredentialTypeEnum.API_KEY, is_default: false },
{ id: '2', credential_type: CredentialTypeEnum.OAUTH2, is_default: true },
]
const mockCredentialInfo = vi.fn().mockReturnValue({
credentials: mockCredentials,
supported_credential_types: [CredentialTypeEnum.API_KEY, CredentialTypeEnum.OAUTH2],
allow_custom_token: true,
})
const mockInvalidate = vi.fn()
vi.mock('../use-credential', () => ({
useGetPluginCredentialInfoHook: (_payload: unknown, enable?: boolean) => ({
data: enable ? mockCredentialInfo() : undefined,
isLoading: false,
}),
useInvalidPluginCredentialInfoHook: () => mockInvalidate,
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
}),
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('usePluginAuth', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should return authorized state when credentials exist', () => {
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.isAuthorized).toBe(true)
expect(result.current.credentials).toHaveLength(2)
})
it('should detect OAuth and API Key support', () => {
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.canOAuth).toBe(true)
expect(result.current.canApiKey).toBe(true)
})
it('should return disabled=false for workspace managers', () => {
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.disabled).toBe(false)
})
it('should return notAllowCustomCredential=false when allowed', () => {
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.notAllowCustomCredential).toBe(false)
})
it('should return unauthorized when enable is false', () => {
const { result } = renderHook(() => usePluginAuth(basePayload, false))
expect(result.current.isAuthorized).toBe(false)
expect(result.current.credentials).toEqual([])
})
it('should provide invalidate function', () => {
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.invalidPluginCredentialInfo).toBe(mockInvalidate)
})
it('should handle empty credentials', () => {
mockCredentialInfo.mockReturnValueOnce({
credentials: [],
supported_credential_types: [],
allow_custom_token: false,
})
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.isAuthorized).toBe(false)
expect(result.current.canOAuth).toBe(false)
expect(result.current.canApiKey).toBe(false)
expect(result.current.notAllowCustomCredential).toBe(true)
})
it('should handle only API Key support', () => {
mockCredentialInfo.mockReturnValueOnce({
credentials: [{ id: '1' }],
supported_credential_types: [CredentialTypeEnum.API_KEY],
allow_custom_token: true,
})
const { result } = renderHook(() => usePluginAuth(basePayload, true))
expect(result.current.canApiKey).toBe(true)
expect(result.current.canOAuth).toBe(false)
})
})