Skip to content

Commit 27c140e

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

3 files changed

Lines changed: 265 additions & 7 deletions

File tree

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-list/AppCard.test.jsx

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import {
33
queryByLabelText,
44
queryByTestId,
55
initializeMocks,
6+
fireEvent,
67
} from '@src/testUtils';
78
import { executeThunk } from '@src/utils';
89

910
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
11+
import PagesAndResourcesProvider from '../../PagesAndResourcesProvider';
1012
import AppCard from './AppCard';
1113
import messages from './messages';
1214
import appMessages from '../app-config-form/messages';
@@ -38,15 +40,17 @@ describe('AppCard', () => {
3840
await executeThunk(fetchProviders(courseId), store.dispatch);
3941
};
4042

41-
const createComponent = (data) => {
43+
const createComponent = (data, { isEditable = false } = {}) => {
4244
const wrapper = render(
4345
<CourseAuthoringProvider courseId={courseId}>
44-
<AppCard
45-
app={data}
46-
onClick={() => jest.fn()}
47-
selected={selected}
48-
features={[]}
49-
/>
46+
<PagesAndResourcesProvider courseId={courseId} isEditable={isEditable}>
47+
<AppCard
48+
app={data}
49+
onClick={() => jest.fn()}
50+
selected={selected}
51+
features={[]}
52+
/>
53+
</PagesAndResourcesProvider>
5054
</CourseAuthoringProvider>,
5155
);
5256
container = wrapper.container;
@@ -96,4 +100,52 @@ describe('AppCard', () => {
96100

97101
expect(queryByTestId(container, 'card-subtitle')).toHaveTextContent(subtitle);
98102
});
103+
104+
describe('isEditable integration', () => {
105+
test('card responds to click when isEditable=true', async () => {
106+
const handleClick = jest.fn();
107+
await mockStore(legacyApiResponse);
108+
109+
const wrapper = render(
110+
<CourseAuthoringProvider courseId={courseId}>
111+
<PagesAndResourcesProvider courseId={courseId} isEditable>
112+
<AppCard
113+
app={app}
114+
onClick={handleClick}
115+
selected={false}
116+
features={[]}
117+
/>
118+
</PagesAndResourcesProvider>
119+
</CourseAuthoringProvider>,
120+
);
121+
const card = wrapper.container.querySelector('[role="radio"]');
122+
123+
fireEvent.click(card);
124+
125+
expect(handleClick).toHaveBeenCalledWith(app.id);
126+
});
127+
128+
test('card does NOT respond to click when isEditable=false', async () => {
129+
const handleClick = jest.fn();
130+
await mockStore(legacyApiResponse);
131+
132+
const wrapper = render(
133+
<CourseAuthoringProvider courseId={courseId}>
134+
<PagesAndResourcesProvider courseId={courseId} isEditable={false}>
135+
<AppCard
136+
app={app}
137+
onClick={handleClick}
138+
selected={false}
139+
features={[]}
140+
/>
141+
</PagesAndResourcesProvider>
142+
</CourseAuthoringProvider>,
143+
);
144+
const card = wrapper.container.querySelector('[role="radio"]');
145+
146+
fireEvent.click(card);
147+
148+
expect(handleClick).not.toHaveBeenCalled();
149+
});
150+
});
99151
});

0 commit comments

Comments
 (0)