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
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';

import ABACUpsellModal from './ABACUpsellModal';

// Mock the hooks used by ABACUpsellModal
jest.mock('../../../hooks/useHasLicenseModule', () => ({
useHasLicenseModule: jest.fn(() => false),
}));
import { createFakeLicenseInfo } from '../../../../tests/mocks/data';

jest.mock('../../GenericUpsellModal/hooks', () => ({
useUpsellActions: jest.fn(() => ({
Expand All @@ -34,6 +30,9 @@ const appRoot = mockAppRoot()
Upgrade: 'Upgrade',
Cancel: 'Cancel',
})
.withEndpoint('GET', '/v1/licenses.info', async () => ({
license: createFakeLicenseInfo(),
}))
.build();

describe('ABACUpsellModal', () => {
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/client/lib/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const links = {
trial: `${GO_ROCKET_CHAT_PREFIX}/i/docs-trial`,
versionSupport: `${GO_ROCKET_CHAT_PREFIX}/i/version-support`,
updateProduct: `${GO_ROCKET_CHAT_PREFIX}/i/update-product`,
// TODO: implement abac links when available
abacDocs: `${GO_ROCKET_CHAT_PREFIX}/i/TODO-ABAC-DOCS`,
abacLicenseRenewalUrl: `${GO_ROCKET_CHAT_PREFIX}/i/TODO-ABAC-LICENSE-RENEWAL-URL`,
abacLDAPDocs: `${GO_ROCKET_CHAT_PREFIX}/i/TODO-ABAC-LDAP-DOCS`,
},
/** @deprecated use `go.rocket.chat` links */
desktopAppDownload: 'https://rocket.chat/download',
Expand Down
47 changes: 47 additions & 0 deletions apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box, Button, Callout } from '@rocket.chat/fuselage';
import { useRouteParameter } from '@rocket.chat/ui-contexts';
import { Trans, useTranslation } from 'react-i18next';

import AdminABACSettings from './AdminABACSettings';
import AdminABACTabs from './AdminABACTabs';
import { Page, PageContent, PageHeader } from '../../../components/Page';
import { useExternalLink } from '../../../hooks/useExternalLink';
import { links } from '../../../lib/links';

type AdminABACPageProps = {
shouldShowWarning: boolean;
};

const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => {
const { t } = useTranslation();
const tab = useRouteParameter('tab');
const learnMore = useExternalLink();

return (
<Page flexDirection='row'>
<Page>
<PageHeader title={t('ABAC')}>
<Button icon='new-window' secondary onClick={() => learnMore(links.go.abacDocs)}>
{t('ABAC_Learn_More')}
</Button>
</PageHeader>
{shouldShowWarning && (
<Box mi={24} mb={16}>
<Callout type='warning' title={t('ABAC_automatically_disabled_callout')}>
<Trans i18nKey='ABAC_automatically_disabled_callout_description'>
Renew your license to continue using all{' '}
<a href={links.go.abacLicenseRenewalUrl} rel='noopener noreferrer' target='_blank'>
ABAC capabilities without restriction.
</a>
</Trans>
</Callout>
</Box>
)}
<AdminABACTabs />
<PageContent>{tab === 'settings' && <AdminABACSettings />}</PageContent>
</Page>
</Page>
);
};

export default AdminABACPage;
64 changes: 64 additions & 0 deletions apps/meteor/client/views/admin/ABAC/AdminABACRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { usePermission, useSetModal, useCurrentModal, useRouter, useRouteParameter, useSettingStructure } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { memo, useEffect, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';

import AdminABACPage from './AdminABACPage';
import ABACUpsellModal from '../../../components/ABAC/ABACUpsellModal/ABACUpsellModal';
import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks';
import PageSkeleton from '../../../components/PageSkeleton';
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';
import SettingsProvider from '../../../providers/SettingsProvider';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
import EditableSettingsProvider from '../settings/EditableSettingsProvider';

const AdminABACRoute = (): ReactElement => {
const { t } = useTranslation();
// TODO: Check what permission is needed to view the ABAC page
const canViewABACPage = usePermission('abac-management');
const hasABAC = useHasLicenseModule('abac') === true;
const isModalOpen = !!useCurrentModal();
const tab = useRouteParameter('tab');
const router = useRouter();

// Check if setting exists in the DB to decide if we show warning or upsell
const ABACEnabledSetting = useSettingStructure('ABAC_Enabled');

useLayoutEffect(() => {
if (!tab) {
router.navigate({
name: 'admin-ABAC',
params: { tab: 'settings' },
});
}
}, [tab, router]);

const { shouldShowUpsell, handleManageSubscription } = useUpsellActions(hasABAC);

const setModal = useSetModal();

useEffect(() => {
// WS has never activated ABAC
if (shouldShowUpsell && ABACEnabledSetting === undefined) {
setModal(<ABACUpsellModal onClose={() => setModal(null)} onConfirm={handleManageSubscription} />);
}
}, [shouldShowUpsell, setModal, t, handleManageSubscription, ABACEnabledSetting]);

if (isModalOpen) {
return <PageSkeleton />;
}

if (!canViewABACPage || (ABACEnabledSetting === undefined && !hasABAC)) {
return <NotAuthorizedPage />;
}

return (
<SettingsProvider>
<EditableSettingsProvider>
<AdminABACPage shouldShowWarning={ABACEnabledSetting !== undefined && !hasABAC} />
</EditableSettingsProvider>
</SettingsProvider>
);
};

export default memo(AdminABACRoute);
142 changes: 142 additions & 0 deletions apps/meteor/client/views/admin/ABAC/AdminABACSettingToggle.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Import ISetting from the correct package.

The import path for ISetting should be from @rocket.chat/core-typings, not from @rocket.chat/apps-engine/definition/settings.

Apply this diff to fix the import:

-import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
+import type { ISetting } from '@rocket.chat/core-typings';
🤖 Prompt for AI Agents
In apps/meteor/client/views/admin/ABAC/AdminABACSettingToggle.spec.tsx around
line 1, the ISetting type is imported from the wrong package; replace the import
from '@rocket.chat/apps-engine/definition/settings' with an import from
'@rocket.chat/core-typings' so the file imports ISetting from the correct
package.

import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';

import AdminABACSettingToggle from './AdminABACSettingToggle';
import EditableSettingsProvider from '../settings/EditableSettingsProvider';

const settingStructure = {
packageValue: false,
blocked: false,
public: true,
type: 'boolean',
i18nLabel: 'ABAC_Enabled',
i18nDescription: 'ABAC_Enabled_Description',
} as Partial<ISetting>;

const baseAppRoot = mockAppRoot()
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
.withTranslations('en', 'core', {
ABAC_Enabled: 'Enable ABAC',
ABAC_Enabled_Description: 'Enable Attribute-Based Access Control',
ABAC_Warning_Modal_Title: 'Disable ABAC',
ABAC_Warning_Modal_Confirm_Text: 'Disable',
Cancel: 'Cancel',
});

describe('AdminABACSettingToggle', () => {
it('should render the setting toggle when setting exists', () => {
const { baseElement } = render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
expect(baseElement).toMatchSnapshot();
});

it('should show warning modal when disabling ABAC', async () => {
const user = userEvent.setup();
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

const toggle = screen.getByRole('checkbox');
await waitFor(() => {
expect(toggle).not.toBeDisabled();
});
await user.click(toggle);

await waitFor(() => {
expect(screen.getByText('Disable ABAC')).toBeInTheDocument();
});

// TODO: discover how to automatically unmount all modals after each test
const cancelButton = screen.getByRole('button', { name: /cancel/i });
await user.click(cancelButton);
});
Comment on lines +37 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Implement proper modal cleanup to ensure test isolation.

The TODO comments at lines 54 and 85 highlight a test isolation concern. Manually dismissing modals in each test is fragile and could cause test pollution if the modal persists between tests.

Consider implementing an afterEach hook to clean up modals:

afterEach(() => {
  // Close any open modals
  const cancelButtons = screen.queryAllByRole('button', { name: /cancel/i });
  cancelButtons.forEach(button => button.click());
});

Alternatively, investigate if the mockAppRoot builder or testing library provides a cleanup mechanism for portaled elements.

Also applies to: 71-87

🤖 Prompt for AI Agents
In apps/meteor/client/views/admin/ABAC/AdminABACSettingToggle.spec.tsx around
lines 37 to 56 (and similarly 71 to 87), tests manually dismiss modals which
risks test pollution; add an afterEach hook in this spec file that reliably
cleans up any open modal before each test ends by either calling the
testing-library cleanup/unmount helpers or by querying for modal close/cancel
buttons and programmatically clicking them, ensuring portaled elements are
removed so each test runs in isolation.


it('should not show warning modal when enabling ABAC', async () => {
const user = userEvent.setup();
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
});

const toggle = screen.getByRole('checkbox');
await user.click(toggle);

// The modal should not appear when enabling ABAC
expect(screen.queryByText('Disable ABAC')).not.toBeInTheDocument();
});

it('should show warning modal when resetting setting', async () => {
const user = userEvent.setup();
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

const resetButton = screen.getByRole('button', { name: /reset/i });
await user.click(resetButton);

await waitFor(() => {
expect(screen.getByText('Disable ABAC')).toBeInTheDocument();
});

// TODO: discover how to automatically unmount all modals after each test
const cancelButton = screen.getByRole('button', { name: /cancel/i });
await user.click(cancelButton);
});
Comment on lines +37 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add test coverage for the confirmation flow.

The existing tests verify that the warning modal appears when disabling or resetting, but they don't test the actual confirmation path. Consider adding a test that clicks the "Disable" button in the modal and verifies the setting is actually changed.

Add a test case like:

it('should disable ABAC when confirmation button is clicked', async () => {
  const user = userEvent.setup();
  render(<AdminABACSettingToggle hasABAC={true} />, {
    wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
  });

  const toggle = screen.getByRole('checkbox');
  await user.click(toggle);

  await waitFor(() => {
    expect(screen.getByText('Disable ABAC')).toBeInTheDocument();
  });

  const disableButton = screen.getByRole('button', { name: /disable/i });
  await user.click(disableButton);

  // Verify the setting was actually changed
  await waitFor(() => {
    expect(toggle).not.toBeChecked();
  });
});
🤖 Prompt for AI Agents
In apps/meteor/client/views/admin/ABAC/AdminABACSettingToggle.spec.tsx around
lines 37-87, add a new test that exercises the confirmation path: render the
component with ABAC_Enabled true, open the disable modal by clicking the
checkbox (or reset), wait for the "Disable ABAC" modal, find and click the
modal's confirm ("Disable") button, then wait and assert the toggle is no longer
checked (or the underlying setting changed); mirror the same pattern for the
reset flow if desired and ensure modal dismissal/cleanup after the assertion.


it('should have no accessibility violations', async () => {
const { container } = render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should handle setting change correctly', async () => {
const user = userEvent.setup();
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
});

const toggle = await screen.findByRole('checkbox', { busy: false });
expect(toggle).not.toBeChecked();

await user.click(toggle);
expect(toggle).toBeChecked();
});

it('should be disabled when abac license is not installed', () => {
const { baseElement } = render(<AdminABACSettingToggle hasABAC={false} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

const toggle = screen.getByRole('checkbox');
expect(toggle).toBeDisabled();
expect(baseElement).toMatchSnapshot();
});

it('should show skeleton when loading', () => {
const { baseElement } = render(<AdminABACSettingToggle hasABAC='loading' />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
expect(baseElement).toMatchSnapshot();
});

it('should show reset button when value differs from package value', () => {
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

expect(screen.getByRole('button', { name: /reset/i })).toBeInTheDocument();
});

it('should not show reset button when value matches package value', () => {
render(<AdminABACSettingToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
});

expect(screen.queryByRole('button', { name: /reset/i })).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { Meta, StoryObj } from '@storybook/react';

import AdminABACSettingToggle from './AdminABACSettingToggle';
import EditableSettingsProvider from '../settings/EditableSettingsProvider';

const meta: Meta<typeof AdminABACSettingToggle> = {
title: 'Admin/ABAC/AdminABACSettingToggle',
component: AdminABACSettingToggle,
parameters: {
layout: 'padded',
},
decorators: [
(Story) => {
const AppRoot = mockAppRoot()
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
.withTranslations('en', 'core', {
ABAC_Enabled: 'Enable ABAC',
ABAC_Enabled_Description: 'Enable Attribute-Based Access Control',
ABAC_Warning_Modal_Title: 'Disable ABAC',
ABAC_Warning_Modal_Confirm_Text: 'Disable',
Cancel: 'Cancel',
})
.withSetting('ABAC_Enabled', true, {
packageValue: false,
blocked: false,
public: true,
type: 'boolean',
i18nLabel: 'ABAC_Enabled',
i18nDescription: 'ABAC_Enabled_Description',
})
.build();

return (
<AppRoot>
<Story />
</AppRoot>
);
},
],
args: {
hasABAC: true,
},
};

export default meta;
type Story = StoryObj<typeof AdminABACSettingToggle>;

export const Default: Story = {
args: {
hasABAC: true,
},
};

export const Loading: Story = {
args: {
hasABAC: 'loading',
},
};

export const False: Story = {
args: {
hasABAC: false,
},
};
Loading
Loading