Skip to content

Commit 3327288

Browse files
committed
fix: address feedback
1 parent fcc4d60 commit 3327288

5 files changed

Lines changed: 64 additions & 61 deletions

File tree

src/authz/hooks.test.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { renderHook } from '@testing-library/react';
22
import { useUserPermissions } from '@src/authz/data/apiHooks';
33
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
4-
import { useUserPermissionsWithAuthzCourse } from './hooks';
4+
import { useCourseUserPermissions } from './hooks';
55
import { COURSE_PERMISSIONS } from './constants';
66

77
jest.mock('@src/authz/data/apiHooks', () => ({
@@ -14,7 +14,7 @@ const permissions = {
1414
canEdit: { action: COURSE_PERMISSIONS.EDIT_GRADING_SETTINGS, scope: courseId },
1515
};
1616

17-
describe('useUserPermissionsWithAuthzCourse', () => {
17+
describe('useCourseUserPermissions', () => {
1818
beforeEach(() => {
1919
jest.clearAllMocks();
2020
jest.mocked(useUserPermissions).mockReturnValue({
@@ -26,12 +26,12 @@ describe('useUserPermissionsWithAuthzCourse', () => {
2626
it('defaults all permissions to true when authz is disabled', () => {
2727
mockWaffleFlags({ enableAuthzCourseAuthoring: false });
2828

29-
const { result } = renderHook(() => useUserPermissionsWithAuthzCourse(courseId, permissions));
29+
const { result } = renderHook(() => useCourseUserPermissions(courseId, permissions));
3030

3131
expect(result.current.isLoading).toBe(false);
3232
expect(result.current.isAuthzEnabled).toBe(false);
33-
expect(result.current.permissions.canView).toBe(true);
34-
expect(result.current.permissions.canEdit).toBe(true);
33+
expect(result.current.canView).toBe(true);
34+
expect(result.current.canEdit).toBe(true);
3535
});
3636

3737
it('returns actual permission values when authz is enabled and permissions are loaded', () => {
@@ -41,26 +41,27 @@ describe('useUserPermissionsWithAuthzCourse', () => {
4141
data: { canView: true, canEdit: false },
4242
} as unknown as ReturnType<typeof useUserPermissions>);
4343

44-
const { result } = renderHook(() => useUserPermissionsWithAuthzCourse(courseId, permissions));
44+
const { result } = renderHook(() => useCourseUserPermissions(courseId, permissions));
4545

4646
expect(result.current.isLoading).toBe(false);
4747
expect(result.current.isAuthzEnabled).toBe(true);
48-
expect(result.current.permissions.canView).toBe(true);
49-
expect(result.current.permissions.canEdit).toBe(false);
48+
expect(result.current.canView).toBe(true);
49+
expect(result.current.canEdit).toBe(false);
5050
});
5151

52-
it('returns isLoading=true and empty permissions while authz permissions are loading', () => {
52+
it('returns isLoading=true and no permission keys while authz permissions are loading', () => {
5353
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
5454
jest.mocked(useUserPermissions).mockReturnValue({
5555
isLoading: true,
5656
data: undefined,
5757
} as unknown as ReturnType<typeof useUserPermissions>);
5858

59-
const { result } = renderHook(() => useUserPermissionsWithAuthzCourse(courseId, permissions));
59+
const { result } = renderHook(() => useCourseUserPermissions(courseId, permissions));
6060

6161
expect(result.current.isLoading).toBe(true);
6262
expect(result.current.isAuthzEnabled).toBe(true);
63-
expect(result.current.permissions).toEqual({});
63+
expect(result.current.canView).toBeUndefined();
64+
expect(result.current.canEdit).toBeUndefined();
6465
});
6566

6667
it('falls back to false for permissions absent from server response when authz is enabled', () => {
@@ -70,9 +71,9 @@ describe('useUserPermissionsWithAuthzCourse', () => {
7071
data: {},
7172
} as unknown as ReturnType<typeof useUserPermissions>);
7273

73-
const { result } = renderHook(() => useUserPermissionsWithAuthzCourse(courseId, permissions));
74+
const { result } = renderHook(() => useCourseUserPermissions(courseId, permissions));
7475

75-
expect(result.current.permissions.canView).toBe(false);
76-
expect(result.current.permissions.canEdit).toBe(false);
76+
expect(result.current.canView).toBe(false);
77+
expect(result.current.canEdit).toBe(false);
7778
});
7879
});

src/authz/hooks.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
import { useWaffleFlags } from '@src/data/apiHooks';
22
import { useUserPermissions } from '@src/authz/data/apiHooks';
3-
import { PermissionValidationQuery, PermissionValidationAnswer } from '@src/authz/types';
3+
import { PermissionValidationQuery } from '@src/authz/types';
44

5-
interface UseUserPermissionsWithAuthzCourseReturn {
5+
type UseCourseUserPermissionsReturn<Query extends PermissionValidationQuery> = {
66
isLoading: boolean;
7-
permissions: PermissionValidationAnswer;
87
isAuthzEnabled: boolean;
9-
}
8+
} & { [K in keyof Query]: boolean; };
109

1110
/**
12-
* Custom hook to handle user permissions with course authorization waffle flag
11+
* Custom hook to retrieve and evaluate user permissions for the current course using the openedx-authz service.
1312
*
14-
* This hook abstracts the common pattern of:
15-
* 1. Checking if authz is enabled via waffle flag
16-
* 2. Fetching user permissions when authz is enabled
17-
* 3. Defaulting all permissions to true when authz is disabled
18-
* 4. Providing fallback values for undefined permissions
13+
* The hook:
14+
* 1. Validate if authz is enabled via waffle flag
15+
* 2. Fetch user permissions when authz is enabled
16+
* 3. Fallback all permissions to 'true' when authz is disabled
17+
* 4. Provide fallback values for undefined permissions
1918
*
2019
* @param courseId - The course ID to check permissions for
2120
* @param permissions - Object mapping permission names to their action/scope definitions
2221
* @returns Object containing loading state, permissions results, and authz status
2322
*
2423
* @example
2524
* ```tsx
26-
* const { isLoading, permissions, isAuthzEnabled } = useUserPermissionsWithAuthzCourse(
25+
* const { isLoading, canViewGradingSettings, canEditGradingSettings, isAuthzEnabled } = useCourseUserPermissions(
2726
* courseId,
2827
* {
2928
* canViewGradingSettings: {
@@ -36,14 +35,12 @@ interface UseUserPermissionsWithAuthzCourseReturn {
3635
* },
3736
* }
3837
* );
39-
*
40-
* const { canViewGradingSettings, canEditGradingSettings } = permissions;
4138
* ```
4239
*/
43-
export const useUserPermissionsWithAuthzCourse = (
40+
export const useCourseUserPermissions = <Query extends PermissionValidationQuery>(
4441
courseId: string,
45-
permissions: PermissionValidationQuery,
46-
): UseUserPermissionsWithAuthzCourseReturn => {
42+
permissions: Query,
43+
): UseCourseUserPermissionsReturn<Query> => {
4744
const waffleFlags = useWaffleFlags(courseId);
4845
const isAuthzEnabled: boolean = waffleFlags?.enableAuthzCourseAuthoring ?? false;
4946

@@ -52,21 +49,23 @@ export const useUserPermissionsWithAuthzCourse = (
5249
data: userPermissions,
5350
} = useUserPermissions(permissions, isAuthzEnabled);
5451

55-
const permissionResults: PermissionValidationAnswer = {};
52+
const resolvePermission = (key: string): boolean => {
53+
if (!isAuthzEnabled) {
54+
return true;
55+
}
56+
return userPermissions?.[key] ?? false;
57+
};
5658

57-
if (isAuthzEnabled && !isLoadingUserPermissions) {
58-
Object.keys(permissions).forEach((permissionKey: string) => {
59-
permissionResults[permissionKey] = userPermissions?.[permissionKey] ?? false;
60-
});
61-
} else if (!isLoadingUserPermissions) {
62-
Object.keys(permissions).forEach((permissionKey: string) => {
63-
permissionResults[permissionKey] = true;
64-
});
65-
}
59+
const permissionResults: Record<string, boolean> = isLoadingUserPermissions
60+
? {}
61+
: Object.keys(permissions).reduce<Record<string, boolean>>((acc, key) => {
62+
acc[key] = resolvePermission(key);
63+
return acc;
64+
}, {});
6665

6766
return {
6867
isLoading: isAuthzEnabled ? isLoadingUserPermissions : false,
69-
permissions: permissionResults,
7068
isAuthzEnabled,
69+
...permissionResults as { [K in keyof Query]: boolean; },
7170
};
7271
};

src/grading-settings/GradingSettings.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Helmet } from 'react-helmet';
1212
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
1313
import { STATEFUL_BUTTON_STATES } from '@src/constants';
1414
import { useCourseSettings } from '@src/data/apiHooks';
15-
import { useUserPermissionsWithAuthzCourse } from '@src/authz/hooks';
15+
import { useCourseUserPermissions } from '@src/authz/hooks';
1616
import { getGradingPermissions } from '@src/authz/permissionHelpers';
1717
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
1818
import PermissionDeniedAlert from '@src/generic/PermissionDeniedAlert';
@@ -40,8 +40,9 @@ const GradingSettings = () => {
4040

4141
const {
4242
isLoading: isLoadingUserPermissions,
43-
permissions: userPermissions,
44-
} = useUserPermissionsWithAuthzCourse(courseId, getGradingPermissions(courseId));
43+
canViewGradingSettings,
44+
canEditGradingSettings,
45+
} = useCourseUserPermissions(courseId, getGradingPermissions(courseId));
4546

4647
const {
4748
data: gradingSettings,
@@ -102,7 +103,7 @@ const GradingSettings = () => {
102103
}
103104
}, [savePending]);
104105

105-
if (!isLoadingUserPermissions && !userPermissions.canViewGradingSettings) {
106+
if (!isLoadingUserPermissions && !canViewGradingSettings) {
106107
return <PermissionDeniedAlert />;
107108
}
108109

@@ -118,7 +119,7 @@ const GradingSettings = () => {
118119
return null;
119120
}
120121

121-
const isEditable = !isLoadingUserPermissions && userPermissions.canEditGradingSettings;
122+
const isEditable = !isLoadingUserPermissions && canEditGradingSettings;
122123

123124
const handleQueryProcessing = () => {
124125
setShowSuccessAlert(false);

src/grading-settings/GradingSettings.test.jsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
99
import { getCourseSettingsApiUrl } from '@src/data/api';
1010
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
11-
import { useUserPermissionsWithAuthzCourse } from '@src/authz/hooks';
11+
import { useCourseUserPermissions } from '@src/authz/hooks';
1212

1313
import gradingSettings from './__mocks__/gradingSettings';
1414
import { getGradingSettingsApiUrl } from './data/api';
@@ -17,12 +17,10 @@ import GradingSettings from './GradingSettings';
1717
import messages from './messages';
1818

1919
jest.mock('@src/authz/hooks', () => ({
20-
useUserPermissionsWithAuthzCourse: jest.fn().mockReturnValue({
20+
useCourseUserPermissions: jest.fn().mockReturnValue({
2121
isLoading: false,
22-
permissions: {
23-
canViewGradingSettings: true,
24-
canEditGradingSettings: true,
25-
},
22+
canViewGradingSettings: true,
23+
canEditGradingSettings: true,
2624
}),
2725
}));
2826

@@ -151,9 +149,10 @@ describe('<GradingSettings /> permissions', () => {
151149
mock.onGet(getGradingSettingsApiUrl(courseId)).reply(200, gradingSettings);
152150
mock.onPost(getGradingSettingsApiUrl(courseId)).reply(200, {});
153151
mock.onGet(getCourseSettingsApiUrl(courseId)).reply(200, {});
154-
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
152+
jest.mocked(useCourseUserPermissions).mockReturnValue({
155153
isLoading: false,
156-
permissions: { canViewGradingSettings: true, canEditGradingSettings: true },
154+
canViewGradingSettings: true,
155+
canEditGradingSettings: true,
157156
});
158157
});
159158

@@ -171,19 +170,21 @@ describe('<GradingSettings /> permissions', () => {
171170

172171
it('should show permission denied alert when user lacks view permission', async () => {
173172
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
174-
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
173+
jest.mocked(useCourseUserPermissions).mockReturnValue({
175174
isLoading: false,
176-
permissions: { canViewGradingSettings: false, canEditGradingSettings: false },
175+
canViewGradingSettings: false,
176+
canEditGradingSettings: false,
177177
});
178178
render(<RootWrapper />);
179179
expect(await screen.findByTestId('permissionDeniedAlert')).toBeInTheDocument();
180180
});
181181

182182
it('should disable inputs when user has view but not edit permission', async () => {
183183
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
184-
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
184+
jest.mocked(useCourseUserPermissions).mockReturnValue({
185185
isLoading: false,
186-
permissions: { canViewGradingSettings: true, canEditGradingSettings: false },
186+
canViewGradingSettings: true,
187+
canEditGradingSettings: false,
187188
});
188189
render(<RootWrapper />);
189190
const segmentInputs = await screen.findAllByTestId('grading-scale-segment-input');
@@ -192,9 +193,10 @@ describe('<GradingSettings /> permissions', () => {
192193

193194
it('should disable save button when user lacks edit permission', async () => {
194195
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
195-
jest.mocked(useUserPermissionsWithAuthzCourse).mockReturnValue({
196+
jest.mocked(useCourseUserPermissions).mockReturnValue({
196197
isLoading: false,
197-
permissions: { canViewGradingSettings: true, canEditGradingSettings: false },
198+
canViewGradingSettings: true,
199+
canEditGradingSettings: false,
198200
});
199201
render(<RootWrapper />);
200202
const segmentInputs = await screen.findAllByTestId('grading-scale-segment-input');

src/grading-settings/grading-scale/components/GradingScaleSegment.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface GradingScaleSegmentProps {
1818
letters: [string];
1919
gradingSegments: RangeSegment[];
2020
removeGradingSegment: (idx: number) => void;
21-
isEditable?: boolean,
21+
isEditable?: boolean;
2222
}
2323

2424
const GradingScaleSegment = ({

0 commit comments

Comments
 (0)