3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 import { screen, waitFor, within } from '@testing-library/react';
21 import userEvent from '@testing-library/user-event';
22 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
23 import { uniq } from 'lodash';
24 import AuthenticationServiceMock from '../../../../api/mocks/AuthenticationServiceMock';
25 import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock';
26 import { mockPermissionGroup, mockPermissionUser } from '../../../../helpers/mocks/permissions';
27 import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../../../helpers/permissions';
28 import { mockAppState } from '../../../../helpers/testMocks';
29 import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils';
30 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
31 import { ComponentQualifier } from '../../../../types/component';
32 import { Feature } from '../../../../types/features';
33 import { Permissions } from '../../../../types/permissions';
34 import { PermissionGroup, PermissionUser } from '../../../../types/types';
35 import routes from '../../routes';
37 const serviceMock = new PermissionsServiceMock();
38 const authServiceMock = new AuthenticationServiceMock();
42 authServiceMock.reset();
45 describe('rendering', () => {
46 it('should render the list of templates', async () => {
47 const user = userEvent.setup();
48 const ui = getPageObject(user);
49 renderPermissionTemplatesApp();
52 // Lists all templates.
53 expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
54 expect(ui.templateLink('Permission Template 2').get()).toBeInTheDocument();
56 // Shows warning for browse and code viewer permissions.
57 await expect(ui.getHeaderTooltipIconByIndex(1)).toHaveATooltipWithContent(
58 'projects_role.public_projects_warning',
60 await expect(ui.getHeaderTooltipIconByIndex(2)).toHaveATooltipWithContent(
61 'projects_role.public_projects_warning',
65 // Note: because of the intricacies of these table cells, and the verbosity
66 // this would introduce in this test, I went ahead and relied on snapshots.
67 // The snapshots only focus on the text content, so any updates in styling
68 // or DOM structure should not alter the snapshots.
69 const row1 = within(screen.getByRole('row', { name: /Permission Template 1/ }));
70 PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
71 expect(row1.getAllByRole('cell').at(i + 1)?.textContent).toMatchSnapshot(
72 `Permission Template 1: ${permission}`,
75 const row2 = within(screen.getByRole('row', { name: /Permission Template 2/ }));
76 PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
77 expect(row2.getAllByRole('cell').at(i + 1)?.textContent).toMatchSnapshot(
78 `Permission Template 2: ${permission}`,
83 it('should render the correct template', async () => {
84 const user = userEvent.setup();
85 const ui = getPageObject(user);
86 renderPermissionTemplatesApp();
89 expect(ui.githubWarning.query()).not.toBeInTheDocument();
90 await ui.openTemplateDetails('Permission Template 1');
93 expect(ui.githubWarning.query()).not.toBeInTheDocument();
95 expect(screen.getByText('This is permission template 1')).toBeInTheDocument();
98 it.each(PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.map((p, i) => [p, i]))(
99 'should show the correct tooltips',
100 async (permission, i) => {
101 const user = userEvent.setup();
102 const ui = getPageObject(user);
103 renderPermissionTemplatesApp();
104 await ui.appLoaded();
106 await expect(ui.getHeaderTooltipIconByIndex(i)).toHaveATooltipWithContent(
107 `projects_role.${permission}.desc`,
113 describe('CRUD', () => {
114 it('should allow the creation of new templates', async () => {
115 const user = userEvent.setup();
116 const ui = getPageObject(user);
117 renderPermissionTemplatesApp();
118 await ui.appLoaded();
120 await ui.createNewTemplate('New Permission Template', 'New template description');
121 await ui.appLoaded();
123 expect(screen.getByRole('heading', { name: 'New Permission Template' })).toBeInTheDocument();
124 expect(screen.getByText('New template description')).toBeInTheDocument();
127 it('should allow the create modal to be opened and closed', async () => {
128 const user = userEvent.setup();
129 const ui = getPageObject(user);
130 renderPermissionTemplatesApp();
131 await ui.appLoaded();
133 await ui.openCreateModal();
134 await ui.closeModal();
136 expect(ui.modal.query()).not.toBeInTheDocument();
139 it('should allow template details to be updated from the list', async () => {
140 const user = userEvent.setup();
141 const ui = getPageObject(user);
142 renderPermissionTemplatesApp();
143 await ui.appLoaded();
145 await ui.updateTemplate(
146 'Permission Template 2',
148 'Updated description',
152 expect(ui.templateLink('Updated name').get()).toBeInTheDocument();
153 expect(screen.getByText('Updated description')).toBeInTheDocument();
154 expect(screen.getByText('/new pattern/')).toBeInTheDocument();
157 it('should allow template details to be updated from the template page directly', async () => {
158 const user = userEvent.setup();
159 const ui = getPageObject(user);
160 renderPermissionTemplatesApp();
161 await ui.appLoaded();
163 await ui.openTemplateDetails('Permission Template 2');
164 await ui.appLoaded();
166 await ui.updateTemplate(
167 'Permission Template 2',
169 'Updated description',
173 expect(screen.getByText('Updated name')).toBeInTheDocument();
174 expect(screen.getByText('Updated description')).toBeInTheDocument();
175 expect(screen.getByText('/new pattern/')).toBeInTheDocument();
178 it('should allow the update modal to be opened and closed', async () => {
179 const user = userEvent.setup();
180 const ui = getPageObject(user);
181 renderPermissionTemplatesApp();
182 await ui.appLoaded();
184 await ui.openUpdateModal('Permission Template 2');
185 await ui.closeModal();
187 expect(ui.modal.query()).not.toBeInTheDocument();
190 it('should allow templates to be deleted from the list', async () => {
191 const user = userEvent.setup();
192 const ui = getPageObject(user);
193 renderPermissionTemplatesApp();
194 await ui.appLoaded();
196 await ui.deleteTemplate('Permission Template 2');
197 await ui.appLoaded();
199 expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
200 expect(ui.templateLink('Permission Template 2').query()).not.toBeInTheDocument();
203 it('should allow templates to be deleted from the template page directly', async () => {
204 const user = userEvent.setup();
205 const ui = getPageObject(user);
206 renderPermissionTemplatesApp();
207 await ui.appLoaded();
209 await ui.openTemplateDetails('Permission Template 2');
210 await ui.appLoaded();
212 await ui.deleteTemplate('Permission Template 2');
213 await ui.appLoaded();
215 expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
216 expect(ui.templateLink('Permission Template 2').query()).not.toBeInTheDocument();
219 it('should allow the delete modal to be opened and closed', async () => {
220 const user = userEvent.setup();
221 const ui = getPageObject(user);
222 renderPermissionTemplatesApp();
223 await ui.appLoaded();
225 await ui.openDeleteModal('Permission Template 2');
226 await ui.closeModal();
228 expect(ui.modal.query()).not.toBeInTheDocument();
231 it('should not allow a default template to be deleted', async () => {
232 const user = userEvent.setup();
233 const ui = getPageObject(user);
234 renderPermissionTemplatesApp();
235 await ui.appLoaded();
237 await user.click(ui.cogMenuBtn('Permission Template 1').get());
239 expect(ui.deleteBtn.query()).not.toBeInTheDocument();
243 describe('filtering', () => {
244 it('should allow to filter permission holders', async () => {
245 const user = userEvent.setup();
246 const ui = getPageObject(user);
247 renderPermissionTemplatesApp();
248 await ui.appLoaded();
250 await ui.openTemplateDetails('Permission Template 1');
251 await ui.appLoaded();
253 expect(screen.getByText('sonar-users')).toBeInTheDocument();
254 expect(screen.getByText('johndoe')).toBeInTheDocument();
256 await ui.showOnlyUsers();
257 expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
258 expect(screen.getByText('johndoe')).toBeInTheDocument();
260 await ui.showOnlyGroups();
261 expect(screen.getByText('sonar-users')).toBeInTheDocument();
262 expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
265 expect(screen.getByText('sonar-users')).toBeInTheDocument();
266 expect(screen.getByText('johndoe')).toBeInTheDocument();
268 await ui.searchFor('sonar-adm');
269 expect(screen.getByText('sonar-admins')).toBeInTheDocument();
270 expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
271 expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
273 await ui.clearSearch();
274 expect(screen.getByText('sonar-users')).toBeInTheDocument();
275 expect(screen.getByText('johndoe')).toBeInTheDocument();
278 it('should allow to show only permission holders with a specific permission', async () => {
279 const user = userEvent.setup();
280 const ui = getPageObject(user);
281 renderPermissionTemplatesApp();
282 await ui.appLoaded();
284 await ui.openTemplateDetails('Permission Template 1');
285 await ui.appLoaded();
287 expect(screen.getAllByRole('row').length).toBe(12);
288 await ui.toggleFilterByPermission(Permissions.Admin);
289 expect(screen.getAllByRole('row').length).toBe(3);
290 await ui.toggleFilterByPermission(Permissions.Admin);
291 expect(screen.getAllByRole('row').length).toBe(12);
295 describe('assigning/revoking permissions', () => {
296 it('should add and remove permissions to/from a group', async () => {
297 const user = userEvent.setup();
298 const ui = getPageObject(user);
299 renderPermissionTemplatesApp();
300 await ui.appLoaded();
302 await ui.openTemplateDetails('Permission Template 1');
303 await ui.appLoaded();
305 expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
307 await ui.togglePermission('sonar-users', Permissions.Admin);
308 await ui.appLoaded();
309 expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
311 await ui.togglePermission('sonar-users', Permissions.Admin);
312 await ui.appLoaded();
313 expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
316 it('should add and remove permissions to/from a user', async () => {
317 const user = userEvent.setup();
318 const ui = getPageObject(user);
319 renderPermissionTemplatesApp();
320 await ui.appLoaded();
322 await ui.openTemplateDetails('Permission Template 1');
323 await ui.appLoaded();
325 expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
327 await ui.togglePermission('johndoe', Permissions.Scan);
328 await ui.appLoaded();
329 expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
331 await ui.togglePermission('johndoe', Permissions.Scan);
332 await ui.appLoaded();
333 expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
336 it('should handle errors correctly', async () => {
337 serviceMock.setIsAllowedToChangePermissions(false);
338 const user = userEvent.setup();
339 const ui = getPageObject(user);
340 renderPermissionTemplatesApp();
341 await ui.appLoaded();
343 await ui.openTemplateDetails('Permission Template 1');
344 await ui.appLoaded();
346 expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
347 await ui.togglePermission('johndoe', Permissions.Scan);
348 await ui.appLoaded();
349 expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
353 it('should correctly handle pagination', async () => {
354 const groups: PermissionGroup[] = [];
355 const users: PermissionUser[] = [];
356 Array.from(Array(20).keys()).forEach((i) => {
357 groups.push(mockPermissionGroup({ name: `Group ${i}` }));
358 users.push(mockPermissionUser({ login: `user-${i}` }));
360 serviceMock.setGroups(groups);
361 serviceMock.setUsers(users);
363 const user = userEvent.setup();
364 const ui = getPageObject(user);
365 renderPermissionTemplatesApp();
366 await ui.appLoaded();
368 await ui.openTemplateDetails('Permission Template 1');
369 await ui.appLoaded();
371 expect(screen.getAllByRole('row').length).toBe(14);
372 await ui.clickLoadMore();
373 expect(screen.getAllByRole('row').length).toBe(24);
376 it.each([ComponentQualifier.Project, ComponentQualifier.Application, ComponentQualifier.Portfolio])(
377 'should correctly be assignable by default to %s',
378 async (qualifier) => {
379 const user = userEvent.setup();
380 const ui = getPageObject(user);
381 renderPermissionTemplatesApp(uniq([ComponentQualifier.Project, qualifier]));
382 await ui.appLoaded();
384 await ui.setTemplateAsDefaultFor('Permission Template 2', qualifier);
386 const row1 = within(screen.getByRole('row', { name: /Permission Template 1/ }));
387 const row2 = within(screen.getByRole('row', { name: /Permission Template 2/ }));
388 const regex = new RegExp(`permission_template\\.default_for\\.(.*)qualifiers.${qualifier}`);
389 expect(row2.getByText(regex)).toBeInTheDocument();
390 expect(row1.queryByText(regex)).not.toBeInTheDocument();
394 it('should show github warning', async () => {
395 const user = userEvent.setup();
396 const ui = getPageObject(user);
397 authServiceMock.githubProvisioningStatus = true;
398 renderPermissionTemplatesApp(undefined, [Feature.GithubProvisioning]);
400 expect(await ui.githubWarning.find()).toBeInTheDocument();
401 await ui.openTemplateDetails('Permission Template 1');
403 expect(await ui.githubWarning.find()).toBeInTheDocument();
406 function getPageObject(user: UserEvent) {
408 loading: byLabelText('loading'),
409 templateLink: (name: string) => byRole('link', { name }),
410 permissionCheckbox: (target: string, permission: Permissions) =>
412 name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
414 tableHeaderFilter: (permission: Permissions) =>
415 byRole('link', { name: `projects_role.${permission}` }),
416 onlyUsersBtn: byRole('button', { name: 'users.page' }),
417 onlyGroupsBtn: byRole('button', { name: 'user_groups.page' }),
418 githubWarning: byText('permission_templates.github_warning'),
419 showAllBtn: byRole('button', { name: 'all' }),
420 searchInput: byRole('searchbox', { name: 'search.search_for_users_or_groups' }),
421 loadMoreBtn: byRole('button', { name: 'show_more' }),
422 createNewTemplateBtn: byRole('button', { name: 'create' }),
423 modal: byRole('dialog'),
424 cogMenuBtn: (name: string) =>
425 byRole('button', { name: `permission_templates.show_actions_for_x.${name}` }),
426 deleteBtn: byRole('button', { name: 'delete' }),
427 updateDetailsBtn: byRole('button', { name: 'update_details' }),
428 setDefaultBtn: (qualifier: ComponentQualifier) =>
431 qualifier === ComponentQualifier.Project
432 ? 'permission_templates.set_default'
433 : `permission_templates.set_default_for qualifier.${qualifier} qualifiers.${qualifier}`,
440 await waitFor(() => {
441 expect(ui.loading.query()).not.toBeInTheDocument();
444 async openTemplateDetails(name: string) {
445 await user.click(ui.templateLink(name).get());
447 async toggleFilterByPermission(permission: Permissions) {
448 await user.click(ui.tableHeaderFilter(permission).get());
450 async showOnlyUsers() {
451 await user.click(ui.onlyUsersBtn.get());
453 async showOnlyGroups() {
454 await user.click(ui.onlyGroupsBtn.get());
457 await user.click(ui.showAllBtn.get());
459 async searchFor(name: string) {
460 await user.type(ui.searchInput.get(), name);
462 async clearSearch() {
463 await user.clear(ui.searchInput.get());
465 async clickLoadMore() {
466 await user.click(ui.loadMoreBtn.get());
468 async togglePermission(target: string, permission: Permissions) {
469 await user.click(ui.permissionCheckbox(target, permission).get());
471 async openCreateModal() {
472 await user.click(ui.createNewTemplateBtn.get());
474 async createNewTemplate(name: string, description: string, pattern?: string) {
475 await user.click(ui.createNewTemplateBtn.get());
476 const modal = within(ui.modal.get());
477 await user.type(modal.getByRole('textbox', { name: /name/ }), name);
478 await user.type(modal.getByRole('textbox', { name: 'description' }), description);
481 modal.getByRole('textbox', { name: 'permission_template.key_pattern' }),
485 await user.click(modal.getByRole('button', { name: 'create' }));
487 async openDeleteModal(name: string) {
488 await user.click(ui.cogMenuBtn(name).get());
489 await user.click(ui.deleteBtn.get());
491 async deleteTemplate(name: string) {
492 await user.click(ui.cogMenuBtn(name).get());
493 await user.click(ui.deleteBtn.get());
494 const modal = within(ui.modal.get());
495 await user.click(modal.getByRole('button', { name: 'delete' }));
497 async openUpdateModal(name: string) {
498 await user.click(ui.cogMenuBtn(name).get());
499 await user.click(ui.updateDetailsBtn.get());
501 async updateTemplate(
504 newDescription: string,
507 await user.click(ui.cogMenuBtn(name).get());
508 await user.click(ui.updateDetailsBtn.get());
510 const modal = within(ui.modal.get());
511 const nameInput = modal.getByRole('textbox', { name: /name/ });
512 const descriptionInput = modal.getByRole('textbox', { name: 'description' });
513 const patternInput = modal.getByRole('textbox', { name: 'permission_template.key_pattern' });
515 await user.clear(nameInput);
516 await user.type(nameInput, newName);
517 await user.clear(descriptionInput);
518 await user.type(descriptionInput, newDescription);
519 await user.clear(patternInput);
520 await user.type(patternInput, newPattern);
522 await user.click(modal.getByRole('button', { name: 'update_verb' }));
525 const modal = within(ui.modal.get());
526 await user.click(modal.getByRole('button', { name: 'cancel' }));
528 async setTemplateAsDefaultFor(name: string, qualifier: ComponentQualifier) {
529 await user.click(ui.cogMenuBtn(name).get());
530 await user.click(ui.setDefaultBtn(qualifier).get());
532 getHeaderTooltipIconByIndex(i: number) {
533 return byRole('columnheader').byTestId('help-tooltip-activator').getAll()[i];
538 function renderPermissionTemplatesApp(
539 qualifiers = [ComponentQualifier.Project],
540 featureList: Feature[] = [],
542 renderAppWithAdminContext('admin/permission_templates', routes, {
543 appState: mockAppState({ qualifiers }),