dify/web/__tests__/plugins/plugin-auth-flow.test.tsx

272 lines
8.6 KiB
TypeScript

/**
* Integration Test: Plugin Authentication Flow
*
* Tests the integration between PluginAuth, usePluginAuth hook,
* Authorize/Authorized components, and credential management.
* Verifies the complete auth flow from checking authorization status
* to rendering the correct UI state.
*/
import { cleanup, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory, CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const map: Record<string, string> = {
'plugin.auth.setUpTip': 'Set up your credentials',
'plugin.auth.authorized': 'Authorized',
'plugin.auth.apiKey': 'API Key',
'plugin.auth.oauth': 'OAuth',
}
return map[key] ?? key
},
}),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
}),
}))
vi.mock('@/utils/classnames', () => ({
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
}))
const mockUsePluginAuth = vi.fn()
vi.mock('@/app/components/plugins/plugin-auth/hooks/use-plugin-auth', () => ({
usePluginAuth: (...args: unknown[]) => mockUsePluginAuth(...args),
}))
vi.mock('@/app/components/plugins/plugin-auth/authorize', () => ({
default: ({ pluginPayload, canOAuth, canApiKey }: {
pluginPayload: { provider: string }
canOAuth: boolean
canApiKey: boolean
}) => (
<div data-testid="authorize-component">
<span data-testid="auth-provider">{pluginPayload.provider}</span>
{canOAuth && <span data-testid="auth-oauth">OAuth available</span>}
{canApiKey && <span data-testid="auth-apikey">API Key available</span>}
</div>
),
}))
vi.mock('@/app/components/plugins/plugin-auth/authorized', () => ({
default: ({ pluginPayload, credentials }: {
pluginPayload: { provider: string }
credentials: Array<{ id: string, name: string }>
}) => (
<div data-testid="authorized-component">
<span data-testid="auth-provider">{pluginPayload.provider}</span>
<span data-testid="auth-credential-count">
{credentials.length}
{' '}
credentials
</span>
</div>
),
}))
const { default: PluginAuth } = await import('@/app/components/plugins/plugin-auth/plugin-auth')
describe('Plugin Authentication Flow Integration', () => {
beforeEach(() => {
vi.clearAllMocks()
cleanup()
})
const basePayload = {
category: AuthCategory.tool,
provider: 'test-provider',
}
describe('Unauthorized State', () => {
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={basePayload} />)
expect(screen.getByTestId('authorize-component')).toBeInTheDocument()
expect(screen.queryByTestId('authorized-component')).not.toBeInTheDocument()
expect(screen.getByTestId('auth-apikey')).toBeInTheDocument()
})
it('shows OAuth option when plugin supports it', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: false,
canOAuth: true,
canApiKey: true,
credentials: [],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
render(<PluginAuth pluginPayload={basePayload} />)
expect(screen.getByTestId('auth-oauth')).toBeInTheDocument()
expect(screen.getByTestId('auth-apikey')).toBeInTheDocument()
})
it('applies className to wrapper when not authorized', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: false,
canOAuth: false,
canApiKey: true,
credentials: [],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
const { container } = render(
<PluginAuth pluginPayload={basePayload} className="custom-class" />,
)
expect(container.firstChild).toHaveClass('custom-class')
})
})
describe('Authorized State', () => {
it('renders Authorized component when authorized and no children', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: true,
canOAuth: false,
canApiKey: true,
credentials: [
{ id: 'cred-1', name: 'My API Key', is_default: true },
],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
render(<PluginAuth pluginPayload={basePayload} />)
expect(screen.queryByTestId('authorize-component')).not.toBeInTheDocument()
expect(screen.getByTestId('authorized-component')).toBeInTheDocument()
expect(screen.getByTestId('auth-credential-count')).toHaveTextContent('1 credentials')
})
it('renders children instead of Authorized when authorized and children provided', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: true,
canOAuth: false,
canApiKey: true,
credentials: [{ id: 'cred-1', name: 'Key', is_default: true }],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
render(
<PluginAuth pluginPayload={basePayload}>
<div data-testid="custom-children">Custom authorized view</div>
</PluginAuth>,
)
expect(screen.queryByTestId('authorize-component')).not.toBeInTheDocument()
expect(screen.queryByTestId('authorized-component')).not.toBeInTheDocument()
expect(screen.getByTestId('custom-children')).toBeInTheDocument()
})
it('does not apply className when authorized', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: true,
canOAuth: false,
canApiKey: true,
credentials: [{ id: 'cred-1', name: 'Key', is_default: true }],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
const { container } = render(
<PluginAuth pluginPayload={basePayload} className="custom-class" />,
)
expect(container.firstChild).not.toHaveClass('custom-class')
})
})
describe('Auth Category Integration', () => {
it('passes correct provider to usePluginAuth for tool category', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: false,
canOAuth: false,
canApiKey: true,
credentials: [],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
const toolPayload = {
category: AuthCategory.tool,
provider: 'google-search-provider',
}
render(<PluginAuth pluginPayload={toolPayload} />)
expect(mockUsePluginAuth).toHaveBeenCalledWith(toolPayload, true)
expect(screen.getByTestId('auth-provider')).toHaveTextContent('google-search-provider')
})
it('passes correct provider to usePluginAuth for datasource category', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: false,
canOAuth: true,
canApiKey: false,
credentials: [],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
const dsPayload = {
category: AuthCategory.datasource,
provider: 'notion-datasource',
}
render(<PluginAuth pluginPayload={dsPayload} />)
expect(mockUsePluginAuth).toHaveBeenCalledWith(dsPayload, true)
expect(screen.getByTestId('auth-oauth')).toBeInTheDocument()
expect(screen.queryByTestId('auth-apikey')).not.toBeInTheDocument()
})
})
describe('Multiple Credentials', () => {
it('shows credential count when multiple credentials exist', () => {
mockUsePluginAuth.mockReturnValue({
isAuthorized: true,
canOAuth: true,
canApiKey: true,
credentials: [
{ id: 'cred-1', name: 'API Key 1', is_default: true },
{ id: 'cred-2', name: 'API Key 2', is_default: false },
{ id: 'cred-3', name: 'OAuth Token', is_default: false, credential_type: CredentialTypeEnum.OAUTH2 },
],
disabled: false,
invalidPluginCredentialInfo: vi.fn(),
notAllowCustomCredential: false,
})
render(<PluginAuth pluginPayload={basePayload} />)
expect(screen.getByTestId('auth-credential-count')).toHaveTextContent('3 credentials')
})
})
})