Skip to content

Commit 6ee7298

Browse files
committed
feat(pages-and-resources): add permission tests for grading and pages/resources access
1 parent bf1f32a commit 6ee7298

9 files changed

Lines changed: 381 additions & 12 deletions

File tree

src/CourseAuthoringPage.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,20 @@ describe('Course authoring page', () => {
108108

109109
axiosMock.onGet(
110110
`${courseAppsApiUrl}/${courseId}`,
111-
).reply(403);
111+
).reply(403, { response: { status: 403 } });
112112
await executeThunk(fetchCourseApps(courseId), store.dispatch);
113113
};
114114
test('renders PermissionDeniedAlert when courseAppsApiStatus is DENIED', async () => {
115115
mockPathname = '/editor/';
116116
await mockStoreDenied();
117117

118-
const wrapper = renderComponent(<CourseAuthoringPage />);
118+
// Test PagesAndResources (which has the PermissionDeniedAlert logic),
119+
// not CourseAuthoringPage which is just the layout wrapper
120+
const wrapper = renderComponent(
121+
<CourseAuthoringPage>
122+
<PagesAndResources />
123+
</CourseAuthoringPage>,
124+
);
119125
expect(await wrapper.findByTestId('permissionDeniedAlert')).toBeInTheDocument();
120126
});
121127
});

src/advanced-settings/setting-card/SettingCard.test.jsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fireEvent, render, waitFor } from '@testing-library/react';
1+
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import { IntlProvider } from '@edx/frontend-platform/i18n';
44

@@ -25,7 +25,7 @@ jest.mock('react-textarea-autosize', () =>
2525
/>
2626
)));
2727

28-
const RootWrapper = () => (
28+
const RootWrapper = (props = {}) => (
2929
<IntlProvider locale="en">
3030
<SettingCard
3131
isOn
@@ -37,6 +37,7 @@ const RootWrapper = () => (
3737
handleBlur={handleBlur}
3838
isEditableState
3939
saveSettingsPrompt={false}
40+
{...props}
4041
/>
4142
</IntlProvider>
4243
);
@@ -91,4 +92,16 @@ describe('<SettingCard />', () => {
9192
expect(handleBlur).toHaveBeenCalled();
9293
});
9394
});
95+
it('renders in readOnly mode with disabled input', () => {
96+
render(<RootWrapper readOnly />);
97+
const input = screen.getByLabelText(/Setting Name/i);
98+
expect(input).toBeDisabled();
99+
});
100+
it('shows help popup when clicking info button', () => {
101+
render(<RootWrapper />);
102+
const helpButton = screen.getByRole('button', { name: /show help text/i });
103+
fireEvent.click(helpButton);
104+
// The help text should be visible in the popup - verify component renders help
105+
expect(screen.queryByText(/This is a help message/i)).toBeInTheDocument();
106+
});
94107
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { getGradingPermissions, getPagesAndResourcesPermissions } from './permissionHelpers';
2+
import { COURSE_PERMISSIONS } from './constants';
3+
4+
describe('permissionHelpers', () => {
5+
const courseId = 'course-v1:org+course+run';
6+
7+
describe('getGradingPermissions', () => {
8+
it('returns correct permission structure with VIEW action', () => {
9+
const result = getGradingPermissions(courseId);
10+
11+
expect(result.canViewGradingSettings).toBeDefined();
12+
expect(result.canViewGradingSettings.action).toBe(COURSE_PERMISSIONS.VIEW_GRADING_SETTINGS);
13+
expect(result.canViewGradingSettings.scope).toBe(courseId);
14+
});
15+
16+
it('returns correct permission structure with EDIT action', () => {
17+
const result = getGradingPermissions(courseId);
18+
19+
expect(result.canEditGradingSettings).toBeDefined();
20+
expect(result.canEditGradingSettings.action).toBe(COURSE_PERMISSIONS.EDIT_GRADING_SETTINGS);
21+
expect(result.canEditGradingSettings.scope).toBe(courseId);
22+
});
23+
24+
it('returns both VIEW and EDIT permissions in single call', () => {
25+
const result = getGradingPermissions(courseId);
26+
27+
const permissions = Object.keys(result);
28+
expect(permissions).toContain('canViewGradingSettings');
29+
expect(permissions).toContain('canEditGradingSettings');
30+
});
31+
32+
it('uses correct courseId as scope for all permissions', () => {
33+
const customCourseId = 'course-v1:custom+org+custom_run';
34+
const result = getGradingPermissions(customCourseId);
35+
36+
expect(result.canViewGradingSettings.scope).toBe(customCourseId);
37+
expect(result.canEditGradingSettings.scope).toBe(customCourseId);
38+
});
39+
});
40+
41+
describe('getPagesAndResourcesPermissions', () => {
42+
it('returns correct permission structure with VIEW action', () => {
43+
const result = getPagesAndResourcesPermissions(courseId);
44+
45+
expect(result.canViewPagesAndResources).toBeDefined();
46+
expect(result.canViewPagesAndResources.action).toBe(COURSE_PERMISSIONS.VIEW_PAGES_AND_RESOURCES);
47+
expect(result.canViewPagesAndResources.scope).toBe(courseId);
48+
});
49+
50+
it('returns correct permission structure with EDIT action', () => {
51+
const result = getPagesAndResourcesPermissions(courseId);
52+
53+
expect(result.canEditPagesAndResources).toBeDefined();
54+
expect(result.canEditPagesAndResources.action).toBe(COURSE_PERMISSIONS.EDIT_PAGES_AND_RESOURCES);
55+
expect(result.canEditPagesAndResources.scope).toBe(courseId);
56+
});
57+
58+
it('returns both VIEW and EDIT permissions in single call', () => {
59+
const result = getPagesAndResourcesPermissions(courseId);
60+
61+
const permissions = Object.keys(result);
62+
expect(permissions).toContain('canViewPagesAndResources');
63+
expect(permissions).toContain('canEditPagesAndResources');
64+
});
65+
66+
it('uses correct courseId as scope for all permissions', () => {
67+
const customCourseId = 'course-v1:another+test+course';
68+
const result = getPagesAndResourcesPermissions(customCourseId);
69+
70+
expect(result.canViewPagesAndResources.scope).toBe(customCourseId);
71+
expect(result.canEditPagesAndResources.scope).toBe(customCourseId);
72+
});
73+
});
74+
75+
describe('permission constants verification', () => {
76+
it('uses correct VIEW_GRADING_SETTINGS constant', () => {
77+
const result = getGradingPermissions(courseId);
78+
expect(result.canViewGradingSettings.action).toBe('courses.view_grading_settings');
79+
});
80+
81+
it('uses correct EDIT_GRADING_SETTINGS constant', () => {
82+
const result = getGradingPermissions(courseId);
83+
expect(result.canEditGradingSettings.action).toBe('courses.edit_grading_settings');
84+
});
85+
86+
it('uses correct VIEW_PAGES_AND_RESOURCES constant', () => {
87+
const result = getPagesAndResourcesPermissions(courseId);
88+
expect(result.canViewPagesAndResources.action).toBe('courses.view_pages_and_resources');
89+
});
90+
91+
it('uses correct EDIT_PAGES_AND_RESOURCES constant', () => {
92+
const result = getPagesAndResourcesPermissions(courseId);
93+
expect(result.canEditPagesAndResources.action).toBe('courses.manage_pages_and_resources');
94+
});
95+
});
96+
97+
describe('edge cases', () => {
98+
it('handles empty courseId', () => {
99+
const result = getGradingPermissions('');
100+
101+
expect(result.canViewGradingSettings.scope).toBe('');
102+
expect(result.canEditGradingSettings.scope).toBe('');
103+
});
104+
105+
it('handles special characters in courseId', () => {
106+
const specialCourseId = 'course-v1:test+special:id';
107+
const result = getPagesAndResourcesPermissions(specialCourseId);
108+
109+
expect(result.canViewPagesAndResources.scope).toBe(specialCourseId);
110+
});
111+
112+
it('returns consistent structure across multiple calls', () => {
113+
const result1 = getGradingPermissions(courseId);
114+
const result2 = getGradingPermissions(courseId);
115+
116+
expect(Object.keys(result1)).toEqual(Object.keys(result2));
117+
expect(result1.canViewGradingSettings.action).toBe(result2.canViewGradingSettings.action);
118+
});
119+
});
120+
});

src/pages-and-resources/PagesAndResources.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ import {
88
import { getConfig, setConfig } from '@edx/frontend-platform';
99
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
1010
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
11+
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
12+
import { useCourseUserPermissions } from '@src/authz/hooks';
1113
import { PagesAndResources } from '.';
1214

15+
// Mock authz hooks
16+
jest.mock('@src/authz/hooks', () => ({
17+
...jest.requireActual('@src/authz/hooks'),
18+
useCourseUserPermissions: jest.fn(),
19+
}));
20+
1321
const mockPlugin = (identifier) => ({
1422
plugins: [
1523
{
@@ -45,8 +53,30 @@ describe('PagesAndResources', () => {
4553
),
4654
},
4755
});
56+
57+
// Set up waffle flags to disable authz by default
58+
mockWaffleFlags({ enableAuthzCourseAuthoring: false });
59+
60+
// Default: authz disabled allows everything
61+
jest.mocked(useCourseUserPermissions).mockReturnValue({
62+
isLoading: false,
63+
isAuthzEnabled: false,
64+
canViewPagesAndResources: true,
65+
canEditPagesAndResources: true,
66+
} as ReturnType<typeof useCourseUserPermissions>);
4867
});
4968

69+
// Helper to set up permission mocks
70+
const mockPermissions = (canView: boolean, canEdit: boolean) => {
71+
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
72+
jest.mocked(useCourseUserPermissions).mockReturnValue({
73+
isLoading: false,
74+
isAuthzEnabled: true,
75+
canViewPagesAndResources: canView,
76+
canEditPagesAndResources: canEdit,
77+
} as ReturnType<typeof useCourseUserPermissions>);
78+
};
79+
5080
it('doesn\'t show content permissions section if relevant apps are not enabled', async () => {
5181
const initialState = {
5282
models: {
@@ -128,4 +158,60 @@ describe('PagesAndResources', () => {
128158
await waitFor(() => expect(screen.queryByTestId('additional_course_plugin')).toBeInTheDocument());
129159
await waitFor(() => expect(screen.queryByTestId('additional_course_content_plugin')).toBeInTheDocument());
130160
});
161+
162+
describe('permission integration', () => {
163+
it('shows PermissionDeniedAlert when user has no VIEW or EDIT permissions', async () => {
164+
mockPermissions(false, false);
165+
166+
const initialState = {
167+
models: {
168+
courseApps: {},
169+
},
170+
pagesAndResources: {
171+
courseAppIds: [],
172+
},
173+
};
174+
175+
initializeMocks({ initialState });
176+
renderComponent();
177+
178+
await waitFor(() => expect(screen.getByTestId('permissionDeniedAlert')).toBeInTheDocument());
179+
});
180+
181+
it('does NOT show PermissionDeniedAlert when user has VIEW permission', async () => {
182+
mockPermissions(true, false);
183+
184+
const initialState = {
185+
models: {
186+
courseApps: {},
187+
},
188+
pagesAndResources: {
189+
courseAppIds: [],
190+
},
191+
};
192+
193+
initializeMocks({ initialState });
194+
renderComponent();
195+
196+
await waitFor(() => expect(screen.queryByTestId('permissionDeniedAlert')).not.toBeInTheDocument());
197+
});
198+
199+
it('does NOT show PermissionDeniedAlert when user has EDIT permission', async () => {
200+
mockPermissions(true, true);
201+
202+
const initialState = {
203+
models: {
204+
courseApps: {},
205+
},
206+
pagesAndResources: {
207+
courseAppIds: [],
208+
},
209+
};
210+
211+
initializeMocks({ initialState });
212+
renderComponent();
213+
214+
await waitFor(() => expect(screen.queryByTestId('permissionDeniedAlert')).not.toBeInTheDocument());
215+
});
216+
});
131217
});

src/pages-and-resources/discussions/app-config-form/apps/openedx/OpenedXConfigForm.test.jsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ describe('OpenedXConfigForm', () => {
7777
axiosMock.reset();
7878
});
7979

80-
const createComponent = (onSubmit = jest.fn(), formRef = createRef(), legacy = true) => {
80+
const createComponent = (onSubmit = jest.fn(), formRef = createRef(), legacy = true, isEditable = false) => {
8181
const wrapper = render(
8282
<AppProvider store={store}>
8383
<IntlProvider locale="en">
8484
<OpenedXConfigForm
8585
onSubmit={onSubmit}
8686
formRef={formRef}
8787
legacy={legacy}
88+
isEditable={isEditable}
8889
/>
8990
</IntlProvider>
9091
</AppProvider>,
@@ -348,4 +349,18 @@ describe('OpenedXConfigForm', () => {
348349
assertHasErrorValidation(false);
349350
});
350351
});
352+
353+
describe('isEditable prop', () => {
354+
test('renders with isEditable=false (read-only mode)', async () => {
355+
await mockStore(legacyApiResponse);
356+
const wrapper = createComponent(jest.fn(), createRef(), true, false);
357+
expect(wrapper.querySelector('form')).toBeInTheDocument();
358+
});
359+
360+
test('renders with isEditable=true (edit mode)', async () => {
361+
await mockStore(legacyApiResponse);
362+
const wrapper = createComponent(jest.fn(), createRef(), true, true);
363+
expect(wrapper.querySelector('form')).toBeInTheDocument();
364+
});
365+
});
351366
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// @ts-check
2+
import { render, screen } from '@testing-library/react';
3+
import { IntlProvider } from '@edx/frontend-platform/i18n';
4+
import { Formik, Form } from 'formik';
5+
import InContextDiscussionFields from './InContextDiscussionFields';
6+
7+
const defaultProps = {
8+
onBlur: jest.fn(),
9+
onChange: jest.fn(),
10+
values: {
11+
enableGradedUnits: false,
12+
groupAtSubsection: false,
13+
},
14+
};
15+
16+
const renderComponent = (props = {}) => render(
17+
<IntlProvider locale="en">
18+
<Formik initialValues={defaultProps.values} onSubmit={jest.fn()}>
19+
<Form>
20+
<InContextDiscussionFields {...defaultProps} {...props} />
21+
</Form>
22+
</Formik>
23+
</IntlProvider>,
24+
);
25+
26+
describe('InContextDiscussionFields', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('renders without crashing', () => {
32+
renderComponent();
33+
// Component renders - check for presence of expected text
34+
expect(screen.getByText(/Visibility of in-context discussions/)).toBeInTheDocument();
35+
});
36+
37+
it('renders with disabled prop', () => {
38+
renderComponent({ disabled: true });
39+
expect(screen.getByText(/Visibility of in-context discussions/)).toBeInTheDocument();
40+
});
41+
42+
it('accepts different values', () => {
43+
renderComponent({
44+
values: {
45+
enableGradedUnits: true,
46+
groupAtSubsection: true,
47+
},
48+
});
49+
expect(screen.getByText(/Visibility of in-context discussions/)).toBeInTheDocument();
50+
});
51+
});

0 commit comments

Comments
 (0)