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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,67 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
import AddApiKeyButton from '../add-api-key-button'
let _mockModalOpen = false
vi.mock('../api-key-modal', () => ({
default: ({ onClose, onUpdate }: { onClose: () => void, onUpdate?: () => void }) => {
_mockModalOpen = true
return (
<div data-testid="api-key-modal">
<button data-testid="modal-close" onClick={onClose}>Close</button>
<button data-testid="modal-update" onClick={onUpdate}>Update</button>
</div>
)
},
}))
const defaultPayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('AddApiKeyButton', () => {
beforeEach(() => {
vi.clearAllMocks()
_mockModalOpen = false
})
afterEach(() => {
cleanup()
})
it('renders button with default text', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('renders button with custom text', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} buttonText="Add Key" />)
expect(screen.getByText('Add Key')).toBeInTheDocument()
})
it('opens modal when button is clicked', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
})
it('respects disabled prop', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} disabled />)
expect(screen.getByRole('button')).toBeDisabled()
})
it('closes modal when onClose is called', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('api-key-modal')).toBeInTheDocument()
fireEvent.click(screen.getByTestId('modal-close'))
expect(screen.queryByTestId('api-key-modal')).not.toBeInTheDocument()
})
it('applies custom button variant', () => {
render(<AddApiKeyButton pluginPayload={defaultPayload} buttonVariant="primary" />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,102 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockGetPluginOAuthUrl = vi.fn().mockResolvedValue({ authorization_url: 'https://auth.example.com' })
const mockOpenOAuthPopup = vi.fn()
vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (obj: Record<string, string> | string) => typeof obj === 'string' ? obj : obj.en_US || '',
}))
vi.mock('@/hooks/use-oauth', () => ({
openOAuthPopup: (...args: unknown[]) => mockOpenOAuthPopup(...args),
}))
vi.mock('../../hooks/use-credential', () => ({
useGetPluginOAuthUrlHook: () => ({
mutateAsync: mockGetPluginOAuthUrl,
}),
useGetPluginOAuthClientSchemaHook: () => ({
data: {
schema: [],
is_oauth_custom_client_enabled: false,
is_system_oauth_params_exists: true,
client_params: {},
redirect_uri: 'https://redirect.example.com',
},
isLoading: false,
}),
}))
vi.mock('../oauth-client-settings', () => ({
default: ({ onClose }: { onClose: () => void }) => (
<div data-testid="oauth-settings-modal">
<button data-testid="oauth-settings-close" onClick={onClose}>Close</button>
</div>
),
}))
vi.mock('@/app/components/base/form/types', () => ({
FormTypeEnum: { radio: 'radio' },
}))
vi.mock('@/utils/classnames', () => ({
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('AddOAuthButton', () => {
let AddOAuthButton: (typeof import('../add-oauth-button'))['default']
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../add-oauth-button')
AddOAuthButton = mod.default
})
it('should render OAuth button when configured (system params exist)', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
expect(screen.getByText('Use OAuth')).toBeInTheDocument()
})
it('should open OAuth settings modal when settings icon clicked', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
fireEvent.click(screen.getByTestId('oauth-settings-button'))
expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument()
})
it('should close OAuth settings modal', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
fireEvent.click(screen.getByTestId('oauth-settings-button'))
fireEvent.click(screen.getByTestId('oauth-settings-close'))
expect(screen.queryByTestId('oauth-settings-modal')).not.toBeInTheDocument()
})
it('should trigger OAuth flow on main button click', async () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
const button = screen.getByText('Use OAuth').closest('button')
if (button)
fireEvent.click(button)
expect(mockGetPluginOAuthUrl).toHaveBeenCalled()
})
it('should be disabled when disabled prop is true', () => {
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" disabled />)
const button = screen.getByText('Use OAuth').closest('button')
expect(button).toBeDisabled()
})
})

View File

@ -0,0 +1,165 @@
import type { ApiKeyModalProps } from '../api-key-modal'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockNotify = vi.fn()
const mockAddPluginCredential = vi.fn().mockResolvedValue({})
const mockUpdatePluginCredential = vi.fn().mockResolvedValue({})
const mockFormValues = { isCheckValidated: true, values: { __name__: 'My Key', api_key: 'sk-123' } }
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('../../hooks/use-credential', () => ({
useAddPluginCredentialHook: () => ({
mutateAsync: mockAddPluginCredential,
}),
useGetPluginCredentialSchemaHook: () => ({
data: [
{ name: 'api_key', label: 'API Key', type: 'secret-input', required: true },
],
isLoading: false,
}),
useUpdatePluginCredentialHook: () => ({
mutateAsync: mockUpdatePluginCredential,
}),
}))
vi.mock('../../../readme-panel/entrance', () => ({
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
}))
vi.mock('../../../readme-panel/store', () => ({
ReadmeShowType: { modal: 'modal' },
}))
vi.mock('@/app/components/base/encrypted-bottom', () => ({
EncryptedBottom: () => <div data-testid="encrypted-bottom" />,
}))
vi.mock('@/app/components/base/modal/modal', () => ({
default: ({ children, title, onClose, onConfirm, onExtraButtonClick, showExtraButton, disabled }: {
children: React.ReactNode
title: string
onClose?: () => void
onCancel?: () => void
onConfirm?: () => void
onExtraButtonClick?: () => void
showExtraButton?: boolean
disabled?: boolean
[key: string]: unknown
}) => (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
<button data-testid="modal-confirm" onClick={onConfirm} disabled={disabled}>Confirm</button>
<button data-testid="modal-close" onClick={onClose}>Close</button>
{showExtraButton && <button data-testid="modal-extra" onClick={onExtraButtonClick}>Remove</button>}
</div>
),
}))
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
return <div data-testid="auth-form" />
}),
}))
vi.mock('@/app/components/base/form/types', () => ({
FormTypeEnum: { textInput: 'text-input' },
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('ApiKeyModal', () => {
let ApiKeyModal: React.FC<ApiKeyModalProps>
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../api-key-modal')
ApiKeyModal = mod.default
})
it('should render modal with correct title', () => {
render(<ApiKeyModal pluginPayload={basePayload} />)
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.auth.useApiAuth')
})
it('should render auth form when data is loaded', () => {
render(<ApiKeyModal pluginPayload={basePayload} />)
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
})
it('should show remove button when editValues is provided', () => {
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing' }} />)
expect(screen.getByTestId('modal-extra')).toBeInTheDocument()
})
it('should not show remove button in add mode', () => {
render(<ApiKeyModal pluginPayload={basePayload} />)
expect(screen.queryByTestId('modal-extra')).not.toBeInTheDocument()
})
it('should call onClose when close button clicked', () => {
const mockOnClose = vi.fn()
render(<ApiKeyModal pluginPayload={basePayload} onClose={mockOnClose} />)
fireEvent.click(screen.getByTestId('modal-close'))
expect(mockOnClose).toHaveBeenCalled()
})
it('should call addPluginCredential on confirm in add mode', async () => {
const mockOnClose = vi.fn()
const mockOnUpdate = vi.fn()
render(<ApiKeyModal pluginPayload={basePayload} onClose={mockOnClose} onUpdate={mockOnUpdate} />)
fireEvent.click(screen.getByTestId('modal-confirm'))
await waitFor(() => {
expect(mockAddPluginCredential).toHaveBeenCalledWith(expect.objectContaining({
type: 'api-key',
name: 'My Key',
}))
})
})
it('should call updatePluginCredential on confirm in edit mode', async () => {
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing', __credential_id__: 'cred-1' }} />)
fireEvent.click(screen.getByTestId('modal-confirm'))
await waitFor(() => {
expect(mockUpdatePluginCredential).toHaveBeenCalled()
})
})
it('should call onRemove when remove button clicked', () => {
const mockOnRemove = vi.fn()
render(<ApiKeyModal pluginPayload={basePayload} editValues={{ api_key: 'existing' }} onRemove={mockOnRemove} />)
fireEvent.click(screen.getByTestId('modal-extra'))
expect(mockOnRemove).toHaveBeenCalled()
})
it('should render readme entrance when detail is provided', () => {
const payload = { ...basePayload, detail: { name: 'Test' } as never }
render(<ApiKeyModal pluginPayload={payload} />)
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
})
})

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import type { PluginPayload } from '../types'
import type { PluginPayload } from '../../types'
import type { FormSchema } from '@/app/components/base/form/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../types'
import { AuthCategory } from '../../types'
// Create a wrapper with QueryClientProvider
const createTestQueryClient = () =>
@ -36,7 +36,7 @@ const mockAddPluginCredential = vi.fn()
const mockUpdatePluginCredential = vi.fn()
const mockGetPluginCredentialSchema = vi.fn()
vi.mock('../hooks/use-credential', () => ({
vi.mock('../../hooks/use-credential', () => ({
useGetPluginOAuthUrlHook: () => ({
mutateAsync: mockGetPluginOAuthUrl,
}),
@ -117,12 +117,12 @@ const createFormSchema = (overrides: Partial<FormSchema> = {}): FormSchema => ({
// ==================== AddApiKeyButton Tests ====================
describe('AddApiKeyButton', () => {
let AddApiKeyButton: typeof import('./add-api-key-button').default
let AddApiKeyButton: typeof import('../add-api-key-button').default
beforeEach(async () => {
vi.clearAllMocks()
mockGetPluginCredentialSchema.mockReturnValue([])
const importedAddApiKeyButton = await import('./add-api-key-button')
const importedAddApiKeyButton = await import('../add-api-key-button')
AddApiKeyButton = importedAddApiKeyButton.default
})
@ -327,7 +327,7 @@ describe('AddApiKeyButton', () => {
describe('Memoization', () => {
it('should be a memoized component', async () => {
const AddApiKeyButtonDefault = (await import('./add-api-key-button')).default
const AddApiKeyButtonDefault = (await import('../add-api-key-button')).default
expect(typeof AddApiKeyButtonDefault).toBe('object')
})
})
@ -335,7 +335,7 @@ describe('AddApiKeyButton', () => {
// ==================== AddOAuthButton Tests ====================
describe('AddOAuthButton', () => {
let AddOAuthButton: typeof import('./add-oauth-button').default
let AddOAuthButton: typeof import('../add-oauth-button').default
beforeEach(async () => {
vi.clearAllMocks()
@ -347,7 +347,7 @@ describe('AddOAuthButton', () => {
redirect_uri: 'https://example.com/callback',
})
mockGetPluginOAuthUrl.mockResolvedValue({ authorization_url: 'https://oauth.example.com/auth' })
const importedAddOAuthButton = await import('./add-oauth-button')
const importedAddOAuthButton = await import('../add-oauth-button')
AddOAuthButton = importedAddOAuthButton.default
})
@ -856,7 +856,7 @@ describe('AddOAuthButton', () => {
// ==================== ApiKeyModal Tests ====================
describe('ApiKeyModal', () => {
let ApiKeyModal: typeof import('./api-key-modal').default
let ApiKeyModal: typeof import('../api-key-modal').default
beforeEach(async () => {
vi.clearAllMocks()
@ -870,7 +870,7 @@ describe('ApiKeyModal', () => {
isCheckValidated: false,
values: {},
})
const importedApiKeyModal = await import('./api-key-modal')
const importedApiKeyModal = await import('../api-key-modal')
ApiKeyModal = importedApiKeyModal.default
})
@ -1272,13 +1272,13 @@ describe('ApiKeyModal', () => {
// ==================== OAuthClientSettings Tests ====================
describe('OAuthClientSettings', () => {
let OAuthClientSettings: typeof import('./oauth-client-settings').default
let OAuthClientSettings: typeof import('../oauth-client-settings').default
beforeEach(async () => {
vi.clearAllMocks()
mockSetPluginOAuthCustomClient.mockResolvedValue({})
mockDeletePluginOAuthCustomClient.mockResolvedValue({})
const importedOAuthClientSettings = await import('./oauth-client-settings')
const importedOAuthClientSettings = await import('../oauth-client-settings')
OAuthClientSettings = importedOAuthClientSettings.default
})
@ -2193,7 +2193,7 @@ describe('OAuthClientSettings', () => {
describe('Memoization', () => {
it('should be a memoized component', async () => {
const OAuthClientSettingsDefault = (await import('./oauth-client-settings')).default
const OAuthClientSettingsDefault = (await import('../oauth-client-settings')).default
expect(typeof OAuthClientSettingsDefault).toBe('object')
})
})
@ -2216,7 +2216,7 @@ describe('Authorize Components Integration', () => {
describe('AddApiKeyButton -> ApiKeyModal Flow', () => {
it('should open ApiKeyModal when AddApiKeyButton is clicked', async () => {
const AddApiKeyButton = (await import('./add-api-key-button')).default
const AddApiKeyButton = (await import('../add-api-key-button')).default
const pluginPayload = createPluginPayload()
render(<AddApiKeyButton pluginPayload={pluginPayload} />, { wrapper: createWrapper() })
@ -2231,7 +2231,7 @@ describe('Authorize Components Integration', () => {
describe('AddOAuthButton -> OAuthClientSettings Flow', () => {
it('should open OAuthClientSettings when setup button is clicked', async () => {
const AddOAuthButton = (await import('./add-oauth-button')).default
const AddOAuthButton = (await import('../add-oauth-button')).default
const pluginPayload = createPluginPayload()
mockGetPluginOAuthClientSchema.mockReturnValue({
schema: [createFormSchema({ name: 'client_id', label: 'Client ID' })],

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import type { PluginPayload } from '../types'
import type { PluginPayload } from '../../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../types'
import Authorize from './index'
import { AuthCategory } from '../../types'
import Authorize from '../index'
// Create a wrapper with QueryClientProvider for real component testing
const createTestQueryClient = () =>
@ -29,7 +29,7 @@ const createWrapper = () => {
// Mock API hooks - only mock network-related hooks
const mockGetPluginOAuthClientSchema = vi.fn()
vi.mock('../hooks/use-credential', () => ({
vi.mock('../../hooks/use-credential', () => ({
useGetPluginOAuthUrlHook: () => ({
mutateAsync: vi.fn().mockResolvedValue({ authorization_url: '' }),
}),
@ -568,7 +568,7 @@ describe('Authorize', () => {
// ==================== Component Memoization ====================
describe('Component Memoization', () => {
it('should be a memoized component (exported with memo)', async () => {
const AuthorizeDefault = (await import('./index')).default
const AuthorizeDefault = (await import('../index')).default
expect(AuthorizeDefault).toBeDefined()
// memo wrapped components are React elements with $$typeof
expect(typeof AuthorizeDefault).toBe('object')

View File

@ -0,0 +1,179 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockNotify = vi.fn()
const mockSetPluginOAuthCustomClient = vi.fn().mockResolvedValue({})
const mockDeletePluginOAuthCustomClient = vi.fn().mockResolvedValue({})
const mockInvalidPluginOAuthClientSchema = vi.fn()
const mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } }
vi.mock('@/app/components/base/toast', () => ({
useToastContext: () => ({
notify: mockNotify,
}),
}))
vi.mock('../../hooks/use-credential', () => ({
useSetPluginOAuthCustomClientHook: () => ({
mutateAsync: mockSetPluginOAuthCustomClient,
}),
useDeletePluginOAuthCustomClientHook: () => ({
mutateAsync: mockDeletePluginOAuthCustomClient,
}),
useInvalidPluginOAuthClientSchemaHook: () => mockInvalidPluginOAuthClientSchema,
}))
vi.mock('../../../readme-panel/entrance', () => ({
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
}))
vi.mock('../../../readme-panel/store', () => ({
ReadmeShowType: { modal: 'modal' },
}))
vi.mock('@/app/components/base/modal/modal', () => ({
default: ({ children, title, onClose: _onClose, onConfirm, onCancel, onExtraButtonClick, footerSlot }: {
children: React.ReactNode
title: string
onClose?: () => void
onConfirm?: () => void
onCancel?: () => void
onExtraButtonClick?: () => void
footerSlot?: React.ReactNode
[key: string]: unknown
}) => (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
<button data-testid="modal-confirm" onClick={onConfirm}>Save And Auth</button>
<button data-testid="modal-cancel" onClick={onCancel}>Save Only</button>
<button data-testid="modal-close" onClick={onExtraButtonClick}>Cancel</button>
{!!footerSlot && <div data-testid="footer-slot">{footerSlot}</div>}
</div>
),
}))
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
return <div data-testid="auth-form" />
}),
}))
vi.mock('@tanstack/react-form', () => ({
useForm: (config: Record<string, unknown>) => ({
store: { subscribe: vi.fn(), getState: () => ({ values: config.defaultValues || {} }) },
}),
useStore: (_store: unknown, selector: (state: Record<string, unknown>) => unknown) => {
return selector({ values: { __oauth_client__: 'custom' } })
},
}))
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
const defaultSchemas = [
{ name: 'client_id', label: 'Client ID', type: 'text-input', required: true },
] as never
describe('OAuthClientSettings', () => {
let OAuthClientSettings: (typeof import('../oauth-client-settings'))['default']
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../oauth-client-settings')
OAuthClientSettings = mod.default
})
it('should render modal with correct title', () => {
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
/>,
)
expect(screen.getByTestId('modal-title')).toHaveTextContent('plugin.auth.oauthClientSettings')
})
it('should render auth form', () => {
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
/>,
)
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
})
it('should call onClose when cancel clicked', () => {
const mockOnClose = vi.fn()
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={mockOnClose}
/>,
)
fireEvent.click(screen.getByTestId('modal-close'))
expect(mockOnClose).toHaveBeenCalled()
})
it('should save settings on save only button click', async () => {
const mockOnClose = vi.fn()
const mockOnUpdate = vi.fn()
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={mockOnClose}
onUpdate={mockOnUpdate}
/>,
)
fireEvent.click(screen.getByTestId('modal-cancel'))
await waitFor(() => {
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledWith(expect.objectContaining({
enable_oauth_custom_client: true,
}))
})
})
it('should save and authorize on confirm button click', async () => {
const mockOnAuth = vi.fn().mockResolvedValue(undefined)
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onAuth={mockOnAuth}
/>,
)
fireEvent.click(screen.getByTestId('modal-confirm'))
await waitFor(() => {
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalled()
})
})
it('should render readme entrance when detail is provided', () => {
const payload = { ...basePayload, detail: { name: 'Test' } as never }
render(
<OAuthClientSettings
pluginPayload={payload}
schemas={defaultSchemas}
/>,
)
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
})
})

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import type { Credential, PluginPayload } from '../types'
import type { Credential, PluginPayload } from '../../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '../types'
import Authorized from './index'
import { AuthCategory, CredentialTypeEnum } from '../../types'
import Authorized from '../index'
// ==================== Mock Setup ====================
@ -13,7 +13,7 @@ const mockDeletePluginCredential = vi.fn()
const mockSetPluginDefaultCredential = vi.fn()
const mockUpdatePluginCredential = vi.fn()
vi.mock('../hooks/use-credential', () => ({
vi.mock('../../hooks/use-credential', () => ({
useDeletePluginCredentialHook: () => ({
mutateAsync: mockDeletePluginCredential,
}),
@ -1620,7 +1620,7 @@ describe('Authorized Component', () => {
// ==================== Memoization Test ====================
describe('Memoization', () => {
it('should be memoized', async () => {
const AuthorizedModule = await import('./index')
const AuthorizedModule = await import('../index')
// memo returns an object with $$typeof
expect(typeof AuthorizedModule.default).toBe('object')
})

View File

@ -1,8 +1,8 @@
import type { Credential } from '../types'
import type { Credential } from '../../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CredentialTypeEnum } from '../types'
import Item from './item'
import { CredentialTypeEnum } from '../../types'
import Item from '../item'
// ==================== Test Utilities ====================
@ -829,7 +829,7 @@ describe('Item Component', () => {
// ==================== Memoization Test ====================
describe('Memoization', () => {
it('should be memoized', async () => {
const ItemModule = await import('./item')
const ItemModule = await import('../item')
// memo returns an object with $$typeof
expect(typeof ItemModule.default).toBe('object')
})

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

File diff suppressed because it is too large Load Diff