Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-bananas-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes app actions ignoring role filters and i18n translation
212 changes: 212 additions & 0 deletions apps/meteor/client/hooks/useApplyButtonFilters.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui';
import { UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook } from '@testing-library/react';

import { useApplyButtonAuthFilter } from './useApplyButtonFilters';

describe('useApplyButtonAuthFilter', () => {
describe('Role-based filtering', () => {
it('should filter button when user does not have required role (hasAllRoles)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasAllRoles: ['admin'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.build(),
});

expect(result.current(button)).toBe(false);
});

it('should show button when user has required role (hasAllRoles)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasAllRoles: ['admin'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withRole('admin').build(),
});

expect(result.current(button)).toBe(true);
});

it('should filter button when user does not have any required role (hasOneRole)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasOneRole: ['admin', 'moderator'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.build(),
});

expect(result.current(button)).toBe(false);
});

it('should show button when user has at least one of the required roles (hasOneRole)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasOneRole: ['admin', 'moderator'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withRole('moderator').build(),
});

expect(result.current(button)).toBe(true);
});

it('should show button when no role filter is specified', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.build(),
});

expect(result.current(button)).toBe(true);
});

it('should filter button when user is not logged in and role is required (hasAllRoles)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasAllRoles: ['admin'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withAnonymous().build(),
});

expect(result.current(button)).toBe(false);
});
});

describe('Permission-based filtering', () => {
it('should filter button when user does not have required permission (hasAllPermissions)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasAllPermissions: ['manage-apps'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().build(),
});

expect(result.current(button)).toBe(false);
});

it('should show button when user has required permission (hasAllPermissions)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasAllPermissions: ['manage-apps'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withPermission('manage-apps').build(),
});

expect(result.current(button)).toBe(true);
});

it('should show button when user has at least one of the required permissions (hasOnePermission)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasOnePermission: ['manage-apps', 'manage-users'],
},
};

const { result } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withPermission('manage-apps').build(),
});

expect(result.current(button)).toBe(true);
});
});

describe('Combined filters', () => {
it('should apply both role and permission filters (AND logic)', () => {
const button: IUIActionButton = {
appId: 'test-app',
actionId: 'test-action',
labelI18n: 'test_label',
context: UIActionButtonContext.USER_DROPDOWN_ACTION,
when: {
hasAllRoles: ['admin'],
hasAllPermissions: ['manage-apps'],
},
};

const { result: result1 } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.build(),
});
expect(result1.current(button)).toBe(false);

const { result: result2 } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot()
.withJohnDoe({ roles: ['user'] })
.withPermission('manage-apps')
.build(),
});
expect(result2.current(button)).toBe(false);

const { result: result3 } = renderHook(() => useApplyButtonAuthFilter(), {
wrapper: mockAppRoot().withJohnDoe().withRole('admin').withPermission('manage-apps').build(),
});
expect(result3.current(button)).toBe(true);
});
});
});
2 changes: 1 addition & 1 deletion apps/meteor/client/hooks/useApplyButtonFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const useApplyButtonAuthFilter = (): ((button: IUIActionButton) => boolea

const hasAllPermissionsResult = hasAllPermissions ? queryAllPermissions(hasAllPermissions)[1]() : true;
const hasOnePermissionResult = hasOnePermission ? queryAtLeastOnePermission(hasOnePermission)[1]() : true;
const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(role, room?._id)) : true;
const hasAllRolesResult = hasAllRoles ? !!uid && hasAllRoles.every((role) => queryRole(role, room?._id)[1]()) : true;
const hasOneRoleResult = hasOneRole ? !!uid && hasOneRole.some((role) => queryRole(role, room?._id)[1]()) : true;

return hasAllPermissionsResult && hasOnePermissionResult && hasAllRolesResult && hasOneRoleResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useAppActionButtons } from './useAppActionButtons';
import { useApplyButtonAuthFilter } from './useApplyButtonFilters';
import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTriggerTimeoutError';
import { Utilities } from '../../ee/lib/misc/Utilities';
import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager';

export const useUserDropdownAppsActionButtons = () => {
Expand All @@ -25,7 +26,7 @@ export const useUserDropdownAppsActionButtons = () => {
return {
id: `${action.appId}_${action.actionId}`,
// icon: action.icon as GenericMenuItemProps['icon'],
content: action.labelI18n,
content: t(Utilities.getI18nKeyForApp(action.labelI18n, action.appId)),
onClick: () => {
void actionManager
.emitInteraction(action.appId, {
Expand Down
Loading