feat: Add validation for Advanced Settings permissions using openedx-authz (#2869)
* feat: Add validation for Advanced Settings permissions using openedx-authz * squash!: Increase test coverage * squash!: Fix lint issues * squash!: Validate advanced settings permission on HelpSidebar * squash!: Increase test coverage * squash!: Attend PR comments
This commit is contained in:
@@ -6,6 +6,10 @@ import {
|
||||
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { useWaffleFlags } from '@src/data/apiHooks';
|
||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||
import { COURSE_PERMISSIONS } from '@src/authz/constants';
|
||||
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
|
||||
import Placeholder from '../editors/Placeholder';
|
||||
|
||||
import AlertProctoringError from '../generic/AlertProctoringError';
|
||||
@@ -41,6 +45,15 @@ const AdvancedSettings = () => {
|
||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
const waffleFlags = useWaffleFlags(courseId);
|
||||
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
|
||||
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
|
||||
canManageAdvancedSettings: {
|
||||
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
|
||||
scope: courseId,
|
||||
},
|
||||
}, isAuthzEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseAppSettings(courseId));
|
||||
dispatch(fetchProctoringExamErrors(courseId));
|
||||
@@ -52,7 +65,7 @@ const AdvancedSettings = () => {
|
||||
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
|
||||
const loadingSettingsStatus = useSelector(getLoadingStatus);
|
||||
|
||||
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS;
|
||||
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS || (isAuthzEnabled && isLoadingUserPermissions);
|
||||
const updateSettingsButtonState = {
|
||||
labels: {
|
||||
default: intl.formatMessage(messages.buttonSaveText),
|
||||
@@ -128,6 +141,15 @@ const AdvancedSettings = () => {
|
||||
showSaveSettingsPrompt(true);
|
||||
};
|
||||
|
||||
// Show permission denied alert when authz is enabled and user doesn't have permission
|
||||
const authzIsEnabledAndNoPermission = isAuthzEnabled
|
||||
&& !isLoadingUserPermissions
|
||||
&& !userPermissions?.canManageAdvancedSettings;
|
||||
|
||||
if (authzIsEnabledAndNoPermission) {
|
||||
return <PermissionDeniedAlert />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" className="advanced-settings px-4">
|
||||
@@ -192,8 +214,8 @@ const AdvancedSettings = () => {
|
||||
defaultMessage="{visibility} deprecated settings"
|
||||
values={{
|
||||
visibility:
|
||||
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
|
||||
: intl.formatMessage(messages.deprecatedButtonShowText),
|
||||
showDeprecated ? intl.formatMessage(messages.deprecatedButtonHideText)
|
||||
: intl.formatMessage(messages.deprecatedButtonShowText),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
|
||||
import {
|
||||
render as baseRender,
|
||||
fireEvent,
|
||||
@@ -21,11 +23,15 @@ const courseId = '123';
|
||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
<textarea
|
||||
{...props}
|
||||
onFocus={() => {}}
|
||||
onBlur={() => {}}
|
||||
onFocus={() => { }}
|
||||
onBlur={() => { }}
|
||||
/>
|
||||
)));
|
||||
|
||||
jest.mock('@src/authz/data/apiHooks', () => ({
|
||||
useUserPermissions: jest.fn(),
|
||||
}));
|
||||
|
||||
const render = () => baseRender(
|
||||
<CourseAuthoringProvider courseId={courseId}>
|
||||
<AdvancedSettings />
|
||||
@@ -41,6 +47,11 @@ describe('<AdvancedSettings />', () => {
|
||||
axiosMock
|
||||
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
||||
.reply(200, advancedSettingsMock);
|
||||
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: true },
|
||||
});
|
||||
});
|
||||
it('should render without errors', async () => {
|
||||
const { getByText } = render();
|
||||
@@ -144,4 +155,38 @@ describe('<AdvancedSettings />', () => {
|
||||
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
|
||||
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render without errors when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: true },
|
||||
});
|
||||
const { getByText } = render();
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
|
||||
selector: 'h2.sub-header-title',
|
||||
});
|
||||
expect(advancedSettingsElement).toBeInTheDocument();
|
||||
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should show permission alert when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: false },
|
||||
});
|
||||
const { getByTestId } = render();
|
||||
await waitFor(() => {
|
||||
const permissionAlert = getByTestId('permissionDeniedAlert');
|
||||
expect(permissionAlert).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,3 +14,7 @@ export const CONTENT_LIBRARY_PERMISSIONS = {
|
||||
MANAGE_LIBRARY_TEAM: 'content_libraries.manage_library_team',
|
||||
VIEW_LIBRARY_TEAM: 'content_libraries.view_library_team',
|
||||
};
|
||||
|
||||
export const COURSE_PERMISSIONS = {
|
||||
MANAGE_ADVANCED_SETTINGS: 'courses.manage_advanced_settings',
|
||||
};
|
||||
|
||||
@@ -92,6 +92,7 @@ export const waffleFlagDefaults = {
|
||||
useNewGroupConfigurationsPage: true,
|
||||
useReactMarkdownEditor: true,
|
||||
useVideoGalleryFlow: false,
|
||||
enableAuthzCourseAuthoring: false,
|
||||
} as const;
|
||||
|
||||
export type WaffleFlagName = keyof typeof waffleFlagDefaults;
|
||||
|
||||
@@ -4,6 +4,8 @@ import classNames from 'classnames';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||
import { COURSE_PERMISSIONS } from '@src/authz/constants';
|
||||
import { useWaffleFlags } from '../../data/apiHooks';
|
||||
import { otherLinkURLParams } from './constants';
|
||||
import messages from './messages';
|
||||
@@ -25,7 +27,7 @@ const HelpSidebar = ({
|
||||
scheduleAndDetails,
|
||||
groupConfigurations,
|
||||
} = otherLinkURLParams;
|
||||
const waffleFlags = useWaffleFlags();
|
||||
const waffleFlags = useWaffleFlags(courseId);
|
||||
|
||||
const showOtherLink = (params) => !pathname.includes(params);
|
||||
const generateLegacyURL = (urlParameter) => {
|
||||
@@ -39,6 +41,26 @@ const HelpSidebar = ({
|
||||
const advancedSettingsDestination = generateLegacyURL(advancedSettings);
|
||||
const groupConfigurationsDestination = generateLegacyURL(groupConfigurations);
|
||||
|
||||
/*
|
||||
AuthZ for Course Authoring
|
||||
If authz.enable_course_authoring flag is enabled, validate permissions using AuthZ API.
|
||||
*/
|
||||
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
|
||||
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
|
||||
canManageAdvancedSettings: {
|
||||
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
|
||||
scope: courseId,
|
||||
},
|
||||
}, isAuthzEnabled);
|
||||
|
||||
// If it's still loading, don't show the Advanced Settings link, otherwise, use the permission to decide
|
||||
const authzCanManageAdvancedSettings = isLoadingUserPermissions
|
||||
? false
|
||||
: !!userPermissions?.canManageAdvancedSettings;
|
||||
|
||||
// When authz is enabled, use permission, otherwise it's always allowed (legacy behavior)
|
||||
const canManageAdvancedSettings = isAuthzEnabled ? authzCanManageAdvancedSettings : true;
|
||||
|
||||
return (
|
||||
<aside className={classNames('help-sidebar', className)}>
|
||||
<div className="help-sidebar-about">{children}</div>
|
||||
@@ -90,7 +112,7 @@ const HelpSidebar = ({
|
||||
isNewPage={waffleFlags.useNewGroupConfigurationsPage}
|
||||
/>
|
||||
)}
|
||||
{showOtherLink(advancedSettings) && (
|
||||
{showOtherLink(advancedSettings) && canManageAdvancedSettings && (
|
||||
<HelpSidebarLink
|
||||
pathToPage={waffleFlags.useNewAdvancedSettingsPage
|
||||
? `/course/${courseId}/${advancedSettings}` : advancedSettingsDestination}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
|
||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||
import { initializeMocks, render } from '../../testUtils';
|
||||
import messages from './messages';
|
||||
import { HelpSidebar } from '.';
|
||||
|
||||
jest.mock('@src/authz/data/apiHooks', () => ({
|
||||
useUserPermissions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockPathname = '/foo-bar';
|
||||
|
||||
const renderHelpSidebar = (props) => render(
|
||||
@@ -22,6 +29,11 @@ const props = {
|
||||
describe('HelpSidebar', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
// @ts-ignore
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders children correctly', () => {
|
||||
@@ -56,4 +68,41 @@ describe('HelpSidebar', () => {
|
||||
const { getByText } = renderHelpSidebar(initialProps);
|
||||
expect(getByText(messages.sidebarLinkToProctoredExamSettings.defaultMessage)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the advanced settings sidebar link when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
// @ts-ignore
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: true },
|
||||
});
|
||||
const { queryByText } = renderHelpSidebar(props);
|
||||
await waitFor(() => expect(queryByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeTruthy());
|
||||
});
|
||||
it('should not render the advanced settings sidebar link when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
// @ts-ignore
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: false },
|
||||
});
|
||||
const { queryByText } = renderHelpSidebar(props);
|
||||
await waitFor(() => expect(queryByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeFalsy());
|
||||
});
|
||||
it('should not render the advanced settings sidebar link when authz.enable_course_authoring flag is enabled and the permissions are still loading', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
// @ts-ignore
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: true,
|
||||
data: undefined,
|
||||
});
|
||||
const { queryByText } = renderHelpSidebar(props);
|
||||
await waitFor(() => expect(queryByText(messages.sidebarLinkToAdvancedSettings.defaultMessage)).toBeFalsy());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactNode } from 'react';
|
||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
|
||||
import messages from './messages';
|
||||
import {
|
||||
useContentMenuItems, useToolsMenuItems, useSettingMenuItems, useLibrarySettingsMenuItems, useLibraryToolsMenuItems,
|
||||
} from './hooks';
|
||||
import { mockWaffleFlags } from '../data/apiHooks.mock';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
@@ -27,6 +30,26 @@ jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@src/authz/data/apiHooks', () => ({
|
||||
useUserPermissions: jest.fn(),
|
||||
}));
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
describe('header utils', () => {
|
||||
describe('getContentMenuItems', () => {
|
||||
it('when video upload page enabled should include Video Uploads option', () => {
|
||||
@@ -37,7 +60,7 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
|
||||
});
|
||||
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
|
||||
const actualItems = renderHook(() => useContentMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
|
||||
expect(actualItems).toHaveLength(5);
|
||||
});
|
||||
it('when video upload page disabled should not include Video Uploads option', () => {
|
||||
@@ -48,14 +71,14 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
|
||||
});
|
||||
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
|
||||
const actualItems = renderHook(() => useContentMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
|
||||
expect(actualItems).toHaveLength(4);
|
||||
});
|
||||
it('adds course libraries link to content menu when libraries v2 is enabled', () => {
|
||||
jest.mocked(useSelector).mockReturnValue({
|
||||
librariesV2Enabled: true,
|
||||
});
|
||||
const actualItems = renderHook(() => useContentMenuItems('course-123')).result.current;
|
||||
const actualItems = renderHook(() => useContentMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
|
||||
expect(actualItems[1]).toEqual({ href: '/course/course-123/libraries', title: 'Library Updates' });
|
||||
});
|
||||
});
|
||||
@@ -65,6 +88,10 @@ describe('header utils', () => {
|
||||
jest.mocked(useSelector).mockReturnValue({
|
||||
canAccessAdvancedSettings: true,
|
||||
});
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: true },
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('when certificate page enabled should include certificates option', () => {
|
||||
@@ -72,7 +99,7 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_CERTIFICATE_PAGE: 'true',
|
||||
});
|
||||
const actualItems = renderHook(() => useSettingMenuItems('course-123')).result.current;
|
||||
const actualItems = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
|
||||
expect(actualItems).toHaveLength(6);
|
||||
});
|
||||
it('when certificate page disabled should not include certificates option', () => {
|
||||
@@ -80,18 +107,62 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_CERTIFICATE_PAGE: 'false',
|
||||
});
|
||||
const actualItems = renderHook(() => useSettingMenuItems('course-123')).result.current;
|
||||
const actualItems = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
|
||||
expect(actualItems).toHaveLength(5);
|
||||
});
|
||||
it('when user has access to advanced settings should include advanced settings option', () => {
|
||||
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123')).result.current.map((item) => item.title);
|
||||
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).toContain('Advanced Settings');
|
||||
});
|
||||
it('when user has no access to advanced settings should not include advanced settings option', () => {
|
||||
jest.mocked(useSelector).mockReturnValue({ canAccessAdvancedSettings: false });
|
||||
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123')).result.current.map((item) => item.title);
|
||||
const actualItemsTitle = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).not.toContain('Advanced Settings');
|
||||
});
|
||||
|
||||
it('when the authz.enable_course_authoring flag is enabled and user has access to advanced settings should include advanced settings option', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: true },
|
||||
} as any);
|
||||
const { result } = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() });
|
||||
await waitFor(() => {
|
||||
const actualItemsTitle = result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).toContain('Advanced Settings');
|
||||
});
|
||||
});
|
||||
it('when authz.enable_course_authoring flag is enabled and user has no access to advanced settings should not include advanced settings option', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: false,
|
||||
data: { canManageAdvancedSettings: false },
|
||||
} as any);
|
||||
const { result } = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() });
|
||||
await waitFor(() => {
|
||||
const actualItemsTitle = result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).not.toContain('Advanced Settings');
|
||||
});
|
||||
});
|
||||
|
||||
it('when authz.enable_course_authoring flag is enabled and the permission request is still loading, should not include advanced settings option', async () => {
|
||||
// Mock feature flag
|
||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
||||
jest.mocked(useUserPermissions).mockReturnValue({
|
||||
isLoading: true,
|
||||
data: undefined,
|
||||
} as any);
|
||||
const { result } = renderHook(() => useSettingMenuItems('course-123'), { wrapper: createWrapper() });
|
||||
await waitFor(() => {
|
||||
const actualItemsTitle = result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).not.toContain('Advanced Settings');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToolsMenuItems', () => {
|
||||
@@ -100,7 +171,7 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
});
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).toEqual([
|
||||
'Import',
|
||||
'Export Course',
|
||||
@@ -113,7 +184,7 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
|
||||
});
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).toEqual([
|
||||
'Import',
|
||||
'Export Course',
|
||||
@@ -125,7 +196,7 @@ describe('header utils', () => {
|
||||
mockWaffleFlags({
|
||||
enableCourseOptimizer: true,
|
||||
});
|
||||
const optimizerItem = renderHook(() => useToolsMenuItems('course-123')).result.current.find(
|
||||
const optimizerItem = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.find(
|
||||
item => item.href === '/course/course-123/optimizer',
|
||||
);
|
||||
expect(optimizerItem).toBeDefined();
|
||||
@@ -135,14 +206,14 @@ describe('header utils', () => {
|
||||
mockWaffleFlags({
|
||||
enableCourseOptimizer: false,
|
||||
});
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123')).result.current.map((item) => item.title);
|
||||
const actualItemsTitle = renderHook(() => useToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current.map((item) => item.title);
|
||||
expect(actualItemsTitle).not.toContain(messages['header.links.optimizer'].defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLibrarySettingsMenuItems', () => {
|
||||
it('should contain team access url', () => {
|
||||
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current;
|
||||
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false), { wrapper: createWrapper() }).result.current;
|
||||
expect(items).toContainEqual({ title: 'Library Team', href: 'http://localhost/?sa=manage-team' });
|
||||
});
|
||||
it('should contain admin console url if set', () => {
|
||||
@@ -150,7 +221,7 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ADMIN_CONSOLE_URL: 'http://admin-console.com',
|
||||
});
|
||||
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false)).result.current;
|
||||
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', false), { wrapper: createWrapper() }).result.current;
|
||||
expect(items).toContainEqual({
|
||||
title: 'Library Team',
|
||||
href: 'http://admin-console.com/authz/libraries/library-123',
|
||||
@@ -161,7 +232,7 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ADMIN_CONSOLE_URL: 'http://admin-console.com',
|
||||
});
|
||||
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', true)).result.current;
|
||||
const items = renderHook(() => useLibrarySettingsMenuItems('library-123', true), { wrapper: createWrapper() }).result.current;
|
||||
expect(items).toContainEqual({
|
||||
title: 'Library Team',
|
||||
href: 'http://admin-console.com/authz/libraries/library-123',
|
||||
@@ -171,7 +242,7 @@ describe('header utils', () => {
|
||||
|
||||
describe('useLibraryToolsMenuItems', () => {
|
||||
it('should contain backup and import url', () => {
|
||||
const items = renderHook(() => useLibraryToolsMenuItems('course-123')).result.current;
|
||||
const items = renderHook(() => useLibraryToolsMenuItems('course-123'), { wrapper: createWrapper() }).result.current;
|
||||
expect(items).toContainEqual({
|
||||
href: '/library/course-123/backup',
|
||||
title: 'Back up to local archive',
|
||||
@@ -9,6 +9,9 @@ import { getStudioHomeData } from '@src/studio-home/data/selectors';
|
||||
import courseOptimizerMessages from '@src/optimizer-page/messages';
|
||||
import { SidebarActions } from '@src/library-authoring/common/context/SidebarContext';
|
||||
import { LibQueryParamKeys } from '@src/library-authoring/routes';
|
||||
|
||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||
import { COURSE_PERMISSIONS } from '@src/authz/constants';
|
||||
import messages from './messages';
|
||||
|
||||
export const useContentMenuItems = (courseId: string) => {
|
||||
@@ -55,8 +58,29 @@ export const useContentMenuItems = (courseId: string) => {
|
||||
export const useSettingMenuItems = (courseId: string) => {
|
||||
const intl = useIntl();
|
||||
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
const { canAccessAdvancedSettings } = useSelector(getStudioHomeData);
|
||||
const waffleFlags = useWaffleFlags();
|
||||
const { canAccessAdvancedSettings: legacyCanAccessAdvancedSettings } = useSelector(getStudioHomeData);
|
||||
const waffleFlags = useWaffleFlags(courseId);
|
||||
|
||||
/*
|
||||
AuthZ for Course Authoring
|
||||
If authz.enable_course_authoring flag is enabled, validate permissions using AuthZ API.
|
||||
Otherwise, fallback to existing logic.
|
||||
*/
|
||||
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
|
||||
const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
|
||||
canManageAdvancedSettings: {
|
||||
action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
|
||||
scope: courseId,
|
||||
},
|
||||
}, isAuthzEnabled);
|
||||
|
||||
const authzCanManageAdvancedSettings = isLoadingUserPermissions
|
||||
? false
|
||||
: userPermissions?.canManageAdvancedSettings || false;
|
||||
|
||||
const canAccessAdvancedSettings = isAuthzEnabled
|
||||
? authzCanManageAdvancedSettings
|
||||
: legacyCanAccessAdvancedSettings;
|
||||
|
||||
const items = [
|
||||
{
|
||||
@@ -75,7 +99,7 @@ export const useSettingMenuItems = (courseId: string) => {
|
||||
href: waffleFlags.useNewGroupConfigurationsPage ? `/course/${courseId}/group_configurations` : `${studioBaseUrl}/group_configurations/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
||||
},
|
||||
...(canAccessAdvancedSettings === true
|
||||
...(canAccessAdvancedSettings
|
||||
? [{
|
||||
href: waffleFlags.useNewAdvancedSettingsPage ? `/course/${courseId}/settings/advanced` : `${studioBaseUrl}/settings/advanced/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
||||
|
||||
Reference in New Issue
Block a user