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 } 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';
28 PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
29 PERMISSIONS_ORDER_FOR_VIEW,
30 } from '../../../../../helpers/permissions';
33 renderAppWithComponentContext,
34 } from '../../../../../helpers/testReactTestingUtils';
35 import { AlmKeys } from '../../../../../types/alm-settings';
37 ComponentContextShape,
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';
47 let serviceMock: PermissionsServiceMock;
48 let authHandler: AuthenticationServiceMock;
50 serviceMock = new PermissionsServiceMock();
51 authHandler = new AuthenticationServiceMock();
59 describe('rendering', () => {
61 [ComponentQualifier.Project, 'roles.page.description2', PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE],
62 [ComponentQualifier.Portfolio, 'roles.page.description_portfolio', PERMISSIONS_ORDER_FOR_VIEW],
64 ComponentQualifier.Application,
65 'roles.page.description_application',
66 PERMISSIONS_ORDER_FOR_VIEW,
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 });
74 expect(screen.getByText(description)).toBeInTheDocument();
75 permissions.forEach((permission) => {
76 expect(ui.projectPermissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
81 describe('filtering', () => {
82 it('should allow to filter permission holders', async () => {
83 const user = userEvent.setup();
84 const ui = getPageObject(user);
85 renderPermissionsProjectApp();
88 expect(screen.getByText('sonar-users')).toBeInTheDocument();
89 expect(screen.getByText('johndoe')).toBeInTheDocument();
91 await ui.showOnlyUsers();
92 expect(screen.queryByText('sonar-users')).not.toBeInTheDocument();
93 expect(screen.getByText('johndoe')).toBeInTheDocument();
95 await ui.showOnlyGroups();
96 expect(screen.getByText('sonar-users')).toBeInTheDocument();
97 expect(screen.queryByText('johndoe')).not.toBeInTheDocument();
100 expect(screen.getByText('sonar-users')).toBeInTheDocument();
101 expect(screen.getByText('johndoe')).toBeInTheDocument();
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();
108 await ui.clearSearch();
109 expect(screen.getByText('sonar-users')).toBeInTheDocument();
110 expect(screen.getByText('johndoe')).toBeInTheDocument();
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();
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);
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();
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();
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();
148 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
150 ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).query()
151 ).not.toBeInTheDocument();
152 await act(async () => {
153 await ui.turnProjectPrivate();
155 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
157 ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()
158 ).toBeInTheDocument();
160 await ui.turnProjectPublic();
161 expect(ui.makePublicDisclaimer.get()).toBeInTheDocument();
162 await act(async () => {
163 await ui.confirmTurnProjectPublic();
165 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
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();
174 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
176 await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
177 await ui.appLoaded();
178 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).toBeChecked();
180 await ui.toggleProjectPermission('sonar-users', Permissions.Admin);
181 await ui.appLoaded();
182 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Admin).get()).not.toBeChecked();
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();
191 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
193 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
194 await ui.appLoaded();
195 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).toBeChecked();
197 await ui.toggleProjectPermission('johndoe', Permissions.Scan);
198 await ui.appLoaded();
199 expect(ui.projectPermissionCheckbox('johndoe', Permissions.Scan).get()).not.toBeChecked();
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();
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();
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}` }));
223 serviceMock.setGroups(groups);
224 serviceMock.setUsers(users);
226 const user = userEvent.setup();
227 const ui = getPageObject(user);
228 renderPermissionsProjectApp();
229 await ui.appLoaded();
231 expect(screen.getAllByRole('row').length).toBe(11);
232 await ui.clickLoadMore();
233 expect(screen.getAllByRole('row').length).toBe(21);
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(
242 { featureList: [Feature.GithubProvisioning] },
243 { projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false } }
245 await ui.appLoaded();
247 expect(ui.visibilityRadio(Visibility.Public).get()).toHaveClass('disabled');
248 expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
249 expect(ui.visibilityRadio(Visibility.Private).get()).toHaveClass('disabled');
250 await act(async () => {
251 await ui.turnProjectPrivate();
253 expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked();
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(
262 { featureList: [Feature.GithubProvisioning] },
263 { projectBinding: { alm: AlmKeys.Azure, key: 'test', repository: 'test', monorepo: false } }
265 await ui.appLoaded();
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();
273 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
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(
282 { featureList: [Feature.GithubProvisioning] },
283 { projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false } }
285 await ui.appLoaded();
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();
293 expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
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(
302 { featureList: [Feature.GithubProvisioning] },
304 component: mockComponent({ visibility: Visibility.Private }),
305 projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false },
308 await ui.appLoaded();
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();
315 expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toBeChecked();
316 expect(ui.projectPermissionCheckbox('John', Permissions.Admin).get()).toHaveAttribute(
320 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toBeChecked();
321 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).toHaveAttribute(
325 await ui.toggleProjectPermission('Alexa', Permissions.IssueAdmin);
326 expect(ui.confirmRemovePermissionDialog.get()).toBeInTheDocument();
327 expect(ui.confirmRemovePermissionDialog.get()).toHaveTextContent(
328 `${Permissions.IssueAdmin}Alexa`
331 user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get())
333 expect(ui.projectPermissionCheckbox('Alexa', Permissions.IssueAdmin).get()).not.toBeChecked();
335 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toBeChecked();
336 expect(ui.projectPermissionCheckbox('sonar-users', Permissions.Browse).get()).toHaveAttribute(
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`
346 user.click(ui.confirmRemovePermissionDialog.byRole('button', { name: 'confirm' }).get())
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(
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();
368 expect(ui.applyTemplateBtn.query()).not.toBeInTheDocument();
370 // not possible to grant permissions at all
373 .getAllByRole('checkbox', { checked: false })
374 .every((item) => item.getAttribute('aria-disabled') === 'true')
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] },
386 projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false },
389 await ui.appLoaded();
391 expect(ui.pageTitle.get()).toBeInTheDocument();
392 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
394 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
398 screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true')
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();
409 expect(ui.pageTitle.get()).toBeInTheDocument();
410 expect(ui.nonGHProjectWarning.get()).toBeInTheDocument();
411 expect(ui.pageTitle.byRole('img').query()).not.toBeInTheDocument();
413 expect(ui.applyTemplateBtn.get()).toBeInTheDocument();
417 screen.getAllByRole('checkbox').every((item) => item.getAttribute('aria-disabled') !== 'true')
421 function renderPermissionsProjectApp(
422 override: Partial<Component> = {},
423 contextOverride: Partial<RenderContext> = {},
424 componentContextOverride: Partial<ComponentContextShape> = {}
426 return renderAppWithComponentContext('project_roles', projectPermissionsRoutes, contextOverride, {
427 component: mockComponent({
428 visibility: Visibility.Public,
430 canUpdateProjectVisibilityToPrivate: true,
431 canApplyPermissionTemplate: true,
435 ...componentContextOverride,