mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 08:58:09 +08:00
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:
@ -0,0 +1,45 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AuthorizedInDataSourceNode from '../authorized-in-data-source-node'
|
||||
|
||||
vi.mock('@/app/components/header/indicator', () => ({
|
||||
default: ({ color }: { color: string }) => <span data-testid="indicator" data-color={color} />,
|
||||
}))
|
||||
|
||||
describe('AuthorizedInDataSourceNode', () => {
|
||||
const mockOnJump = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders with green indicator', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green')
|
||||
})
|
||||
|
||||
it('renders singular text for 1 authorization', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByText('plugin.auth.authorization')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders plural text for multiple authorizations', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={3} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByText('plugin.auth.authorizations')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onJumpToDataSourcePage when button is clicked', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(mockOnJump).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders settings button', () => {
|
||||
render(<AuthorizedInDataSourceNode authorizationsNum={1} onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,210 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
const mockGetPluginCredentialInfo = vi.fn()
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (url: string) => ({
|
||||
data: url ? mockGetPluginCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useDeletePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useSetPluginDefaultCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useUpdatePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginCredentialInfo: () => vi.fn(),
|
||||
useGetPluginOAuthUrl: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginOAuthClientSchema: () => ({
|
||||
data: mockGetPluginOAuthClientSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
useSetPluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useDeletePluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginOAuthClientSchema: () => vi.fn(),
|
||||
useAddPluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const mockIsCurrentWorkspaceManager = vi.fn()
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: { options: [] }, isLoading: false }),
|
||||
useTriggerPluginDynamicOptionsInfo: () => ({ data: null, isLoading: false }),
|
||||
useInvalidTriggerDynamicOptions: () => vi.fn(),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const testQueryClient = createTestQueryClient()
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const createPluginPayload = (overrides: Partial<PluginPayload> = {}): PluginPayload => ({
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
id: 'test-credential-id',
|
||||
name: 'Test Credential',
|
||||
provider: 'test-provider',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
is_default: false,
|
||||
credentials: { api_key: 'test-key' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('AuthorizedInNode Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsCurrentWorkspaceManager.mockReturnValue(true)
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential({ is_default: true })],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
mockGetPluginOAuthClientSchema.mockReturnValue({
|
||||
schema: [],
|
||||
is_oauth_custom_client_enabled: false,
|
||||
is_system_oauth_params_exists: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render with workspace default when no credentialId', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('plugin.auth.workspaceDefault')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render credential name when credentialId matches', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({ id: 'selected-id', name: 'My Credential' })
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} credentialId="selected-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('My Credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show auth removed when credentialId not found', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential()],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} credentialId="non-existent" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('plugin.auth.authRemoved')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show unavailable when credential is not allowed', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({
|
||||
id: 'unavailable-id',
|
||||
not_allowed_to_use: true,
|
||||
from_enterprise: false,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} credentialId="unavailable-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should show unavailable when default credential is not allowed', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({
|
||||
is_default: true,
|
||||
not_allowed_to_use: true,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should call onAuthorizationItemClick when clicking', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[0])
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should be memoized', async () => {
|
||||
const AuthorizedInNodeModule = await import('../authorized-in-node')
|
||||
expect(typeof AuthorizedInNodeModule.default).toBe('object')
|
||||
})
|
||||
})
|
||||
247
web/app/components/plugins/plugin-auth/__tests__/index.spec.tsx
Normal file
247
web/app/components/plugins/plugin-auth/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
const mockGetPluginCredentialInfo = vi.fn()
|
||||
const mockDeletePluginCredential = vi.fn()
|
||||
const mockSetPluginDefaultCredential = vi.fn()
|
||||
const mockUpdatePluginCredential = vi.fn()
|
||||
const mockInvalidPluginCredentialInfo = vi.fn()
|
||||
const mockGetPluginOAuthUrl = vi.fn()
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
const mockSetPluginOAuthCustomClient = vi.fn()
|
||||
const mockDeletePluginOAuthCustomClient = vi.fn()
|
||||
const mockInvalidPluginOAuthClientSchema = vi.fn()
|
||||
const mockAddPluginCredential = vi.fn()
|
||||
const mockGetPluginCredentialSchema = vi.fn()
|
||||
const mockInvalidToolsByType = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (url: string) => ({
|
||||
data: url ? mockGetPluginCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useDeletePluginCredential: () => ({
|
||||
mutateAsync: mockDeletePluginCredential,
|
||||
}),
|
||||
useSetPluginDefaultCredential: () => ({
|
||||
mutateAsync: mockSetPluginDefaultCredential,
|
||||
}),
|
||||
useUpdatePluginCredential: () => ({
|
||||
mutateAsync: mockUpdatePluginCredential,
|
||||
}),
|
||||
useInvalidPluginCredentialInfo: () => mockInvalidPluginCredentialInfo,
|
||||
useGetPluginOAuthUrl: () => ({
|
||||
mutateAsync: mockGetPluginOAuthUrl,
|
||||
}),
|
||||
useGetPluginOAuthClientSchema: () => ({
|
||||
data: mockGetPluginOAuthClientSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
useSetPluginOAuthCustomClient: () => ({
|
||||
mutateAsync: mockSetPluginOAuthCustomClient,
|
||||
}),
|
||||
useDeletePluginOAuthCustomClient: () => ({
|
||||
mutateAsync: mockDeletePluginOAuthCustomClient,
|
||||
}),
|
||||
useInvalidPluginOAuthClientSchema: () => mockInvalidPluginOAuthClientSchema,
|
||||
useAddPluginCredential: () => ({
|
||||
mutateAsync: mockAddPluginCredential,
|
||||
}),
|
||||
useGetPluginCredentialSchema: () => ({
|
||||
data: mockGetPluginCredentialSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => mockInvalidToolsByType,
|
||||
}))
|
||||
|
||||
const mockIsCurrentWorkspaceManager = vi.fn()
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({
|
||||
data: { options: [] },
|
||||
isLoading: false,
|
||||
}),
|
||||
useTriggerPluginDynamicOptionsInfo: () => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
}),
|
||||
useInvalidTriggerDynamicOptions: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const _createWrapper = () => {
|
||||
const testQueryClient = createTestQueryClient()
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const _createPluginPayload = (overrides: Partial<PluginPayload> = {}): PluginPayload => ({
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
id: 'test-credential-id',
|
||||
name: 'Test Credential',
|
||||
provider: 'test-provider',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
is_default: false,
|
||||
credentials: { api_key: 'test-key' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const _createCredentialList = (count: number, overrides: Partial<Credential>[] = []): Credential[] => {
|
||||
return Array.from({ length: count }, (_, i) => createCredential({
|
||||
id: `credential-${i}`,
|
||||
name: `Credential ${i}`,
|
||||
is_default: i === 0,
|
||||
...overrides[i],
|
||||
}))
|
||||
}
|
||||
|
||||
describe('Index Exports', () => {
|
||||
it('should export all required components and hooks', async () => {
|
||||
const exports = await import('../index')
|
||||
|
||||
expect(exports.AddApiKeyButton).toBeDefined()
|
||||
expect(exports.AddOAuthButton).toBeDefined()
|
||||
expect(exports.ApiKeyModal).toBeDefined()
|
||||
expect(exports.Authorized).toBeDefined()
|
||||
expect(exports.AuthorizedInDataSourceNode).toBeDefined()
|
||||
expect(exports.AuthorizedInNode).toBeDefined()
|
||||
expect(exports.usePluginAuth).toBeDefined()
|
||||
expect(exports.PluginAuth).toBeDefined()
|
||||
expect(exports.PluginAuthInAgent).toBeDefined()
|
||||
expect(exports.PluginAuthInDataSourceNode).toBeDefined()
|
||||
}, 15000)
|
||||
|
||||
it('should export AuthCategory enum', async () => {
|
||||
const exports = await import('../index')
|
||||
|
||||
expect(exports.AuthCategory).toBeDefined()
|
||||
expect(exports.AuthCategory.tool).toBe('tool')
|
||||
expect(exports.AuthCategory.datasource).toBe('datasource')
|
||||
expect(exports.AuthCategory.model).toBe('model')
|
||||
expect(exports.AuthCategory.trigger).toBe('trigger')
|
||||
}, 15000)
|
||||
|
||||
it('should export CredentialTypeEnum', async () => {
|
||||
const exports = await import('../index')
|
||||
|
||||
expect(exports.CredentialTypeEnum).toBeDefined()
|
||||
expect(exports.CredentialTypeEnum.OAUTH2).toBe('oauth2')
|
||||
expect(exports.CredentialTypeEnum.API_KEY).toBe('api-key')
|
||||
}, 15000)
|
||||
})
|
||||
|
||||
describe('Types', () => {
|
||||
describe('AuthCategory enum', () => {
|
||||
it('should have correct values', () => {
|
||||
expect(AuthCategory.tool).toBe('tool')
|
||||
expect(AuthCategory.datasource).toBe('datasource')
|
||||
expect(AuthCategory.model).toBe('model')
|
||||
expect(AuthCategory.trigger).toBe('trigger')
|
||||
})
|
||||
|
||||
it('should have exactly 4 categories', () => {
|
||||
const values = Object.values(AuthCategory)
|
||||
expect(values).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CredentialTypeEnum', () => {
|
||||
it('should have correct values', () => {
|
||||
expect(CredentialTypeEnum.OAUTH2).toBe('oauth2')
|
||||
expect(CredentialTypeEnum.API_KEY).toBe('api-key')
|
||||
})
|
||||
|
||||
it('should have exactly 2 types', () => {
|
||||
const values = Object.values(CredentialTypeEnum)
|
||||
expect(values).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Credential type', () => {
|
||||
it('should allow creating valid credentials', () => {
|
||||
const credential: Credential = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
provider: 'test-provider',
|
||||
is_default: true,
|
||||
}
|
||||
expect(credential.id).toBe('test-id')
|
||||
expect(credential.is_default).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow optional fields', () => {
|
||||
const credential: Credential = {
|
||||
id: 'test-id',
|
||||
name: 'Test',
|
||||
provider: 'test-provider',
|
||||
is_default: false,
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
credentials: { key: 'value' },
|
||||
isWorkspaceDefault: true,
|
||||
from_enterprise: false,
|
||||
not_allowed_to_use: false,
|
||||
}
|
||||
expect(credential.credential_type).toBe(CredentialTypeEnum.API_KEY)
|
||||
expect(credential.isWorkspaceDefault).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PluginPayload type', () => {
|
||||
it('should allow creating valid plugin payload', () => {
|
||||
const payload: PluginPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
expect(payload.category).toBe(AuthCategory.tool)
|
||||
})
|
||||
|
||||
it('should allow optional fields', () => {
|
||||
const payload: PluginPayload = {
|
||||
category: AuthCategory.datasource,
|
||||
provider: 'test-provider',
|
||||
providerType: 'builtin',
|
||||
detail: undefined,
|
||||
}
|
||||
expect(payload.providerType).toBe('builtin')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,255 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
// ==================== Mock Setup ====================
|
||||
|
||||
const mockGetPluginCredentialInfo = vi.fn()
|
||||
const mockGetPluginOAuthClientSchema = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins-auth', () => ({
|
||||
useGetPluginCredentialInfo: (url: string) => ({
|
||||
data: url ? mockGetPluginCredentialInfo() : undefined,
|
||||
isLoading: false,
|
||||
}),
|
||||
useDeletePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useSetPluginDefaultCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useUpdatePluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginCredentialInfo: () => vi.fn(),
|
||||
useGetPluginOAuthUrl: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginOAuthClientSchema: () => ({
|
||||
data: mockGetPluginOAuthClientSchema(),
|
||||
isLoading: false,
|
||||
}),
|
||||
useSetPluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useDeletePluginOAuthCustomClient: () => ({ mutateAsync: vi.fn() }),
|
||||
useInvalidPluginOAuthClientSchema: () => vi.fn(),
|
||||
useAddPluginCredential: () => ({ mutateAsync: vi.fn() }),
|
||||
useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidToolsByType: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const mockIsCurrentWorkspaceManager = vi.fn()
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-oauth', () => ({
|
||||
openOAuthPopup: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useTriggerPluginDynamicOptions: () => ({ data: { options: [] }, isLoading: false }),
|
||||
useTriggerPluginDynamicOptionsInfo: () => ({ data: null, isLoading: false }),
|
||||
useInvalidTriggerDynamicOptions: () => vi.fn(),
|
||||
}))
|
||||
|
||||
// ==================== Test Utilities ====================
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
const createWrapper = () => {
|
||||
const testQueryClient = createTestQueryClient()
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={testQueryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const createPluginPayload = (overrides: Partial<PluginPayload> = {}): PluginPayload => ({
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
id: 'test-credential-id',
|
||||
name: 'Test Credential',
|
||||
provider: 'test-provider',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
is_default: false,
|
||||
credentials: { api_key: 'test-key' },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==================== Tests ====================
|
||||
|
||||
describe('PluginAuthInAgent Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsCurrentWorkspaceManager.mockReturnValue(true)
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential()],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
mockGetPluginOAuthClientSchema.mockReturnValue({
|
||||
schema: [],
|
||||
is_oauth_custom_client_enabled: false,
|
||||
is_system_oauth_params_exists: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Authorize when not authorized', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Authorized with workspace default when authorized', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.auth.workspaceDefault')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show credential name when credentialId is provided', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const credential = createCredential({ id: 'selected-id', name: 'Selected Credential' })
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} credentialId="selected-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('Selected Credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show auth removed when credential not found', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [createCredential()],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} credentialId="non-existent-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByText('plugin.auth.authRemoved')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show unavailable when credential is not allowed to use', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const credential = createCredential({
|
||||
id: 'unavailable-id',
|
||||
name: 'Unavailable Credential',
|
||||
not_allowed_to_use: true,
|
||||
from_enterprise: false,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} credentialId="unavailable-id" />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should call onAuthorizationItemClick when item is clicked', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[0])
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should trigger handleAuthorizationItemClick and close popup when item is clicked', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const credential = createCredential({ id: 'test-cred-id', name: 'Test Credential' })
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const triggerButton = screen.getByRole('button')
|
||||
fireEvent.click(triggerButton)
|
||||
const workspaceDefaultItems = screen.getAllByText('plugin.auth.workspaceDefault')
|
||||
const popupItem = workspaceDefaultItems.length > 1 ? workspaceDefaultItems[1] : workspaceDefaultItems[0]
|
||||
fireEvent.click(popupItem)
|
||||
expect(onAuthorizationItemClick).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should call onAuthorizationItemClick with credential id when specific credential is clicked', async () => {
|
||||
const PluginAuthInAgent = (await import('../plugin-auth-in-agent')).default
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const credential = createCredential({
|
||||
id: 'specific-cred-id',
|
||||
name: 'Specific Credential',
|
||||
credential_type: CredentialTypeEnum.API_KEY,
|
||||
})
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [credential],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<PluginAuthInAgent pluginPayload={pluginPayload} onAuthorizationItemClick={onAuthorizationItemClick} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
const triggerButton = screen.getByRole('button')
|
||||
fireEvent.click(triggerButton)
|
||||
const credentialItems = screen.getAllByText('Specific Credential')
|
||||
const popupItem = credentialItems[credentialItems.length - 1]
|
||||
fireEvent.click(popupItem)
|
||||
expect(onAuthorizationItemClick).toHaveBeenCalledWith('specific-cred-id')
|
||||
})
|
||||
|
||||
it('should be memoized', async () => {
|
||||
const PluginAuthInAgentModule = await import('../plugin-auth-in-agent')
|
||||
expect(typeof PluginAuthInAgentModule.default).toBe('object')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,51 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import PluginAuthInDataSourceNode from '../plugin-auth-in-datasource-node'
|
||||
|
||||
describe('PluginAuthInDataSourceNode', () => {
|
||||
const mockOnJump = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders connect button when not authorized', () => {
|
||||
render(<PluginAuthInDataSourceNode onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByText('common.integrations.connect')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders connect button', () => {
|
||||
render(<PluginAuthInDataSourceNode onJumpToDataSourcePage={mockOnJump} />)
|
||||
expect(screen.getByRole('button', { name: /common\.integrations\.connect/ })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onJumpToDataSourcePage when connect button is clicked', () => {
|
||||
render(<PluginAuthInDataSourceNode onJumpToDataSourcePage={mockOnJump} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.integrations\.connect/ }))
|
||||
expect(mockOnJump).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('hides connect button and shows children when authorized', () => {
|
||||
render(
|
||||
<PluginAuthInDataSourceNode isAuthorized onJumpToDataSourcePage={mockOnJump}>
|
||||
<div data-testid="child-content">Data Source Connected</div>
|
||||
</PluginAuthInDataSourceNode>,
|
||||
)
|
||||
expect(screen.queryByText('common.integrations.connect')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('child-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows connect button when isAuthorized is false', () => {
|
||||
render(
|
||||
<PluginAuthInDataSourceNode isAuthorized={false} onJumpToDataSourcePage={mockOnJump}>
|
||||
<div data-testid="child-content">Data Source Connected</div>
|
||||
</PluginAuthInDataSourceNode>,
|
||||
)
|
||||
expect(screen.getByText('common.integrations.connect')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('child-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,139 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import PluginAuth from '../plugin-auth'
|
||||
import { AuthCategory } from '../types'
|
||||
|
||||
const mockUsePluginAuth = vi.fn()
|
||||
vi.mock('../hooks/use-plugin-auth', () => ({
|
||||
usePluginAuth: (...args: unknown[]) => mockUsePluginAuth(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../authorize', () => ({
|
||||
default: ({ pluginPayload }: { pluginPayload: { provider: string } }) => (
|
||||
<div data-testid="authorize">
|
||||
Authorize:
|
||||
{pluginPayload.provider}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../authorized', () => ({
|
||||
default: ({ pluginPayload }: { pluginPayload: { provider: string } }) => (
|
||||
<div data-testid="authorized">
|
||||
Authorized:
|
||||
{pluginPayload.provider}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const defaultPayload = {
|
||||
category: AuthCategory.tool,
|
||||
provider: 'test-provider',
|
||||
}
|
||||
|
||||
describe('PluginAuth', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders Authorize component when not authorized', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: false,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
expect(screen.getByTestId('authorize')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('authorized')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Authorized component when authorized and no children', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: true,
|
||||
canOAuth: true,
|
||||
canApiKey: true,
|
||||
credentials: [{ id: '1', name: 'key', is_default: true, provider: 'test' }],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
expect(screen.getByTestId('authorized')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('authorize')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders children when authorized and children provided', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: true,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [{ id: '1', name: 'key', is_default: true, provider: 'test' }],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(
|
||||
<PluginAuth pluginPayload={defaultPayload}>
|
||||
<div data-testid="custom-children">Custom Content</div>
|
||||
</PluginAuth>,
|
||||
)
|
||||
expect(screen.getByTestId('custom-children')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('authorized')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies className when not authorized', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: false,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
const { container } = render(<PluginAuth pluginPayload={defaultPayload} className="custom-class" />)
|
||||
expect((container.firstChild as HTMLElement).className).toContain('custom-class')
|
||||
})
|
||||
|
||||
it('does not apply className when authorized', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: true,
|
||||
canOAuth: false,
|
||||
canApiKey: true,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
const { container } = render(<PluginAuth pluginPayload={defaultPayload} className="custom-class" />)
|
||||
expect((container.firstChild as HTMLElement).className).not.toContain('custom-class')
|
||||
})
|
||||
|
||||
it('passes pluginPayload.provider to usePluginAuth', () => {
|
||||
mockUsePluginAuth.mockReturnValue({
|
||||
isAuthorized: false,
|
||||
canOAuth: false,
|
||||
canApiKey: false,
|
||||
credentials: [],
|
||||
disabled: false,
|
||||
invalidPluginCredentialInfo: vi.fn(),
|
||||
notAllowCustomCredential: false,
|
||||
})
|
||||
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
expect(mockUsePluginAuth).toHaveBeenCalledWith(defaultPayload, true)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { transformFormSchemasSecretInput } from '../utils'
|
||||
|
||||
describe('plugin-auth/utils', () => {
|
||||
describe('transformFormSchemasSecretInput', () => {
|
||||
it('replaces secret input values with [__HIDDEN__]', () => {
|
||||
const values = { api_key: 'sk-12345', username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result.api_key).toBe('[__HIDDEN__]')
|
||||
expect(result.username).toBe('admin')
|
||||
})
|
||||
|
||||
it('does not replace falsy values (empty string)', () => {
|
||||
const values = { api_key: '', username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result.api_key).toBe('')
|
||||
})
|
||||
|
||||
it('does not replace undefined values', () => {
|
||||
const values = { username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result.api_key).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles multiple secret fields', () => {
|
||||
const values = { key1: 'secret1', key2: 'secret2', normal: 'value' }
|
||||
const result = transformFormSchemasSecretInput(['key1', 'key2'], values)
|
||||
expect(result.key1).toBe('[__HIDDEN__]')
|
||||
expect(result.key2).toBe('[__HIDDEN__]')
|
||||
expect(result.normal).toBe('value')
|
||||
})
|
||||
|
||||
it('does not mutate the original values', () => {
|
||||
const values = { api_key: 'sk-12345' }
|
||||
const result = transformFormSchemasSecretInput(['api_key'], values)
|
||||
expect(result).not.toBe(values)
|
||||
expect(values.api_key).toBe('sk-12345')
|
||||
})
|
||||
|
||||
it('returns same values when no secret names provided', () => {
|
||||
const values = { api_key: 'sk-12345', username: 'admin' }
|
||||
const result = transformFormSchemasSecretInput([], values)
|
||||
expect(result).toEqual(values)
|
||||
})
|
||||
|
||||
it('handles null-like values correctly', () => {
|
||||
const values = { key: null, key2: 0, key3: false }
|
||||
const result = transformFormSchemasSecretInput(['key', 'key2', 'key3'], values)
|
||||
// null, 0, false are falsy — should not be replaced
|
||||
expect(result.key).toBeNull()
|
||||
expect(result.key2).toBe(0)
|
||||
expect(result.key3).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user