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(10);
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(10);
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()).toBeDisabled();
334 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
335 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeEnabled();
336 await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
337 expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
338 expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
339 `${Permissions.IssueAdmin}Alexa`,
342 user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
344 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
346 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
347 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeEnabled();
348 await ui.toggleProjectPermission('sonar-users', Permissions.Browse);
349 expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
350 expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
351 `${Permissions.Browse}sonar-users`,
354 user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get()),
356 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).not.toBeChecked();
357 expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toBeChecked();
358 expect(ui.projectPermissionCheckbox('sonar-admins', Permissions.Admin).get()).toHaveAttribute(
362 const johnRow = screen.getAllByRole('row')[4];
363 expect(johnRow).toHaveTextContent('John');
364 expect(ui.githubLogo.get(johnRow)).toBeInTheDocument();
365 const alexaRow = screen.getAllByRole('row')[5];
366 expect(alexaRow).toHaveTextContent('Alexa');
367 expect(ui.githubLogo.query(alexaRow)).not.toBeInTheDocument();
368 const usersGroupRow = screen.getAllByRole('row')[1];
369 expect(usersGroupRow).toHaveTextContent('sonar-users');
370 expect(ui.githubLogo.query(usersGroupRow)).not.toBeInTheDocument();
371 const adminsGroupRow = screen.getAllByRole('row')[2];
372 expect(adminsGroupRow).toHaveTextContent('sonar-admins');
373 expect(ui.githubLogo.query(adminsGroupRow)).toBeInTheDocument();
375 expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
377 // not possible to grant permissions at all
380 .getAllByRole('checkbox', { checked: false })
381 .every((item) => item.getAttributeNames().includes('disabled')),
385 it('should allow to change permissions for GH Project without auto-provisioning', async () => {
386 const user = userEvent.setup();
387 const ui = getPageObject(user);
388 authHandler.githubProvisioningStatus = false;
389 almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
393 project: 'my-project',
395 renderPermissionsProjectApp(
396 { visibility: Visibility.Private },
397 { featureList: [Feature.GithubProvisioning] },
399 await ui.appLoaded();
401 expect(ui.pageTitle.get()).toBeInTheDocument();
402 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
404 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
408 screen.getAllByRole('checkbox').every((item) => item.getAttributeNames().includes('disabled')),
412 it('should allow to change permissions for non-GH Project', async () => {
413 const user = userEvent.setup();
414 const ui = getPageObject(user);
415 authHandler.githubProvisioningStatus = true;
416 renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
417 await ui.appLoaded();
419 expect(ui.pageTitle.get()).toBeInTheDocument();
420 expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
421 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
423 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
427 screen.getAllByRole('checkbox').every((item) => item.getAttributeNames().includes('disabled')),
431 function renderPermissionsProjectApp(
432 override: Partial<Component> = {},
433 contextOverride: Partial<RenderContext> = {},
434 componentContextOverride: Partial<ComponentContextShape> = {},
436 return renderAppWithComponentContext(
437 'project_roles?id=my-project',
438 projectPermissionsRoutes,
441 component: mockComponent({
442 visibility: Visibility.Public,
444 canUpdateProjectVisibilityToPrivate: true,
445 canApplyPermissionTemplate: true,
449 ...componentContextOverride,