dify/web/app/components/app-sidebar/__tests__/app-detail-section.spec.tsx

300 lines
8.8 KiB
TypeScript

import { screen } from '@testing-library/react'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { AppModeEnum } from '@/types/app'
import { AppACLPermission } from '@/utils/permission'
import AppDetailSection from '../app-detail-section'
let mockAppMode = 'chat'
let mockPathname = '/app/app-1/logs'
let mockAppPermissionKeys: string[] = []
let mockIsRbacEnabled = true
const mockConsoleState = vi.hoisted(() => ({
current: {
userProfile: { id: 'user-1' },
workspacePermissionKeys: [] as string[],
},
}))
const render = (ui: Parameters<typeof renderWithConsoleQuery>[0]) =>
renderWithConsoleQuery(ui, {
systemFeatures: {
rbac_enabled: mockIsRbacEnabled,
enable_app_deploy: false,
},
})
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
appDetail: {
id: 'app-1',
name: 'Test App',
mode: mockAppMode,
icon: '🤖',
icon_type: 'emoji',
icon_background: '#fff',
permission_keys: mockAppPermissionKeys,
},
}),
}))
vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => ({
isCurrentWorkspaceEditor: false,
}))
})
vi.mock('@/next/navigation', () => ({
usePathname: () => mockPathname,
}))
vi.mock('../app-info', () => ({
AppInfoView: () => <div />,
}))
vi.mock('../app-info/use-app-info-actions', () => ({
useAppInfoActions: vi.fn(() => ({})),
}))
vi.mock('../../base/divider', () => ({
default: ({ className }: { className?: string }) => <hr className={className} />,
}))
vi.mock('../nav-link', () => ({
default: ({ name, href }: { name: string; href: string }) => <a href={href}>{name}</a>,
}))
describe('AppDetailSection', () => {
beforeEach(() => {
vi.clearAllMocks()
mockAppMode = 'chat'
mockPathname = '/app/app-1/logs'
mockAppPermissionKeys = [AppACLPermission.Monitor]
mockIsRbacEnabled = true
})
// Rendering behavior for app detail navigation entries.
describe('Rendering', () => {
it('should render only overview for chat apps with app monitor permission', () => {
// Arrange
mockAppMode = 'chat'
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.overview' })).toHaveAttribute(
'href',
'/app/app-1/overview',
)
expect(screen.queryByRole('link', { name: 'common.appMenus.logs' })).not.toBeInTheDocument()
expect(
screen.queryByRole('link', { name: 'common.appMenus.annotations' }),
).not.toBeInTheDocument()
})
it('should render logs and annotations for chat apps with app log and annotation permission', () => {
// Arrange
mockAppMode = 'chat'
mockAppPermissionKeys = [AppACLPermission.LogAndAnnotation]
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.logs' })).toHaveAttribute(
'href',
'/app/app-1/logs',
)
expect(screen.getByRole('link', { name: 'common.appMenus.annotations' })).toHaveAttribute(
'href',
'/app/app-1/annotations',
)
expect(
screen.queryByRole('link', { name: 'common.appMenus.overview' }),
).not.toBeInTheDocument()
})
it('should only render logs navigation for workflow apps', () => {
// Arrange
mockAppMode = 'workflow'
mockAppPermissionKeys = [AppACLPermission.LogAndAnnotation]
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.logs' })).toHaveAttribute(
'href',
'/app/app-1/logs',
)
expect(
screen.queryByRole('link', { name: 'common.appMenus.annotations' }),
).not.toBeInTheDocument()
})
it('should only render logs navigation for completion apps', () => {
// Arrange
mockAppMode = 'completion'
mockAppPermissionKeys = [AppACLPermission.LogAndAnnotation]
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.logs' })).toHaveAttribute(
'href',
'/app/app-1/logs',
)
expect(
screen.queryByRole('link', { name: 'common.appMenus.annotations' }),
).not.toBeInTheDocument()
})
it('should render the layout navigation for users with view layout permission', () => {
// Arrange
mockAppPermissionKeys = [AppACLPermission.ViewLayout]
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.promptEng' })).toHaveAttribute(
'href',
'/app/app-1/configuration',
)
expect(screen.queryByRole('link', { name: 'common.appMenus.logs' })).not.toBeInTheDocument()
expect(
screen.queryByRole('link', { name: 'common.appMenus.annotations' }),
).not.toBeInTheDocument()
expect(
screen.queryByRole('link', { name: 'common.appMenus.overview' }),
).not.toBeInTheDocument()
})
it('should hide the layout navigation when layout access is missing', () => {
// Act
render(<AppDetailSection />)
// Assert
expect(
screen.queryByRole('link', { name: 'common.appMenus.promptEng' }),
).not.toBeInTheDocument()
})
it('should render access point navigation using its app route', () => {
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.accessPoint' })).toHaveAttribute(
'href',
'/app/app-1/access-point',
)
expect(
screen.queryByRole('link', { name: 'common.appMenus.apiAccess' }),
).not.toBeInTheDocument()
})
it('should render deploy navigation with app deploy ACL regardless of the legacy workspace role', () => {
// Arrange
mockAppMode = 'workflow'
mockAppPermissionKeys = [AppACLPermission.Deploy]
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.deploy' })).toHaveAttribute(
'href',
'/app/app-1/deploy',
)
})
it.each([
{
label: 'the app is not a workflow app',
mode: 'chat',
permissionKeys: [AppACLPermission.Deploy],
},
{
label: 'app deploy ACL permission is missing',
mode: 'workflow',
permissionKeys: [AppACLPermission.Monitor],
},
])('should hide deploy navigation when $label', ({ mode, permissionKeys }) => {
// Arrange
mockAppMode = mode
mockAppPermissionKeys = permissionKeys
// Act
render(<AppDetailSection />)
// Assert
expect(screen.queryByRole('link', { name: 'common.appMenus.deploy' })).not.toBeInTheDocument()
})
it.each([AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT])(
'should render resource access navigation for %s apps when app access config permission is granted',
(mode) => {
// Arrange
mockAppMode = mode
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
// Act
render(<AppDetailSection />)
// Assert
expect(
screen.getByRole('link', { name: 'common.settings.resourceAccess' }),
).toHaveAttribute('href', '/app/app-1/access-config')
expect(
screen.queryByRole('link', { name: 'common.appMenus.overview' }),
).not.toBeInTheDocument()
},
)
it('should hide resource access navigation for Agent apps', () => {
// Arrange
mockAppMode = AppModeEnum.AGENT
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
// Act
render(<AppDetailSection />)
// Assert
expect(
screen.queryByRole('link', { name: 'common.settings.resourceAccess' }),
).not.toBeInTheDocument()
})
it('should hide resource access navigation when app access config permission is missing', () => {
// Act
render(<AppDetailSection />)
// Assert
expect(
screen.queryByRole('link', { name: 'common.settings.resourceAccess' }),
).not.toBeInTheDocument()
})
it('should hide resource access navigation when RBAC is disabled', () => {
// Arrange
mockIsRbacEnabled = false
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
// Act
render(<AppDetailSection />)
// Assert
expect(
screen.queryByRole('link', { name: 'common.settings.resourceAccess' }),
).not.toBeInTheDocument()
})
})
})