Skip to content

Commit bcb81a6

Browse files
committed
test: improve test coverage
1 parent 1f9a5b5 commit bcb81a6

9 files changed

Lines changed: 1239 additions & 1 deletion

src/authz-module/constants.test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import {
2+
getRolesMetadata,
3+
getPermissions,
4+
getResourceTypes,
5+
RESOURCE_TYPES,
6+
RoleOperationErrorStatus,
7+
ROUTES,
8+
libraryRolesMetadata,
9+
courseRolesMetadata,
10+
libraryPermissions,
11+
libraryResourceTypes,
12+
CONTENT_LIBRARY_PERMISSIONS,
13+
COURSE_PERMISSIONS,
14+
} from './constants';
15+
16+
describe('ROUTES', () => {
17+
it('defines the expected paths', () => {
18+
expect(ROUTES.LIBRARIES_TEAM_PATH).toBe('/libraries/:libraryId');
19+
expect(ROUTES.LIBRARIES_USER_PATH).toBe('/libraries/:libraryId/:username');
20+
expect(ROUTES.ASSIGN_ROLE_WIZARD_PATH).toBe('/assign-role');
21+
});
22+
});
23+
24+
describe('RoleOperationErrorStatus', () => {
25+
it('has expected enum values', () => {
26+
expect(RoleOperationErrorStatus.USER_NOT_FOUND).toBe('user_not_found');
27+
expect(RoleOperationErrorStatus.USER_ALREADY_HAS_ROLE).toBe('user_already_has_role');
28+
expect(RoleOperationErrorStatus.USER_DOES_NOT_HAVE_ROLE).toBe('user_does_not_have_role');
29+
expect(RoleOperationErrorStatus.ROLE_ASSIGNMENT_ERROR).toBe('role_assignment_error');
30+
expect(RoleOperationErrorStatus.ROLE_REMOVAL_ERROR).toBe('role_removal_error');
31+
});
32+
});
33+
34+
describe('getRolesMetadata', () => {
35+
it('returns library roles for LIBRARY resource type', () => {
36+
expect(getRolesMetadata(RESOURCE_TYPES.LIBRARY)).toEqual(libraryRolesMetadata);
37+
expect(getRolesMetadata(RESOURCE_TYPES.LIBRARY)).toHaveLength(4);
38+
});
39+
40+
it('returns course roles for COURSE resource type', () => {
41+
expect(getRolesMetadata(RESOURCE_TYPES.COURSE)).toEqual(courseRolesMetadata);
42+
expect(getRolesMetadata(RESOURCE_TYPES.COURSE)).toHaveLength(4);
43+
});
44+
45+
it('returns empty array for unknown resource type', () => {
46+
// @ts-expect-error testing invalid input
47+
expect(getRolesMetadata('unknown')).toEqual([]);
48+
});
49+
});
50+
51+
describe('getPermissions', () => {
52+
it('returns library permissions for LIBRARY resource type', () => {
53+
expect(getPermissions(RESOURCE_TYPES.LIBRARY)).toEqual(libraryPermissions);
54+
expect(getPermissions(RESOURCE_TYPES.LIBRARY).length).toBeGreaterThan(0);
55+
});
56+
57+
it('returns empty array for COURSE resource type', () => {
58+
expect(getPermissions(RESOURCE_TYPES.COURSE)).toEqual([]);
59+
});
60+
});
61+
62+
describe('getResourceTypes', () => {
63+
it('returns library resource types for LIBRARY resource type', () => {
64+
expect(getResourceTypes(RESOURCE_TYPES.LIBRARY)).toEqual(libraryResourceTypes);
65+
expect(getResourceTypes(RESOURCE_TYPES.LIBRARY)).toHaveLength(4);
66+
});
67+
68+
it('returns empty array for COURSE resource type', () => {
69+
expect(getResourceTypes(RESOURCE_TYPES.COURSE)).toEqual([]);
70+
});
71+
});
72+
73+
describe('libraryRolesMetadata', () => {
74+
it('includes all expected library roles', () => {
75+
const roles = libraryRolesMetadata.map((r) => r.role);
76+
expect(roles).toContain('library_admin');
77+
expect(roles).toContain('library_author');
78+
expect(roles).toContain('library_contributor');
79+
expect(roles).toContain('library_user');
80+
});
81+
82+
it('all library roles have contextType "library"', () => {
83+
libraryRolesMetadata.forEach((r) => {
84+
expect(r.contextType).toBe('library');
85+
});
86+
});
87+
});
88+
89+
describe('courseRolesMetadata', () => {
90+
it('includes expected course roles', () => {
91+
const roles = courseRolesMetadata.map((r) => r.role);
92+
expect(roles).toContain('course_admin');
93+
expect(roles).toContain('course_staff');
94+
expect(roles).toContain('course_editor');
95+
expect(roles).toContain('course_auditor');
96+
});
97+
98+
it('all course roles have contextType "course"', () => {
99+
courseRolesMetadata.forEach((r) => {
100+
expect(r.contextType).toBe('course');
101+
});
102+
});
103+
104+
it('course_editor and course_auditor are disabled', () => {
105+
const editor = courseRolesMetadata.find((r) => r.role === 'course_editor');
106+
const auditor = courseRolesMetadata.find((r) => r.role === 'course_auditor');
107+
expect(editor?.disabled).toBe(true);
108+
expect(auditor?.disabled).toBe(true);
109+
});
110+
111+
it('course_admin and course_staff are not disabled', () => {
112+
const admin = courseRolesMetadata.find((r) => r.role === 'course_admin');
113+
const staff = courseRolesMetadata.find((r) => r.role === 'course_staff');
114+
expect(admin?.disabled).toBeUndefined();
115+
expect(staff?.disabled).toBeUndefined();
116+
});
117+
});
118+
119+
describe('CONTENT_LIBRARY_PERMISSIONS', () => {
120+
it('defines all expected permission keys', () => {
121+
expect(CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY).toBe('content_libraries.delete_library');
122+
expect(CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TAGS).toBe('content_libraries.manage_library_tags');
123+
expect(CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY).toBe('content_libraries.view_library');
124+
expect(CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_CONTENT).toBe('content_libraries.edit_library_content');
125+
expect(CONTENT_LIBRARY_PERMISSIONS.PUBLISH_LIBRARY_CONTENT).toBe('content_libraries.publish_library_content');
126+
expect(CONTENT_LIBRARY_PERMISSIONS.REUSE_LIBRARY_CONTENT).toBe('content_libraries.reuse_library_content');
127+
expect(CONTENT_LIBRARY_PERMISSIONS.CREATE_LIBRARY_COLLECTION).toBe('content_libraries.create_library_collection');
128+
expect(CONTENT_LIBRARY_PERMISSIONS.EDIT_LIBRARY_COLLECTION).toBe('content_libraries.edit_library_collection');
129+
expect(CONTENT_LIBRARY_PERMISSIONS.DELETE_LIBRARY_COLLECTION).toBe('content_libraries.delete_library_collection');
130+
expect(CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM).toBe('content_libraries.manage_library_team');
131+
expect(CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM).toBe('content_libraries.view_library_team');
132+
});
133+
});
134+
135+
describe('COURSE_PERMISSIONS', () => {
136+
it('defines view permissions', () => {
137+
expect(COURSE_PERMISSIONS.VIEW_COURSE).toBe('courses.view_course');
138+
expect(COURSE_PERMISSIONS.VIEW_COURSE_UPDATES).toBe('courses.view_course_updates');
139+
expect(COURSE_PERMISSIONS.VIEW_PAGES_AND_RESOURCES).toBe('courses.view_pages_and_resources');
140+
expect(COURSE_PERMISSIONS.VIEW_FILES).toBe('courses.view_files');
141+
expect(COURSE_PERMISSIONS.VIEW_GRADING_SETTINGS).toBe('courses.view_grading_settings');
142+
expect(COURSE_PERMISSIONS.VIEW_CHECKLISTS).toBe('courses.view_checklists');
143+
expect(COURSE_PERMISSIONS.VIEW_COURSE_TEAM).toBe('courses.view_course_team');
144+
expect(COURSE_PERMISSIONS.VIEW_SCHEDULE_AND_DETAILS).toBe('courses.view_schedule_and_details');
145+
});
146+
147+
it('defines edit permissions', () => {
148+
expect(COURSE_PERMISSIONS.EDIT_COURSE_CONTENT).toBe('courses.edit_course_content');
149+
expect(COURSE_PERMISSIONS.MANAGE_LIBRARY_UPDATES).toBe('courses.manage_library_updates');
150+
expect(COURSE_PERMISSIONS.MANAGE_COURSE_UPDATES).toBe('courses.manage_course_updates');
151+
expect(COURSE_PERMISSIONS.MANAGE_PAGES_AND_RESOURCES).toBe('courses.manage_pages_and_resources');
152+
expect(COURSE_PERMISSIONS.CREATE_FILES).toBe('courses.create_files');
153+
expect(COURSE_PERMISSIONS.EDIT_FILES).toBe('courses.edit_files');
154+
expect(COURSE_PERMISSIONS.EDIT_GRADING_SETTINGS).toBe('courses.edit_grading_settings');
155+
expect(COURSE_PERMISSIONS.MANAGE_GROUP_CONFIGURATIONS).toBe('courses.manage_group_configurations');
156+
expect(COURSE_PERMISSIONS.EDIT_DETAILS).toBe('courses.edit_details');
157+
expect(COURSE_PERMISSIONS.MANAGE_TAGS).toBe('courses.manage_tags');
158+
});
159+
160+
it('defines publish and lifecycle permissions', () => {
161+
expect(COURSE_PERMISSIONS.PUBLISH_COURSE_CONTENT).toBe('courses.publish_course_content');
162+
expect(COURSE_PERMISSIONS.DELETE_FILES).toBe('courses.delete_files');
163+
expect(COURSE_PERMISSIONS.EDIT_SCHEDULE).toBe('courses.edit_schedule');
164+
expect(COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS).toBe('courses.manage_advanced_settings');
165+
expect(COURSE_PERMISSIONS.MANAGE_CERTIFICATES).toBe('courses.manage_certificates');
166+
expect(COURSE_PERMISSIONS.IMPORT_COURSE).toBe('courses.import_course');
167+
expect(COURSE_PERMISSIONS.EXPORT_COURSE).toBe('courses.export_course');
168+
expect(COURSE_PERMISSIONS.EXPORT_TAGS).toBe('courses.export_tags');
169+
});
170+
171+
it('defines team and taxonomy permissions', () => {
172+
expect(COURSE_PERMISSIONS.MANAGE_COURSE_TEAM).toBe('courses.manage_course_team');
173+
expect(COURSE_PERMISSIONS.MANAGE_TAXONOMIES).toBe('courses.manage_taxonomies');
174+
});
175+
176+
it('defines legacy role permissions', () => {
177+
expect(COURSE_PERMISSIONS.LEGACY_STAFF_ROLE_PERMISSIONS).toBe('courses.legacy_staff_role_permissions');
178+
expect(COURSE_PERMISSIONS.LEGACY_INSTRUCTOR_ROLE_PERMISSIONS).toBe('courses.legacy_instructor_role_permissions');
179+
expect(COURSE_PERMISSIONS.LEGACY_LIMITED_STAFF_ROLE_PERMISSIONS).toBe('courses.legacy_limited_staff_role_permissions');
180+
expect(COURSE_PERMISSIONS.LEGACY_DATA_RESEARCHER_PERMISSIONS).toBe('courses.legacy_data_researcher_permissions');
181+
expect(COURSE_PERMISSIONS.LEGACY_BETA_TESTER_PERMISSIONS).toBe('courses.legacy_beta_tester_permissions');
182+
});
183+
});
184+
185+
describe('getPermissions default case', () => {
186+
it('returns empty array for unknown resource type', () => {
187+
// @ts-expect-error testing invalid input
188+
expect(getPermissions('unknown')).toEqual([]);
189+
});
190+
});
191+
192+
describe('getResourceTypes default case', () => {
193+
it('returns empty array for unknown resource type', () => {
194+
// @ts-expect-error testing invalid input
195+
expect(getResourceTypes('unknown')).toEqual([]);
196+
});
197+
});

src/authz-module/data/api.test.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
2+
import {
3+
getTeamMembers,
4+
assignTeamMembersRole,
5+
validateUsers,
6+
getLibrary,
7+
getPermissionsByRole,
8+
revokeUserRoles,
9+
} from './api';
10+
11+
jest.mock('@edx/frontend-platform/auth', () => ({
12+
getAuthenticatedHttpClient: jest.fn(),
13+
}));
14+
15+
jest.mock('@src/data/utils', () => ({
16+
getApiUrl: (path: string) => `http://localhost:8000${path}`,
17+
getStudioApiUrl: (path: string) => `http://localhost:8010${path}`,
18+
}));
19+
20+
jest.mock('@edx/frontend-platform', () => ({
21+
camelCaseObject: (obj: unknown) => obj,
22+
}));
23+
24+
const mockGet = jest.fn();
25+
const mockPost = jest.fn();
26+
const mockPut = jest.fn();
27+
const mockDelete = jest.fn();
28+
29+
const baseQuerySettings = {
30+
roles: null,
31+
search: null,
32+
order: null,
33+
sortBy: null,
34+
pageSize: 10,
35+
pageIndex: 0,
36+
};
37+
38+
beforeEach(() => {
39+
jest.clearAllMocks();
40+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
41+
get: mockGet,
42+
post: mockPost,
43+
put: mockPut,
44+
delete: mockDelete,
45+
});
46+
});
47+
48+
describe('getTeamMembers', () => {
49+
it('builds URL with required params and returns data', async () => {
50+
const mockData = { count: 1, results: [{ username: 'user1' }] };
51+
mockGet.mockResolvedValue({ data: mockData });
52+
53+
const result = await getTeamMembers('lib:123', baseQuerySettings);
54+
55+
expect(mockGet).toHaveBeenCalled();
56+
const calledUrl = new URL(mockGet.mock.calls[0][0]);
57+
expect(calledUrl.searchParams.get('scope')).toBe('lib:123');
58+
expect(calledUrl.searchParams.get('page_size')).toBe('10');
59+
expect(calledUrl.searchParams.get('page')).toBe('1');
60+
expect(result).toEqual(mockData);
61+
});
62+
63+
it('appends roles and search params when provided', async () => {
64+
mockGet.mockResolvedValue({ data: { count: 0, results: [] } });
65+
66+
await getTeamMembers('lib:123', {
67+
...baseQuerySettings,
68+
roles: 'admin',
69+
search: 'alice',
70+
});
71+
72+
const calledUrl = new URL(mockGet.mock.calls[0][0]);
73+
expect(calledUrl.searchParams.get('roles')).toBe('admin');
74+
expect(calledUrl.searchParams.get('search')).toBe('alice');
75+
});
76+
77+
it('appends sort params when sortBy and order are provided', async () => {
78+
mockGet.mockResolvedValue({ data: { count: 0, results: [] } });
79+
80+
await getTeamMembers('lib:123', {
81+
...baseQuerySettings,
82+
sortBy: 'username',
83+
order: 'asc',
84+
});
85+
86+
const calledUrl = new URL(mockGet.mock.calls[0][0]);
87+
expect(calledUrl.searchParams.get('sort_by')).toBe('username');
88+
expect(calledUrl.searchParams.get('order')).toBe('asc');
89+
});
90+
});
91+
92+
describe('assignTeamMembersRole', () => {
93+
it('sends PUT request and returns camelCased data', async () => {
94+
const mockResponse = { completed: [{ userIdentifier: 'jdoe', status: 'role_added' }], errors: [] };
95+
mockPut.mockResolvedValue({ data: mockResponse });
96+
97+
const result = await assignTeamMembersRole({ users: ['jdoe'], role: 'admin', scope: 'lib:123' });
98+
99+
expect(mockPut).toHaveBeenCalledWith(
100+
'http://localhost:8000/api/authz/v1/roles/users/',
101+
{ users: ['jdoe'], role: 'admin', scope: 'lib:123' },
102+
);
103+
expect(result).toEqual(mockResponse);
104+
});
105+
});
106+
107+
describe('validateUsers', () => {
108+
it('sends POST request and returns valid/invalid users', async () => {
109+
const mockResponse = { validUsers: ['jdoe'], invalidUsers: ['unknown'] };
110+
mockPost.mockResolvedValue({ data: mockResponse });
111+
112+
const result = await validateUsers({ users: ['jdoe', 'unknown'] });
113+
114+
expect(mockPost).toHaveBeenCalledWith(
115+
'http://localhost:8000/api/authz/v1/users/validate',
116+
{ users: ['jdoe', 'unknown'] },
117+
);
118+
expect(result).toEqual(mockResponse);
119+
});
120+
121+
it('returns empty lists when all users are valid', async () => {
122+
const mockResponse = { validUsers: ['jdoe', 'alice'], invalidUsers: [] };
123+
mockPost.mockResolvedValue({ data: mockResponse });
124+
125+
const result = await validateUsers({ users: ['jdoe', 'alice'] });
126+
expect(result.invalidUsers).toHaveLength(0);
127+
expect(result.validUsers).toHaveLength(2);
128+
});
129+
});
130+
131+
describe('getLibrary', () => {
132+
it('fetches library and maps fields correctly', async () => {
133+
const mockData = {
134+
id: 'lib:org/test',
135+
org: 'org',
136+
title: 'Test Library',
137+
slug: 'test-library',
138+
allow_public_read: true,
139+
};
140+
mockGet.mockResolvedValue({ data: mockData });
141+
142+
const result = await getLibrary('lib:org/test');
143+
144+
expect(mockGet).toHaveBeenCalledWith('http://localhost:8010/api/libraries/v2/lib:org/test/');
145+
expect(result).toEqual({
146+
id: 'lib:org/test',
147+
org: 'org',
148+
title: 'Test Library',
149+
slug: 'test-library',
150+
allowPublicRead: true,
151+
});
152+
});
153+
});
154+
155+
describe('getPermissionsByRole', () => {
156+
it('fetches roles with scope param and returns results', async () => {
157+
const mockRoles = [{ role: 'admin', permissions: ['perm1'], userCount: 2 }];
158+
mockGet.mockResolvedValue({ data: { results: mockRoles } });
159+
160+
const result = await getPermissionsByRole('lib:123');
161+
162+
const calledUrl = new URL(mockGet.mock.calls[0][0]);
163+
expect(calledUrl.searchParams.get('scope')).toBe('lib:123');
164+
expect(result).toEqual(mockRoles);
165+
});
166+
});
167+
168+
describe('revokeUserRoles', () => {
169+
it('sends DELETE with correct query params', async () => {
170+
const mockResponse = { completed: [{ userIdentifiers: 'jdoe', status: 'role_removed' }], errors: [] };
171+
mockDelete.mockResolvedValue({ data: mockResponse });
172+
173+
const result = await revokeUserRoles({ users: 'jdoe', role: 'admin', scope: 'lib:123' });
174+
175+
expect(mockDelete).toHaveBeenCalled();
176+
const calledUrl = new URL(mockDelete.mock.calls[0][0]);
177+
expect(calledUrl.searchParams.get('users')).toBe('jdoe');
178+
expect(calledUrl.searchParams.get('role')).toBe('admin');
179+
expect(calledUrl.searchParams.get('scope')).toBe('lib:123');
180+
expect(result).toEqual(mockResponse);
181+
});
182+
});

0 commit comments

Comments
 (0)