3 * Copyright (C) 2009-2024 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 } 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';
32 PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
33 PERMISSIONS_ORDER_FOR_VIEW,
34 } from '../../../../../helpers/permissions';
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';
48 let serviceMock: PermissionsServiceMock;
49 let dopTranslationHandler: DopTranslationServiceMock;
50 let githubHandler: GithubProvisioningServiceMock;
51 let almHandler: AlmSettingsServiceMock;
52 let systemHandler: SystemServiceMock;
54 serviceMock = new PermissionsServiceMock();
55 dopTranslationHandler = new DopTranslationServiceMock();
56 githubHandler = new GithubProvisioningServiceMock(dopTranslationHandler);
57 almHandler = new AlmSettingsServiceMock();
58 systemHandler = new SystemServiceMock();
63 dopTranslationHandler.reset();
64 githubHandler.reset();
68 describe('rendering', () => {
70 [ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE],
71 [ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW],
73 ComponentQualifier.Application,
74 'roles.page.description_application',
75 PERMISSIONS_ORDER_FOR_VIEW,
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 });
83 expect(screen.getByText(description)).toBeInTheDocument();
84 permissions.forEach((permission) => {
85 expect(ui.projectPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
90 describe('filtering', () => {
91 it('should allow to filter permission holders', async () => {
92 const user = userEvent.setup();
93 const ui = getPageObject(user);
94 renderPermissionsProjectApp();
97 expect(screen.getByText('sonar-users')).toBeInTheDocument();
98 expect(screen.getByText('johndoe')).toBeInTheDocument();
100 await ui.showOnlyUsers();
101 expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
102 expect(screen.getByText('johndoe')).toBeInTheDocument();
104 await ui.showOnlyGroups();
105 expect(screen.getByText('sonar-users')).toBeInTheDocument();
106 expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
109 expect(screen.getByText('sonar-users')).toBeInTheDocument();
110 expect(screen.getByText('johndoe')).toBeInTheDocument();
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();
117 await ui.clearSearch();
118 expect(screen.getByText('sonar-users')).toBeInTheDocument();
119 expect(screen.getByText('johndoe')).toBeInTheDocument();
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();
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);
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();
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();
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();
157 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
159 ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).query(),
160 ).not.toBeInTheDocument();
161 await ui.turnProjectPrivate();
162 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
164 ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get(),
165 ).toBeInTheDocument();
167 await ui.turnProjectPublic();
168 expect(ui.makePublicDisclaimer.get()).toBeInTheDocument();
169 await ui.confirmTurnProjectPublic();
170 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
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();
179 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
181 await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
182 await ui.appLoaded();
183 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
185 await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
186 await ui.appLoaded();
187 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
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();
196 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
198 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
199 await ui.appLoaded();
200 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
202 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
203 await ui.appLoaded();
204 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
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();
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();
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}` }));
228 serviceMock.setGroups(groups);
229 serviceMock.setUsers(users);
231 const user = userEvent.setup();
232 const ui = getPageObject(user);
233 renderPermissionsProjectApp();
234 await ui.appLoaded();
236 expect(screen.getAllByRole('row').length).toBe(11);
237 await ui.clickLoadMore();
238 expect(screen.getAllByRole('row').length).toBe(21);
241 describe('GH provisioning', () => {
243 systemHandler.setProvider(Provider.Github);
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 }),
252 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
256 project: 'my-project',
258 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
259 await ui.appLoaded();
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();
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 }),
274 almHandler.handleSetProjectBinding(AlmKeys.Azure, {
278 project: 'my-project',
280 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
281 await ui.appLoaded();
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();
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, {
298 project: 'my-project',
300 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
301 await ui.appLoaded();
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();
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 }),
316 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
320 project: 'my-project',
322 renderPermissionsProjectApp(
324 { featureList: [Feature.GithubProvisioning] },
326 component: mockComponent({ visibility: Visibility.Private }),
329 await ui.appLoaded();
331 expect(ui.pageTitle.get()).toBeInTheDocument();
333 expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/),
335 expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
336 expect(ui.githubExplanations.get()).toBeInTheDocument();
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`,
347 await user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get());
348 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
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`,
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(
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();
377 expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
379 // not possible to grant permissions at all
382 .getAllByRole('checkbox', { checked: false })
383 .every((item) => item.getAttributeNames().includes('disabled')),
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, {
395 project: 'my-project',
397 renderPermissionsProjectApp(
398 { visibility: Visibility.Private },
399 { featureList: [Feature.GithubProvisioning] },
401 await ui.appLoaded();
403 expect(ui.pageTitle.get()).toBeInTheDocument();
404 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
406 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
411 .getAllByRole('checkbox')
412 .every((item) => item.getAttributeNames().includes('disabled')),
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 }),
422 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
423 await ui.appLoaded();
425 expect(ui.pageTitle.get()).toBeInTheDocument();
426 expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
427 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
429 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
434 .getAllByRole('checkbox')
435 .every((item) => item.getAttributeNames().includes('disabled')),
440 function renderPermissionsProjectApp(
441 override: Partial<Component> = {},
442 contextOverride: Partial<RenderContext> = {},
443 componentContextOverride: Partial<ComponentContextShape> = {},
445 return renderAppWithComponentContext(
446 'project_roles?id=my-project',
447 projectPermissionsRoutes,
450 component: mockComponent({
451 visibility: Visibility.Public,
453 canUpdateProjectVisibilityToPrivate: true,
454 canApplyPermissionTemplate: true,
458 ...componentContextOverride,