]> source.dussan.org Git - sonarqube.git/blob
4c0199c386fe82530f86e296b1e28a0908ed933f
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
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 GithubProvisioningServiceMock from '../../../../api/mocks/GithubProvisioningServiceMock';
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 { 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';
36
37 const serviceMock = new PermissionsServiceMock();
38 const githubHandler = new GithubProvisioningServiceMock();
39
40 beforeEach(() => {
41   serviceMock.reset();
42   githubHandler.reset();
43 });
44
45 describe('rendering', () => {
46   it('should render the list of templates', async () => {
47     const user = userEvent.setup();
48     const ui = getPageObject(user);
49     renderPermissionTemplatesApp();
50     await ui.appLoaded();
51
52     // Lists all templates.
53     expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
54     expect(ui.templateLink('Permission Template 2').get()).toBeInTheDocument();
55
56     // Shows warning for browse and code viewer permissions.
57     await expect(ui.getHeaderTooltipIconByIndex(1)).toHaveATooltipWithContent(
58       'projects_role.public_projects_warning',
59     );
60     await expect(ui.getHeaderTooltipIconByIndex(2)).toHaveATooltipWithContent(
61       'projects_role.public_projects_warning',
62     );
63
64     // Check summaries.
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}`,
73       );
74     });
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}`,
79       );
80     });
81   });
82
83   it('should render the correct template', async () => {
84     const user = userEvent.setup();
85     const ui = getPageObject(user);
86     renderPermissionTemplatesApp();
87     await ui.appLoaded();
88
89     expect(ui.githubWarning.query()).not.toBeInTheDocument();
90     await ui.openTemplateDetails('Permission Template 1');
91     await ui.appLoaded();
92
93     expect(ui.githubWarning.query()).not.toBeInTheDocument();
94
95     expect(screen.getByText('This is permission template 1')).toBeInTheDocument();
96   });
97
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();
105
106       await expect(ui.getHeaderTooltipIconByIndex(i)).toHaveATooltipWithContent(
107         `projects_role.${permission}.desc`,
108       );
109     },
110   );
111 });
112
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();
119
120     await ui.createNewTemplate('New Permission Template', 'New template description');
121     await ui.appLoaded();
122
123     expect(screen.getByRole('heading', { name: 'New Permission Template' })).toBeInTheDocument();
124     expect(screen.getByText('New template description')).toBeInTheDocument();
125   });
126
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();
132
133     await ui.openCreateModal();
134     await ui.closeModal();
135
136     expect(ui.modal.query()).not.toBeInTheDocument();
137   });
138
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();
144
145     await ui.updateTemplate(
146       'Permission Template 2',
147       'Updated name',
148       'Updated description',
149       '/new pattern/',
150     );
151
152     expect(ui.templateLink('Updated name').get()).toBeInTheDocument();
153     expect(screen.getByText('Updated description')).toBeInTheDocument();
154     expect(screen.getByText('/new pattern/')).toBeInTheDocument();
155   });
156
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();
162
163     await ui.openTemplateDetails('Permission Template 2');
164     await ui.appLoaded();
165
166     await ui.updateTemplate(
167       'Permission Template 2',
168       'Updated name',
169       'Updated description',
170       '/new pattern/',
171     );
172
173     expect(screen.getByText('Updated name')).toBeInTheDocument();
174     expect(screen.getByText('Updated description')).toBeInTheDocument();
175     expect(screen.getByText('/new pattern/')).toBeInTheDocument();
176   });
177
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();
183
184     await ui.openUpdateModal('Permission Template 2');
185     await ui.closeModal();
186
187     expect(ui.modal.query()).not.toBeInTheDocument();
188   });
189
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();
195
196     await ui.deleteTemplate('Permission Template 2');
197     await ui.appLoaded();
198
199     expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
200     expect(ui.templateLink('Permission Template 2').query()).not.toBeInTheDocument();
201   });
202
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();
208
209     await ui.openTemplateDetails('Permission Template 2');
210     await ui.appLoaded();
211
212     await ui.deleteTemplate('Permission Template 2');
213     await ui.appLoaded();
214
215     expect(ui.templateLink('Permission Template 1').get()).toBeInTheDocument();
216     expect(ui.templateLink('Permission Template 2').query()).not.toBeInTheDocument();
217   });
218
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();
224
225     await ui.openDeleteModal('Permission Template 2');
226     await ui.closeModal();
227
228     expect(ui.modal.query()).not.toBeInTheDocument();
229   });
230
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();
236
237     await user.click(ui.cogMenuBtn('Permission Template 1').get());
238
239     expect(ui.deleteBtn.query()).not.toBeInTheDocument();
240   });
241 });
242
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();
249
250     await ui.openTemplateDetails('Permission Template 1');
251     await ui.appLoaded();
252
253     expect(screen.getByText('sonar-users')).toBeInTheDocument();
254     expect(screen.getByText('johndoe')).toBeInTheDocument();
255
256     await ui.showOnlyUsers();
257     expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
258     expect(screen.getByText('johndoe')).toBeInTheDocument();
259
260     await ui.showOnlyGroups();
261     expect(screen.getByText('sonar-users')).toBeInTheDocument();
262     expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
263
264     await ui.showAll();
265     expect(screen.getByText('sonar-users')).toBeInTheDocument();
266     expect(screen.getByText('johndoe')).toBeInTheDocument();
267
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();
272
273     await ui.clearSearch();
274     expect(screen.getByText('sonar-users')).toBeInTheDocument();
275     expect(screen.getByText('johndoe')).toBeInTheDocument();
276   });
277
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();
283
284     await ui.openTemplateDetails('Permission Template 1');
285     await ui.appLoaded();
286
287     expect(screen.getAllByRole('row').length).toBe(11);
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(11);
292   });
293 });
294
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();
301
302     await ui.openTemplateDetails('Permission Template 1');
303     await ui.appLoaded();
304
305     expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
306
307     await ui.togglePermission('sonar-users', Permissions.Admin);
308     await ui.appLoaded();
309     expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
310
311     await ui.togglePermission('sonar-users', Permissions.Admin);
312     await ui.appLoaded();
313     expect(ui.permissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
314   });
315
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();
321
322     await ui.openTemplateDetails('Permission Template 1');
323     await ui.appLoaded();
324
325     expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
326
327     await ui.togglePermission('johndoe', Permissions.Scan);
328     await ui.appLoaded();
329     expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
330
331     await ui.togglePermission('johndoe', Permissions.Scan);
332     await ui.appLoaded();
333     expect(ui.permissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
334   });
335
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();
342
343     await ui.openTemplateDetails('Permission Template 1');
344     await ui.appLoaded();
345
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();
350   });
351 });
352
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}` }));
359   });
360   serviceMock.setGroups(groups);
361   serviceMock.setUsers(users);
362
363   const user = userEvent.setup();
364   const ui = getPageObject(user);
365   renderPermissionTemplatesApp();
366   await ui.appLoaded();
367
368   await ui.openTemplateDetails('Permission Template 1');
369   await ui.appLoaded();
370
371   expect(screen.getAllByRole('row').length).toBe(13);
372   await ui.clickLoadMore();
373   expect(screen.getAllByRole('row').length).toBe(23);
374 });
375
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();
383
384     await ui.setTemplateAsDefaultFor('Permission Template 2', qualifier);
385
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();
391   },
392 );
393
394 it('should show github warning', async () => {
395   const user = userEvent.setup();
396   const ui = getPageObject(user);
397   githubHandler.githubProvisioningStatus = true;
398   renderPermissionTemplatesApp(undefined, [Feature.GithubProvisioning]);
399
400   expect(await ui.githubWarning.find()).toBeInTheDocument();
401   await ui.openTemplateDetails('Permission Template 1');
402
403   expect(await ui.githubWarning.find()).toBeInTheDocument();
404 });
405
406 function getPageObject(user: UserEvent) {
407   const ui = {
408     loading: byText('loading'),
409     templateLink: (name: string) => byRole('link', { name }),
410     permissionCheckbox: (target: string, permission: Permissions) =>
411       byRole('checkbox', {
412         name: `permission.assign_x_to_y.projects_role.${permission}.${target}`,
413       }),
414     tableHeaderFilter: (permission: Permissions) =>
415       byRole('button', { name: `projects_role.${permission}` }),
416     onlyUsersBtn: byRole('radio', { name: 'users.page' }),
417     onlyGroupsBtn: byRole('radio', { name: 'user_groups.page' }),
418     githubWarning: byText('permission_templates.github_warning'),
419     showAllBtn: byRole('radio', { 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('menuitem', { name: 'delete' }),
427     updateDetailsBtn: byRole('menuitem', { name: 'update_details' }),
428     setDefaultBtn: (qualifier: ComponentQualifier) =>
429       byRole('menuitem', {
430         name:
431           qualifier === ComponentQualifier.Project
432             ? 'permission_templates.set_default'
433             : `permission_templates.set_default_for qualifier.${qualifier} qualifiers.${qualifier}`,
434       }),
435   };
436
437   return {
438     ...ui,
439     async appLoaded() {
440       await waitFor(() => {
441         expect(ui.loading.query()).not.toBeInTheDocument();
442       });
443     },
444     async openTemplateDetails(name: string) {
445       await user.click(ui.templateLink(name).get());
446     },
447     async toggleFilterByPermission(permission: Permissions) {
448       await user.click(ui.tableHeaderFilter(permission).get());
449     },
450     async showOnlyUsers() {
451       await user.click(ui.onlyUsersBtn.get());
452     },
453     async showOnlyGroups() {
454       await user.click(ui.onlyGroupsBtn.get());
455     },
456     async showAll() {
457       await user.click(ui.showAllBtn.get());
458     },
459     async searchFor(name: string) {
460       await user.type(ui.searchInput.get(), name);
461     },
462     async clearSearch() {
463       await user.clear(ui.searchInput.get());
464     },
465     async clickLoadMore() {
466       await user.click(ui.loadMoreBtn.get());
467     },
468     async togglePermission(target: string, permission: Permissions) {
469       await user.click(ui.permissionCheckbox(target, permission).get());
470     },
471     async openCreateModal() {
472       await user.click(ui.createNewTemplateBtn.get());
473     },
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);
479       if (pattern) {
480         await user.type(
481           modal.getByRole('textbox', { name: 'permission_template.key_pattern' }),
482           pattern,
483         );
484       }
485       await user.click(modal.getByRole('button', { name: 'create' }));
486     },
487     async openDeleteModal(name: string) {
488       await user.click(ui.cogMenuBtn(name).get());
489       await user.click(ui.deleteBtn.get());
490     },
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' }));
496     },
497     async openUpdateModal(name: string) {
498       await user.click(ui.cogMenuBtn(name).get());
499       await user.click(ui.updateDetailsBtn.get());
500     },
501     async updateTemplate(
502       name: string,
503       newName: string,
504       newDescription: string,
505       newPattern: string,
506     ) {
507       await user.click(ui.cogMenuBtn(name).get());
508       await user.click(ui.updateDetailsBtn.get());
509
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' });
514
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);
521
522       await user.click(modal.getByRole('button', { name: 'update_verb' }));
523     },
524     async closeModal() {
525       const modal = within(ui.modal.get());
526       await user.click(modal.getByRole('button', { name: 'cancel' }));
527     },
528     async setTemplateAsDefaultFor(name: string, qualifier: ComponentQualifier) {
529       await user.click(ui.cogMenuBtn(name).get());
530       await user.click(ui.setDefaultBtn(qualifier).get());
531     },
532     getHeaderTooltipIconByIndex(i: number) {
533       return byRole('columnheader').byTestId('help-tooltip-activator').getAll()[i];
534     },
535   };
536 }
537
538 function renderPermissionTemplatesApp(
539   qualifiers = [ComponentQualifier.Project],
540   featureList: Feature[] = [],
541 ) {
542   renderAppWithAdminContext('admin/permission_templates', routes, {
543     appState: mockAppState({ qualifiers }),
544     featureList,
545   });
546 }