mirror of
https://github.com/langgenius/dify.git
synced 2026-07-24 13:08:34 +08:00
350 lines
11 KiB
TypeScript
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'])
|
|
})
|
|
})
|