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.
21 import { act, screen, waitFor } from '@testing-library/react';
22 import userEvent from '@testing-library/user-event';
23 import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
24 import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
25 import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
26 import { mockComponent } from '../../../../../helpers/mocks/component';
27 import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions';
29 PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
30 PERMISSIONS_ORDER_FOR_VIEW,
31 } from '../../../../../helpers/permissions';
34 renderAppWithComponentContext,
35 } from '../../../../../helpers/testReactTestingUtils';
36 import { AlmKeys } from '../../../../../types/alm-settings';
38 ComponentContextShape,
41 } from '../../../../../types/component';
42 import { Feature } from '../../../../../types/features';
43 import { Permissions } from '../../../../../types/permissions';
44 import { Component, PermissionGroup, PermissionUser } from '../../../../../types/types';
45 import { projectPermissionsRoutes } from '../../../routes';
46 import { getPageObject } from '../../../test-utils';
48 let serviceMock: PermissionsServiceMock;
49 let authHandler: AuthenticationServiceMock;
50 let almHandler: AlmSettingsServiceMock;
52 serviceMock = new PermissionsServiceMock();
53 authHandler = new AuthenticationServiceMock();
54 almHandler = new AlmSettingsServiceMock();
63 describe('rendering', () => {
65 [ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE],
66 [ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW],
68 ComponentQualifier.Application,
69 'roles.page.description_application',
70 PERMISSIONS_ORDER_FOR_VIEW,
72 ])('should render correctly for %s', async (qualifier, description, permissions) => {
73 const user = userEvent.setup();
74 const ui = getPageObject(user);
75 renderPermissionsProjectApp({ qualifier, visibility: Visibility.Private });
78 expect(screen.getByText(description)).toBeInTheDocument();
79 permissions.forEach((permission) => {
80 expect(ui.projectPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
85 describe('filtering', () => {
86 it('should allow to filter permission holders', async () => {
87 const user = userEvent.setup();
88 const ui = getPageObject(user);
89 renderPermissionsProjectApp();
92 expect(screen.getByText('sonar-users')).toBeInTheDocument();
93 expect(screen.getByText('johndoe')).toBeInTheDocument();
95 await ui.showOnlyUsers();
96 expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
97 expect(screen.getByText('johndoe')).toBeInTheDocument();
99 await ui.showOnlyGroups();
100 expect(screen.getByText('sonar-users')).toBeInTheDocument();
101 expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
104 expect(screen.getByText('sonar-users')).toBeInTheDocument();
105 expect(screen.getByText('johndoe')).toBeInTheDocument();
107 await ui.searchFor('sonar-adm');
108 expect(screen.getByText('sonar-admins')).toBeInTheDocument();
109 expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
110 expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
112 await ui.clearSearch();
113 expect(screen.getByText('sonar-users')).toBeInTheDocument();
114 expect(screen.getByText('johndoe')).toBeInTheDocument();
117 it('should allow to show only permission holders with a specific permission', async () => {
118 const user = userEvent.setup();
119 const ui = getPageObject(user);
120 renderPermissionsProjectApp();
121 await ui.appLoaded();
123 expect(screen.getAllByRole('row').length).toBe(11);
124 await ui.toggleFilterByPermission(Permissions.Admin);
125 expect(screen.getAllByRole('row').length).toBe(3);
126 await ui.toggleFilterByPermission(Permissions.Admin);
127 expect(screen.getAllByRole('row').length).toBe(11);
131 describe('assigning/revoking permissions', () => {
132 it('should allow to apply a permission template', async () => {
133 const user = userEvent.setup();
134 const ui = getPageObject(user);
135 renderPermissionsProjectApp();
136 await ui.appLoaded();
138 await ui.openTemplateModal();
139 expect(ui.confirmApplyTemplateBtn.get()).toBeDisabled();
140 await ui.chooseTemplate('Permission Template 2');
141 expect(ui.templateSuccessfullyApplied.get()).toBeInTheDocument();
142 await ui.closeTemplateModal();
143 expect(ui.templateSuccessfullyApplied.query()).not.toBeInTheDocument();
146 it('should allow to turn a public project private (and vice-versa)', async () => {
147 const user = userEvent.setup();
148 const ui = getPageObject(user);
149 renderPermissionsProjectApp();
150 await ui.appLoaded();
152 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
154 ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).query(),
155 ).not.toBeInTheDocument();
156 await act(async () => {
157 await ui.turnProjectPrivate();
159 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
161 ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get(),
162 ).toBeInTheDocument();
164 await ui.turnProjectPublic();
165 expect(ui.makePublicDisclaimer.get()).toBeInTheDocument();
166 await act(async () => {
167 await ui.confirmTurnProjectPublic();
169 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
172 it('should add and remove permissions to/from a group', async () => {
173 const user = userEvent.setup();
174 const ui = getPageObject(user);
175 renderPermissionsProjectApp();
176 await ui.appLoaded();
178 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
180 await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
181 await ui.appLoaded();
182 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
184 await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
185 await ui.appLoaded();
186 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
189 it('should add and remove permissions to/from a user', async () => {
190 const user = userEvent.setup();
191 const ui = getPageObject(user);
192 renderPermissionsProjectApp();
193 await ui.appLoaded();
195 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
197 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
198 await ui.appLoaded();
199 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
201 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
202 await ui.appLoaded();
203 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
206 it('should handle errors correctly', async () => {
207 serviceMock.setIsAllowedToChangePermissions(false);
208 const user = userEvent.setup();
209 const ui = getPageObject(user);
210 renderPermissionsProjectApp();
211 await ui.appLoaded();
213 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
214 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
215 await ui.appLoaded();
216 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
220 it('should correctly handle pagination', async () => {
221 const groups: PermissionGroup[] = [];
222 const users: PermissionUser[] = [];
223 Array.from(Array(20).keys()).forEach((i) => {
224 groups.push(mockPermissionGroup({ name: `Group ${i}` }));
225 users.push(mockPermissionUser({ login: `user-${i}` }));
227 serviceMock.setGroups(groups);
228 serviceMock.setUsers(users);
230 const user = userEvent.setup();
231 const ui = getPageObject(user);
232 renderPermissionsProjectApp();
233 await ui.appLoaded();
235 expect(screen.getAllByRole('row').length).toBe(11);
236 await ui.clickLoadMore();
237 expect(screen.getAllByRole('row').length).toBe(21);
240 it('should not allow to change visibility for GH Project with auto-provisioning', async () => {
241 const user = userEvent.setup();
242 const ui = getPageObject(user);
243 authHandler.githubProvisioningStatus = true;
244 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
248 project: 'my-project',
250 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
251 await ui.appLoaded();
253 expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled();
254 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
255 expect(ui.visibilityRadio(Visibility.Private).get()).toBeDisabled();
256 await act(async () => {
257 await ui.turnProjectPrivate();
259 expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked();
262 it('should allow to change visibility for non-GH Project', async () => {
263 const user = userEvent.setup();
264 const ui = getPageObject(user);
265 authHandler.githubProvisioningStatus = true;
266 almHandler.handleSetProjectBinding(AlmKeys.Azure, {
270 project: 'my-project',
272 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
273 await ui.appLoaded();
275 expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
276 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
277 expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
278 await act(async () => {
279 await ui.turnProjectPrivate();
281 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
284 it('should allow to change visibility for GH Project with disabled auto-provisioning', async () => {
285 const user = userEvent.setup();
286 const ui = getPageObject(user);
287 authHandler.githubProvisioningStatus = false;
288 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
292 project: 'my-project',
294 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
295 await ui.appLoaded();
297 expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
298 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
299 expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
300 await act(async () => {
301 await ui.turnProjectPrivate();
303 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
306 it('should have disabled permissions for GH Project', async () => {
307 const user = userEvent.setup();
308 const ui = getPageObject(user);
309 authHandler.githubProvisioningStatus = true;
310 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
314 project: 'my-project',
316 renderPermissionsProjectApp(
318 { featureList: [Feature.GithubProvisioning] },
320 component: mockComponent({ visibility: Visibility.Private }),
323 await ui.appLoaded();
325 expect(ui.pageTitle.get()).toBeInTheDocument();
327 expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/),
329 expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
330 expect(ui.githubExplanations.get()).toBeInTheDocument();
332 expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeChecked();
333 expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toHaveAttribute(
337 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
338 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toHaveAttribute(
342 await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
343 expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
344 expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
345 `${Permissions.IssueAdmin}Alexa`,
348 user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
350 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
352 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
353 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toHaveAttribute(
357 await ui.toggleProjectPermission('sonar-users', Permissions.Browse);
358 expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
359 expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
360 `${Permissions.Browse}sonar-users`,
363 user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
365 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).not.toBeChecked();
366 expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toBeChecked();
367 expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toHaveAttribute(
372 const johnRow = screen.getAllByRole('row')[4];
373 expect(johnRow).toHaveTextContent('John');
374 expect(ui.githubLogo.get(johnRow)).toBeInTheDocument();
375 const alexaRow = screen.getAllByRole('row')[5];
376 expect(alexaRow).toHaveTextContent('Alexa');
377 expect(ui.githubLogo.query(alexaRow)).not.toBeInTheDocument();
378 const usersGroupRow = screen.getAllByRole('row')[1];
379 expect(usersGroupRow).toHaveTextContent('sonar-users');
380 expect(ui.githubLogo.query(usersGroupRow)).not.toBeInTheDocument();
381 const adminsGroupRow = screen.getAllByRole('row')[2];
382 expect(adminsGroupRow).toHaveTextContent('sonar-admins');
383 expect(ui.githubLogo.query(adminsGroupRow)).toBeInTheDocument();
385 expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
387 // not possible to grant permissions at all
390 .getAllByRole('checkbox', { checked: false })
391 .every((item) => item.getAttribute('aria-disabled') === 'true'),
395 it('should allow to change permissions for GH Project without auto-provisioning', async () => {
396 const user = userEvent.setup();
397 const ui = getPageObject(user);
398 authHandler.githubProvisioningStatus = false;
399 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
403 project: 'my-project',
405 renderPermissionsProjectApp(
406 { visibility: Visibility.Private },
407 { featureList: [Feature.GithubProvisioning] },
409 await ui.appLoaded();
411 expect(ui.pageTitle.get()).toBeInTheDocument();
412 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
414 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
418 screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true'),
422 it('should allow to change permissions for non-GH Project', async () => {
423 const user = userEvent.setup();
424 const ui = getPageObject(user);
425 authHandler.githubProvisioningStatus = true;
426 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
427 await ui.appLoaded();
429 expect(ui.pageTitle.get()).toBeInTheDocument();
430 expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
431 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
433 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
437 screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true'),
441 function renderPermissionsProjectApp(
442 override: Partial<Component> = {},
443 contextOverride: Partial<RenderContext> = {},
444 componentContextOverride: Partial<ComponentContextShape> = {},
446 return renderAppWithComponentContext(
447 'project_roles?id=my-project',
448 projectPermissionsRoutes,
451 component: mockComponent({
452 visibility: Visibility.Public,
454 canUpdateProjectVisibilityToPrivate: true,
455 canApplyPermissionTemplate: true,
459 ...componentContextOverride,