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