]> source.dussan.org Git - sonarqube.git/blob
59323401c05c079cee8c6848c172461619cb76f0
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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
21 import { act, screen } from '@testing-library/react';
22 import userEvent from '@testing-library/user-event';
23 import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
24 import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
25 import { mockComponent } from '../../../../../helpers/mocks/component';
26 import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions';
27 import {
28   PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
29   PERMISSIONS_ORDER_FOR_VIEW,
30 } from '../../../../../helpers/permissions';
31 import {
32   RenderContext,
33   renderAppWithComponentContext,
34 } from '../../../../../helpers/testReactTestingUtils';
35 import { AlmKeys } from '../../../../../types/alm-settings';
36 import {
37   ComponentContextShape,
38   ComponentQualifier,
39   Visibility,
40 } from '../../../../../types/component';
41 import { Feature } from '../../../../../types/features';
42 import { Permissions } from '../../../../../types/permissions';
43 import { Component, PermissionGroup, PermissionUser } from '../../../../../types/types';
44 import { projectPermissionsRoutes } from '../../../routes';
45 import { getPageObject } from '../../../test-utils';
46
47 let serviceMock: PermissionsServiceMock;
48 let authHandler: AuthenticationServiceMock;
49 beforeAll(() => {
50   serviceMock = new PermissionsServiceMock();
51   authHandler = new AuthenticationServiceMock();
52 });
53
54 afterEach(() => {
55   serviceMock.reset();
56   authHandler.reset();
57 });
58
59 describe('rendering', () => {
60   it.each([
61     [ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE],
62     [ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW],
63     [
64       ComponentQualifier.Application,
65       'roles.page.description_application',
66       PERMISSIONS_ORDER_FOR_VIEW,
67     ],
68   ])('should render correctly for %s', async (qualifier, description, permissions) => {
69     const user = userEvent.setup();
70     const ui = getPageObject(user);
71     renderPermissionsProjectApp({ qualifier, visibility: Visibility.Private });
72     await ui.appLoaded();
73
74     expect(screen.getByText(description)).toBeInTheDocument();
75     permissions.forEach((permission) => {
76       expect(ui.projectPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
77     });
78   });
79 });
80
81 describe('filtering', () => {
82   it('should allow to filter permission holders', async () => {
83     const user = userEvent.setup();
84     const ui = getPageObject(user);
85     renderPermissionsProjectApp();
86     await ui.appLoaded();
87
88     expect(screen.getByText('sonar-users')).toBeInTheDocument();
89     expect(screen.getByText('johndoe')).toBeInTheDocument();
90
91     await ui.showOnlyUsers();
92     expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
93     expect(screen.getByText('johndoe')).toBeInTheDocument();
94
95     await ui.showOnlyGroups();
96     expect(screen.getByText('sonar-users')).toBeInTheDocument();
97     expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
98
99     await ui.showAll();
100     expect(screen.getByText('sonar-users')).toBeInTheDocument();
101     expect(screen.getByText('johndoe')).toBeInTheDocument();
102
103     await ui.searchFor('sonar-adm');
104     expect(screen.getByText('sonar-admins')).toBeInTheDocument();
105     expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
106     expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
107
108     await ui.clearSearch();
109     expect(screen.getByText('sonar-users')).toBeInTheDocument();
110     expect(screen.getByText('johndoe')).toBeInTheDocument();
111   });
112
113   it('should allow to show only permission holders with a specific permission', async () => {
114     const user = userEvent.setup();
115     const ui = getPageObject(user);
116     renderPermissionsProjectApp();
117     await ui.appLoaded();
118
119     expect(screen.getAllByRole('row').length).toBe(11);
120     await ui.toggleFilterByPermission(Permissions.Admin);
121     expect(screen.getAllByRole('row').length).toBe(3);
122     await ui.toggleFilterByPermission(Permissions.Admin);
123     expect(screen.getAllByRole('row').length).toBe(11);
124   });
125 });
126
127 describe('assigning/revoking permissions', () => {
128   it('should allow to apply a permission template', async () => {
129     const user = userEvent.setup();
130     const ui = getPageObject(user);
131     renderPermissionsProjectApp();
132     await ui.appLoaded();
133
134     await ui.openTemplateModal();
135     expect(ui.confirmApplyTemplateBtn.get()).toBeDisabled();
136     await ui.chooseTemplate('Permission Template 2');
137     expect(ui.templateSuccessfullyApplied.get()).toBeInTheDocument();
138     await ui.closeTemplateModal();
139     expect(ui.templateSuccessfullyApplied.query()).not.toBeInTheDocument();
140   });
141
142   it('should allow to turn a public project private (and vice-versa)', async () => {
143     const user = userEvent.setup();
144     const ui = getPageObject(user);
145     renderPermissionsProjectApp();
146     await ui.appLoaded();
147
148     expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
149     expect(
150       ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).query()
151     ).not.toBeInTheDocument();
152     await act(async () => {
153       await ui.turnProjectPrivate();
154     });
155     expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
156     expect(
157       ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()
158     ).toBeInTheDocument();
159
160     await ui.turnProjectPublic();
161     expect(ui.makePublicDisclaimer.get()).toBeInTheDocument();
162     await act(async () => {
163       await ui.confirmTurnProjectPublic();
164     });
165     expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
166   });
167
168   it('should add and remove permissions to/from a group', async () => {
169     const user = userEvent.setup();
170     const ui = getPageObject(user);
171     renderPermissionsProjectApp();
172     await ui.appLoaded();
173
174     expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
175
176     await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
177     await ui.appLoaded();
178     expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
179
180     await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
181     await ui.appLoaded();
182     expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
183   });
184
185   it('should add and remove permissions to/from a user', async () => {
186     const user = userEvent.setup();
187     const ui = getPageObject(user);
188     renderPermissionsProjectApp();
189     await ui.appLoaded();
190
191     expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
192
193     await ui.toggleProjectPermission('johndoe', Permissions.Scan);
194     await ui.appLoaded();
195     expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
196
197     await ui.toggleProjectPermission('johndoe', Permissions.Scan);
198     await ui.appLoaded();
199     expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
200   });
201
202   it('should handle errors correctly', async () => {
203     serviceMock.setIsAllowedToChangePermissions(false);
204     const user = userEvent.setup();
205     const ui = getPageObject(user);
206     renderPermissionsProjectApp();
207     await ui.appLoaded();
208
209     expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
210     await ui.toggleProjectPermission('johndoe', Permissions.Scan);
211     await ui.appLoaded();
212     expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
213   });
214 });
215
216 it('should correctly handle pagination', async () => {
217   const groups: PermissionGroup[] = [];
218   const users: PermissionUser[] = [];
219   Array.from(Array(20).keys()).forEach((i) => {
220     groups.push(mockPermissionGroup({ name: `Group ${i}` }));
221     users.push(mockPermissionUser({ login: `user-${i}` }));
222   });
223   serviceMock.setGroups(groups);
224   serviceMock.setUsers(users);
225
226   const user = userEvent.setup();
227   const ui = getPageObject(user);
228   renderPermissionsProjectApp();
229   await ui.appLoaded();
230
231   expect(screen.getAllByRole('row').length).toBe(11);
232   await ui.clickLoadMore();
233   expect(screen.getAllByRole('row').length).toBe(21);
234 });
235
236 it('should not allow to change visibility for GH Project with auto-provisioning', async () => {
237   const user = userEvent.setup();
238   const ui = getPageObject(user);
239   authHandler.githubProvisioningStatus = true;
240   renderPermissionsProjectApp(
241     {},
242     { featureList: [Feature.GithubProvisioning] },
243     { projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false } }
244   );
245   await ui.appLoaded();
246
247   expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled();
248   expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
249   expect(ui.visibilityRadio(Visibility.Private).get()).toBeDisabled();
250   await act(async () => {
251     await ui.turnProjectPrivate();
252   });
253   expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked();
254 });
255
256 it('should allow to change visibility for non-GH Project', async () => {
257   const user = userEvent.setup();
258   const ui = getPageObject(user);
259   authHandler.githubProvisioningStatus = true;
260   renderPermissionsProjectApp(
261     {},
262     { featureList: [Feature.GithubProvisioning] },
263     { projectBinding: { alm: AlmKeys.Azure, key: 'test', repository: 'test', monorepo: false } }
264   );
265   await ui.appLoaded();
266
267   expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
268   expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
269   expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
270   await act(async () => {
271     await ui.turnProjectPrivate();
272   });
273   expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
274 });
275
276 it('should allow to change visibility for GH Project with disabled auto-provisioning', async () => {
277   const user = userEvent.setup();
278   const ui = getPageObject(user);
279   authHandler.githubProvisioningStatus = false;
280   renderPermissionsProjectApp(
281     {},
282     { featureList: [Feature.GithubProvisioning] },
283     { projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false } }
284   );
285   await ui.appLoaded();
286
287   expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
288   expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
289   expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
290   await act(async () => {
291     await ui.turnProjectPrivate();
292   });
293   expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
294 });
295
296 it('should have disabled permissions for GH Project', async () => {
297   const user = userEvent.setup();
298   const ui = getPageObject(user);
299   authHandler.githubProvisioningStatus = true;
300   renderPermissionsProjectApp(
301     {},
302     { featureList: [Feature.GithubProvisioning] },
303     {
304       component: mockComponent({ visibility: Visibility.Private }),
305       projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false },
306     }
307   );
308   await ui.appLoaded();
309
310   expect(ui.pageTitle.get()).toBeInTheDocument();
311   expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/);
312   expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
313   expect(ui.githubExplanations.get()).toBeInTheDocument();
314
315   expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeChecked();
316   expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toHaveAttribute(
317     'aria-disabled',
318     'true'
319   );
320   expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
321   expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toHaveAttribute(
322     'aria-disabled',
323     'false'
324   );
325   await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
326   expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
327   expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
328     `${Permissions.IssueAdmin}Alexa`
329   );
330   await act(() =>
331     user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get())
332   );
333   expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
334
335   expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
336   expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toHaveAttribute(
337     'aria-disabled',
338     'false'
339   );
340   await ui.toggleProjectPermission('sonar-users', Permissions.Browse);
341   expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
342   expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
343     `${Permissions.Browse}sonar-users`
344   );
345   await act(() =>
346     user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get())
347   );
348   expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).not.toBeChecked();
349   expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toBeChecked();
350   expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toHaveAttribute(
351     'aria-disabled',
352     'true'
353   );
354
355   const johnRow = screen.getAllByRole('row')[4];
356   expect(johnRow).toHaveTextContent('John');
357   expect(ui.githubLogo.get(johnRow)).toBeInTheDocument();
358   const alexaRow = screen.getAllByRole('row')[5];
359   expect(alexaRow).toHaveTextContent('Alexa');
360   expect(ui.githubLogo.query(alexaRow)).not.toBeInTheDocument();
361   const usersGroupRow = screen.getAllByRole('row')[1];
362   expect(usersGroupRow).toHaveTextContent('sonar-users');
363   expect(ui.githubLogo.query(usersGroupRow)).not.toBeInTheDocument();
364   const adminsGroupRow = screen.getAllByRole('row')[2];
365   expect(adminsGroupRow).toHaveTextContent('sonar-admins');
366   expect(ui.githubLogo.query(adminsGroupRow)).toBeInTheDocument();
367
368   expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
369
370   // not possible to grant permissions at all
371   expect(
372     screen
373       .getAllByRole('checkbox', { checked: false })
374       .every((item) => item.getAttribute('aria-disabled') === 'true')
375   ).toBe(true);
376 });
377
378 it('should allow to change permissions for GH Project without auto-provisioning', async () => {
379   const user = userEvent.setup();
380   const ui = getPageObject(user);
381   authHandler.githubProvisioningStatus = false;
382   renderPermissionsProjectApp(
383     { visibility: Visibility.Private },
384     { featureList: [Feature.GithubProvisioning] },
385     {
386       projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false },
387     }
388   );
389   await ui.appLoaded();
390
391   expect(ui.pageTitle.get()).toBeInTheDocument();
392   expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
393
394   expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
395
396   // no restrictions
397   expect(
398     screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true')
399   ).toBe(true);
400 });
401
402 it('should allow to change permissions for non-GH Project', async () => {
403   const user = userEvent.setup();
404   const ui = getPageObject(user);
405   authHandler.githubProvisioningStatus = true;
406   renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
407   await ui.appLoaded();
408
409   expect(ui.pageTitle.get()).toBeInTheDocument();
410   expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
411   expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
412
413   expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
414
415   // no restrictions
416   expect(
417     screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true')
418   ).toBe(true);
419 });
420
421 function renderPermissionsProjectApp(
422   override: Partial<Component> = {},
423   contextOverride: Partial<RenderContext> = {},
424   componentContextOverride: Partial<ComponentContextShape> = {}
425 ) {
426   return renderAppWithComponentContext('project_roles', projectPermissionsRoutes, contextOverride, {
427     component: mockComponent({
428       visibility: Visibility.Public,
429       configuration: {
430         canUpdateProjectVisibilityToPrivate: true,
431         canApplyPermissionTemplate: true,
432       },
433       ...override,
434     }),
435     ...componentContextOverride,
436   });
437 }