dify/web/app/components/access-rules-editor/__tests__/index.spec.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

350 lines
11 KiB
TypeScript

import type { AccessPolicyWithBindings, ResourceUserAccessSetting } from '@/models/access-control'
import type { Member } from '@/models/common'
import { fireEvent, render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import AccessRulesEditor from '../index'
const mockMembers = vi.hoisted(() => ({
accounts: [] as Member[] | null,
isLoading: false,
}))
vi.mock('@/service/use-common', () => ({
useMembers: vi.fn(() => ({
data: { accounts: mockMembers.accounts },
isLoading: mockMembers.isLoading,
})),
}))
const createRule = (resourceType: 'app' | 'dataset'): AccessPolicyWithBindings => ({
policy: {
id: `${resourceType}-policy-id`,
tenant_id: 'tenant-id',
resource_type: resourceType,
policy_key: `${resourceType}-policy-key`,
name: `${resourceType} policy`,
description: `${resourceType} policy description`,
permission_keys: [],
is_builtin: false,
category: 'global_custom',
created_at: '2026-05-22T00:00:00Z',
updated_at: '2026-05-22T00:00:00Z',
},
roles: [],
accounts: [],
})
const createUserAccessSetting = (): ResourceUserAccessSetting => ({
account: {
account_id: 'account-1',
account_name: 'Evan',
email: 'evan@example.com',
},
roles: [
{
id: 'role-1',
type: 'app',
category: 'global_custom',
name: 'Maintainer',
is_builtin: false,
permission_keys: [],
},
],
access_policies: [
{
id: 'app-policy-id',
tenant_id: 'tenant-id',
resource_type: 'app',
policy_key: 'app-policy-key',
name: 'Manage',
description: 'Can manage this app',
permission_keys: [],
is_builtin: false,
category: 'global_custom',
},
],
})
const createDefaultUserAccessSetting = (): ResourceUserAccessSetting => ({
...createUserAccessSetting(),
access_policies: [],
})
const createMember = (overrides: Partial<Member> = {}): Member =>
({
id: 'account-2',
name: 'Mia',
email: 'mia@example.com',
avatar: '',
avatar_url: '',
status: 'active',
role: 'normal',
roles: [],
...overrides,
}) as Member
describe('AccessRulesEditor', () => {
beforeEach(() => {
vi.clearAllMocks()
mockMembers.accounts = []
mockMembers.isLoading = false
})
it('should render loading state before empty or row content', () => {
render(
<AccessRulesEditor
rules={[]}
userAccessSettings={[]}
isLoadingRules
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
/>,
)
expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument()
expect(screen.queryByText('permission.accessRule.noUserAccessSettings')).not.toBeInTheDocument()
})
it('should render empty state when there are no user access settings', () => {
render(
<AccessRulesEditor
rules={[]}
userAccessSettings={[]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
/>,
)
expect(screen.getByText('permission.accessRule.noUserAccessSettings')).toBeInTheDocument()
})
it('should disable resource access controls before open scope is available', () => {
render(
<AccessRulesEditor
rules={[]}
userAccessSettings={[]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
isUpdatingOpenScope={false}
updatingAccountId={null}
/>,
)
expect(screen.getByText('permission.accessRule.resourceOpenScope')).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'permission.accessRule.resourceOpenScopeDescription' }),
).toBeInTheDocument()
const allMembersButton = screen.getByRole('button', {
name: /permission\.accessRule\.allPermittedMembers/,
})
const onlyMeButton = screen.getByRole('button', { name: /permission\.accessRule\.onlyMe/ })
const specificMembersButton = screen.getByRole('button', {
name: /permission\.accessRule\.specificMembersOnly/,
})
expect(allMembersButton).toBeDisabled()
expect(onlyMeButton).toBeDisabled()
expect(specificMembersButton).toBeDisabled()
expect(allMembersButton).toHaveAttribute('aria-pressed', 'false')
expect(onlyMeButton).toHaveAttribute('aria-pressed', 'false')
expect(specificMembersButton).toHaveAttribute('aria-pressed', 'false')
})
it('should render resource access controls and update account exceptions', () => {
const onOpenScopeChange = vi.fn()
const onUserAccessPoliciesChange = vi.fn()
const onRemoveAccessPolicyMemberBinding = vi.fn()
render(
<AccessRulesEditor
rules={[createRule('app')]}
userAccessSettings={[createUserAccessSetting()]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
onOpenScopeChange={onOpenScopeChange}
onUserAccessPoliciesChange={onUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={onRemoveAccessPolicyMemberBinding}
/>,
)
expect(screen.getByText('permission.accessRule.resourceOpenScope')).toBeInTheDocument()
expect(
screen.getByRole('button', { name: /permission\.accessRule\.specificMembersOnly/ }),
).toHaveAttribute('aria-pressed', 'true')
expect(
screen.getByText('permission.accessRule.individualPermissionSettings'),
).toBeInTheDocument()
expect(screen.getByText('Evan')).toBeInTheDocument()
expect(screen.getByText('evan@example.com')).toBeInTheDocument()
expect(screen.queryByText('Maintainer')).not.toBeInTheDocument()
expect(screen.getAllByText('Manage').length).toBeGreaterThan(0)
fireEvent.click(
screen.getByRole('button', { name: /permission\.accessRule\.allPermittedMembers/ }),
)
expect(onOpenScopeChange).not.toHaveBeenCalled()
expect(screen.getByText('permission.accessRule.changeOpenScopeTitle')).toBeInTheDocument()
expect(screen.getByText('permission.accessRule.changeOpenScopeDescription')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'common.operation.change' }))
expect(onOpenScopeChange).toHaveBeenCalledWith('all')
fireEvent.click(screen.getByRole('button', { name: 'common.operation.remove' }))
expect(onRemoveAccessPolicyMemberBinding).toHaveBeenCalledWith('account-1', 'app-policy-id')
})
it('should render and update the only-me resource access scope', () => {
const onOpenScopeChange = vi.fn()
render(
<AccessRulesEditor
rules={[]}
userAccessSettings={[]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="only_me"
isUpdatingOpenScope={false}
updatingAccountId={null}
onOpenScopeChange={onOpenScopeChange}
/>,
)
expect(screen.getByRole('button', { name: /permission\.accessRule\.onlyMe/ })).toHaveAttribute(
'aria-pressed',
'true',
)
fireEvent.click(
screen.getByRole('button', { name: /permission\.accessRule\.specificMembersOnly/ }),
)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.change' }))
expect(onOpenScopeChange).toHaveBeenCalledWith('specific')
})
it('should render the fixed default option when an account has no exception policy', () => {
render(
<AccessRulesEditor
rules={[createRule('app')]}
userAccessSettings={[createDefaultUserAccessSetting()]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
/>,
)
expect(screen.getByText('permission.accessRule.defaultPermission')).toBeInTheDocument()
})
it('should mark maintainer rows and prevent editing them', () => {
const onUserAccessPoliciesChange = vi.fn()
const onRemoveAccessPolicyMemberBinding = vi.fn()
render(
<AccessRulesEditor
rules={[createRule('app')]}
userAccessSettings={[createUserAccessSetting()]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
maintainerId="account-1"
onUserAccessPoliciesChange={onUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={onRemoveAccessPolicyMemberBinding}
/>,
)
expect(screen.getByText('permission.accessRule.maintainer')).toBeInTheDocument()
expect(screen.getByLabelText(/permission\.accessRule\.exceptionPermissionFor/)).toBeDisabled()
const removeButton = screen.getByRole('button', { name: 'common.operation.remove' })
expect(removeButton).toBeDisabled()
fireEvent.click(removeButton)
expect(onUserAccessPoliciesChange).not.toHaveBeenCalled()
expect(onRemoveAccessPolicyMemberBinding).not.toHaveBeenCalled()
})
it('should keep open scope unchanged when the confirmation is cancelled', () => {
const onOpenScopeChange = vi.fn()
render(
<AccessRulesEditor
rules={[]}
userAccessSettings={[]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
onOpenScopeChange={onOpenScopeChange}
/>,
)
fireEvent.click(
screen.getByRole('button', { name: /permission\.accessRule\.allPermittedMembers/ }),
)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
expect(onOpenScopeChange).not.toHaveBeenCalled()
})
it('should add unassigned members with default permission', async () => {
const user = userEvent.setup()
const onAddAccessSubject = vi.fn()
mockMembers.accounts = [
createMember({
id: 'account-1',
name: 'Evan',
email: 'evan@example.com',
}),
createMember(),
]
render(
<AccessRulesEditor
rules={[]}
userAccessSettings={[createUserAccessSetting()]}
isLoadingRules={false}
isLoadingUserAccessSettings={false}
openScope="specific"
isUpdatingOpenScope={false}
updatingAccountId={null}
onAddAccessSubject={onAddAccessSubject}
/>,
)
await user.click(screen.getByRole('button', { name: 'common.operation.add' }))
const dialog = screen.getByRole('dialog', { name: 'permission.accessRule.addMembersTitle' })
expect(within(dialog).getByText('Evan')).toBeInTheDocument()
expect(within(dialog).getByRole('button', { name: 'common.operation.added' })).toBeDisabled()
expect(
within(dialog).queryByRole('button', {
name: 'permission.accessRule.addMemberAria:{"name":"Evan"}',
}),
).not.toBeInTheDocument()
expect(within(dialog).getByText('Mia')).toBeInTheDocument()
expect(within(dialog).queryByRole('tablist')).not.toBeInTheDocument()
await user.click(
within(dialog).getByRole('button', {
name: 'permission.accessRule.addMemberAria:{"name":"Mia"}',
}),
)
expect(onAddAccessSubject).toHaveBeenCalledWith('account-2', ['default'])
})
})