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.
20 import { act, screen, waitFor, within } from '@testing-library/react';
21 import userEvent from '@testing-library/user-event';
22 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
23 import React from 'react';
24 import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
25 import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
26 import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
27 import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
28 import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
29 import { definitions } from '../../../../../helpers/mocks/definitions-list';
30 import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
31 import { byRole, byText } from '../../../../../helpers/testSelector';
32 import { Feature } from '../../../../../types/features';
33 import { GitHubProvisioningStatus } from '../../../../../types/provisioning';
34 import { TaskStatuses } from '../../../../../types/tasks';
35 import Authentication from '../Authentication';
37 jest.mock('../../../../../api/system');
39 let handler: AuthenticationServiceMock;
40 let system: SystemServiceMock;
41 let settingsHandler: SettingsServiceMock;
42 let computeEngineHandler: ComputeEngineServiceMock;
45 handler = new AuthenticationServiceMock();
46 system = new SystemServiceMock();
47 settingsHandler = new SettingsServiceMock();
48 computeEngineHandler = new ComputeEngineServiceMock();
51 key: 'sonar.auth.saml.signature.enabled',
55 key: 'sonar.auth.saml.enabled',
59 key: 'sonar.auth.saml.applicationId',
63 key: 'sonar.auth.saml.providerName',
66 ].forEach((setting: any) => settingsHandler.set(setting.key, setting.value));
71 settingsHandler.reset();
73 computeEngineHandler.reset();
77 saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }),
78 customMessageInformation: byText('settings.authentication.custom_message_information'),
79 enabledToggle: byRole('switch'),
80 testButton: byText('settings.authentication.saml.form.test'),
81 textbox1: byRole('textbox', { name: 'test1' }),
82 textbox2: byRole('textbox', { name: 'test2' }),
84 noSamlConfiguration: byText('settings.authentication.saml.form.not_configured'),
85 createConfigButton: byRole('button', { name: 'settings.authentication.form.create' }),
86 providerName: byRole('textbox', { name: 'property.sonar.auth.saml.providerName.name' }),
87 providerId: byRole('textbox', { name: 'property.sonar.auth.saml.providerId.name' }),
88 providerCertificate: byRole('textbox', {
89 name: 'property.sonar.auth.saml.certificate.secured.name',
91 loginUrl: byRole('textbox', { name: 'property.sonar.auth.saml.loginUrl.name' }),
92 userLoginAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.login.name' }),
93 userNameAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.name.name' }),
94 saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
95 confirmProvisioningButton: byRole('button', { name: 'yes' }),
96 saveScim: byRole('button', { name: 'save' }),
97 enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
98 disableConfigButton: byRole('button', { name: 'settings.authentication.form.disable' }),
99 editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
100 enableFirstMessage: byText('settings.authentication.saml.enable_first'),
101 jitProvisioningButton: byRole('radio', {
102 name: 'settings.authentication.saml.form.provisioning_at_login',
104 scimProvisioningButton: byRole('radio', {
105 name: 'settings.authentication.saml.form.provisioning_with_scim',
107 fillForm: async (user: UserEvent) => {
109 await act(async () => {
110 await user.clear(saml.providerName.get());
111 await user.type(saml.providerName.get(), 'Awsome SAML config');
112 await user.type(saml.providerId.get(), 'okta-1234');
113 await user.type(saml.loginUrl.get(), 'http://test.org');
114 await user.type(saml.providerCertificate.get(), '-secret-');
115 await user.type(saml.userLoginAttribute.get(), 'login');
116 await user.type(saml.userNameAttribute.get(), 'name');
119 createConfiguration: async (user: UserEvent) => {
121 await act(async () => {
122 await user.click((await saml.createConfigButton.findAll())[0]);
124 await saml.fillForm(user);
125 await act(async () => {
126 await user.click(saml.saveConfigButton.get());
131 tab: byRole('tab', { name: 'github GitHub' }),
132 noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
133 createConfigButton: byRole('button', { name: 'settings.authentication.form.create' }),
134 clientId: byRole('textbox', { name: 'property.sonar.auth.github.clientId.secured.name' }),
135 clientSecret: byRole('textbox', {
136 name: 'property.sonar.auth.github.clientSecret.secured.name',
138 githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }),
139 githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }),
140 allowUserToSignUp: byRole('switch', {
141 name: 'sonar.auth.github.allowUsersToSignUp',
143 organizations: byRole('textbox', { name: 'property.sonar.auth.github.organizations.name' }),
144 saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
145 confirmProvisioningButton: byRole('button', { name: 'yes' }),
146 saveGithubProvisioning: byRole('button', { name: 'save' }),
147 groupAttribute: byRole('textbox', { name: 'property.sonar.auth.github.group.name.name' }),
148 enableConfigButton: byRole('button', { name: 'settings.authentication.form.enable' }),
149 disableConfigButton: byRole('button', { name: 'settings.authentication.form.disable' }),
150 editConfigButton: byRole('button', { name: 'settings.authentication.form.edit' }),
151 editMappingButton: byRole('button', {
152 name: 'settings.authentication.github.configuration.roles_mapping.button_label',
154 mappingRow: byRole('dialog', {
155 name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
157 customRoleInput: byRole('textbox', {
158 name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role',
160 customRoleAddBtn: byRole('dialog', {
161 name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
162 }).byRole('button', { name: 'add_verb' }),
163 roleExistsError: byRole('dialog', {
164 name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
165 }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'),
166 emptyRoleError: byRole('dialog', {
167 name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
168 }).byText('settings.authentication.github.configuration.roles_mapping.empty_custom_role'),
169 deleteCustomRoleCustom2: byRole('button', {
170 name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2',
172 getMappingRowByRole: (text: string) =>
173 ui.github.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
174 mappingCheckbox: byRole('checkbox'),
175 mappingDialogClose: byRole('dialog', {
176 name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
177 }).byRole('button', {
180 deleteOrg: (org: string) =>
182 name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
184 enableFirstMessage: byText('settings.authentication.github.enable_first'),
185 jitProvisioningButton: byRole('radio', {
186 name: 'settings.authentication.form.provisioning_at_login',
188 githubProvisioningButton: byRole('radio', {
189 name: 'settings.authentication.github.form.provisioning_with_github',
191 githubProvisioningPending: byText(/synchronization_pending/),
192 githubProvisioningInProgress: byText(/synchronization_in_progress/),
193 githubProvisioningSuccess: byText(/synchronization_successful/),
194 githubProvisioningAlert: byText(/synchronization_failed/),
195 configurationValidityLoading: byRole('status', {
196 name: /github.configuration.validation.loading/,
198 configurationValiditySuccess: byRole('status', {
199 name: /github.configuration.validation.valid/,
201 configurationValidityError: byRole('status', {
202 name: /github.configuration.validation.invalid/,
204 syncWarning: byText(/Warning/),
205 syncSummary: byText(/Test summary/),
206 configurationValidityWarning: byRole('status', {
207 name: /github.configuration.validation.valid.short/,
209 checkConfigButton: byRole('button', {
210 name: 'settings.authentication.github.configuration.validation.test',
212 viewConfigValidityDetailsButton: byRole('button', {
213 name: 'settings.authentication.github.configuration.validation.details',
215 configDetailsDialog: byRole('dialog', {
216 name: 'settings.authentication.github.configuration.validation.details.title',
218 continueAutoButton: byRole('button', {
219 name: 'settings.authentication.github.confirm_auto_provisioning.continue',
221 switchJitButton: byRole('button', {
222 name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
224 consentDialog: byRole('dialog', {
225 name: 'settings.authentication.github.confirm_auto_provisioning.header',
227 getConfigDetailsTitle: () => within(ui.github.configDetailsDialog.get()).getByRole('heading'),
228 getOrgs: () => within(ui.github.configDetailsDialog.get()).getAllByRole('listitem'),
229 fillForm: async (user: UserEvent) => {
230 const { github } = ui;
231 await act(async () => {
232 await user.type(await github.clientId.find(), 'Awsome GITHUB config');
233 await user.type(github.clientSecret.get(), 'Client shut');
234 await user.type(github.githubApiUrl.get(), 'API Url');
235 await user.type(github.githubWebUrl.get(), 'WEb Url');
236 await user.type(github.organizations.get(), 'organization1');
239 createConfiguration: async (user: UserEvent) => {
240 const { github } = ui;
241 await act(async () => {
242 await user.click((await github.createConfigButton.findAll())[1]);
244 await github.fillForm(user);
245 await act(async () => {
246 await user.click(github.saveConfigButton.get());
249 enableConfiguration: async (user: UserEvent) => {
250 const { github } = ui;
251 await act(async () => user.click(await github.tab.find()));
252 await github.createConfiguration(user);
253 await act(async () => user.click(await github.enableConfigButton.find()));
255 enableProvisioning: async (user: UserEvent) => {
256 const { github } = ui;
257 await act(async () => user.click(await github.tab.find()));
259 await github.createConfiguration(user);
261 await act(async () => user.click(await github.enableConfigButton.find()));
262 await user.click(await github.githubProvisioningButton.find());
263 await user.click(github.saveGithubProvisioning.get());
264 await act(() => user.click(github.confirmProvisioningButton.get()));
269 it('should render tabs and allow navigation', async () => {
270 const user = userEvent.setup();
271 renderAuthentication();
273 expect(screen.getAllByRole('tab')).toHaveLength(4);
275 expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-selected', 'true');
277 await user.click(screen.getByRole('tab', { name: 'github GitHub' }));
279 expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-selected', 'false');
280 expect(screen.getByRole('tab', { name: 'github GitHub' })).toHaveAttribute(
286 it('should not display the login message feature info box', () => {
287 renderAuthentication();
289 expect(ui.customMessageInformation.query()).not.toBeInTheDocument();
292 it('should display the login message feature info box', () => {
293 renderAuthentication([Feature.LoginMessage]);
295 expect(ui.customMessageInformation.get()).toBeInTheDocument();
298 describe('SAML tab', () => {
301 it('should render an empty SAML configuration', async () => {
302 renderAuthentication();
303 expect(await saml.noSamlConfiguration.find()).toBeInTheDocument();
306 it('should be able to create a configuration', async () => {
307 const user = userEvent.setup();
308 renderAuthentication();
310 await user.click((await saml.createConfigButton.findAll())[0]);
312 expect(saml.saveConfigButton.get()).toBeDisabled();
313 await saml.fillForm(user);
314 expect(saml.saveConfigButton.get()).toBeEnabled();
316 await act(async () => {
317 await user.click(saml.saveConfigButton.get());
320 expect(await saml.editConfigButton.find()).toBeInTheDocument();
323 it('should be able to enable/disable configuration', async () => {
325 const user = userEvent.setup();
326 renderAuthentication();
328 await saml.createConfiguration(user);
329 await user.click(await saml.enableConfigButton.find());
331 expect(await saml.disableConfigButton.find()).toBeInTheDocument();
332 await user.click(saml.disableConfigButton.get());
333 await waitFor(() => expect(saml.disableConfigButton.query()).not.toBeInTheDocument());
335 expect(await saml.enableConfigButton.find()).toBeInTheDocument();
338 it('should be able to choose provisioning', async () => {
340 const user = userEvent.setup();
342 renderAuthentication([Feature.Scim]);
344 await saml.createConfiguration(user);
346 expect(await saml.enableFirstMessage.find()).toBeInTheDocument();
347 await user.click(await saml.enableConfigButton.find());
349 expect(await saml.jitProvisioningButton.find()).toBeChecked();
350 expect(saml.saveScim.get()).toBeDisabled();
352 await user.click(saml.scimProvisioningButton.get());
353 expect(saml.saveScim.get()).toBeEnabled();
354 await user.click(saml.saveScim.get());
355 await user.click(saml.confirmProvisioningButton.get());
357 expect(await saml.scimProvisioningButton.find()).toBeChecked();
358 expect(await saml.saveScim.find()).toBeDisabled();
361 it('should not allow editions below Enterprise to select SCIM provisioning', async () => {
363 const user = userEvent.setup();
365 renderAuthentication();
367 await saml.createConfiguration(user);
368 await user.click(await saml.enableConfigButton.find());
370 expect(await saml.jitProvisioningButton.find()).toBeChecked();
371 expect(saml.scimProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
375 describe('Github tab', () => {
376 const { github } = ui;
378 it('should render an empty Github configuration', async () => {
379 renderAuthentication();
380 const user = userEvent.setup();
381 await user.click(await github.tab.find());
382 expect(await github.noGithubConfiguration.find()).toBeInTheDocument();
385 it('should be able to create a configuration', async () => {
386 const user = userEvent.setup();
387 renderAuthentication();
389 await user.click(await github.tab.find());
390 await user.click((await github.createConfigButton.findAll())[1]);
392 expect(github.saveConfigButton.get()).toBeDisabled();
394 await github.fillForm(user);
395 expect(github.saveConfigButton.get()).toBeEnabled();
397 await act(async () => {
398 await user.click(github.saveConfigButton.get());
401 expect(await github.editConfigButton.find()).toBeInTheDocument();
404 it('should be able to edit configuration', async () => {
405 const { github } = ui;
406 const user = userEvent.setup();
407 renderAuthentication();
408 await user.click(await github.tab.find());
410 await github.createConfiguration(user);
412 await user.click(github.editConfigButton.get());
413 await user.click(github.deleteOrg('organization1').get());
415 await user.click(github.saveConfigButton.get());
417 await user.click(await github.editConfigButton.find());
419 expect(github.organizations.get()).toHaveValue('');
422 it('should be able to enable/disable configuration', async () => {
423 const { github } = ui;
424 const user = userEvent.setup();
425 renderAuthentication();
426 await user.click(await github.tab.find());
428 await github.createConfiguration(user);
430 await user.click(await github.enableConfigButton.find());
432 expect(await github.disableConfigButton.find()).toBeInTheDocument();
433 await user.click(github.disableConfigButton.get());
434 await waitFor(() => expect(github.disableConfigButton.query()).not.toBeInTheDocument());
436 expect(await github.enableConfigButton.find()).toBeInTheDocument();
439 it('should not allow edtion below Enterprise to select Github provisioning', async () => {
440 const { github } = ui;
441 const user = userEvent.setup();
443 renderAuthentication();
444 await user.click(await github.tab.find());
446 await github.createConfiguration(user);
447 await user.click(await github.enableConfigButton.find());
449 expect(await github.jitProvisioningButton.find()).toBeChecked();
450 expect(github.githubProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
453 it('should be able to choose provisioning', async () => {
454 const { github } = ui;
455 const user = userEvent.setup();
457 renderAuthentication([Feature.GithubProvisioning]);
458 await user.click(await github.tab.find());
460 await github.createConfiguration(user);
462 expect(await github.enableFirstMessage.find()).toBeInTheDocument();
463 await user.click(await github.enableConfigButton.find());
465 expect(await github.jitProvisioningButton.find()).toBeChecked();
467 expect(github.saveGithubProvisioning.get()).toBeDisabled();
468 await user.click(github.allowUserToSignUp.get());
470 expect(github.saveGithubProvisioning.get()).toBeEnabled();
471 await user.click(github.saveGithubProvisioning.get());
473 await waitFor(() => expect(github.saveGithubProvisioning.query()).toBeDisabled());
475 await user.click(github.githubProvisioningButton.get());
477 expect(github.saveGithubProvisioning.get()).toBeEnabled();
478 await user.click(github.saveGithubProvisioning.get());
479 await user.click(github.confirmProvisioningButton.get());
481 expect(await github.githubProvisioningButton.find()).toBeChecked();
482 expect(github.disableConfigButton.get()).toBeDisabled();
483 expect(github.saveGithubProvisioning.get()).toBeDisabled();
486 describe('Github Provisioning', () => {
492 now: new Date('2022-02-04T12:00:59Z'),
494 user = userEvent.setup();
498 jest.runOnlyPendingTimers();
499 jest.useRealTimers();
502 it('should display a success status when the synchronisation is a success', async () => {
503 handler.addProvisioningTask({
504 status: TaskStatuses.Success,
505 executedAt: '2022-02-03T11:45:35+0200',
508 renderAuthentication([Feature.GithubProvisioning]);
509 await github.enableProvisioning(user);
510 expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
511 expect(github.syncSummary.get()).toBeInTheDocument();
514 it('should display a success status even when another task is pending', async () => {
515 handler.addProvisioningTask({
516 status: TaskStatuses.Pending,
517 executedAt: '2022-02-03T11:55:35+0200',
519 handler.addProvisioningTask({
520 status: TaskStatuses.Success,
521 executedAt: '2022-02-03T11:45:35+0200',
523 renderAuthentication([Feature.GithubProvisioning]);
524 await github.enableProvisioning(user);
525 expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
526 expect(github.githubProvisioningPending.get()).toBeInTheDocument();
529 it('should display an error alert when the synchronisation failed', async () => {
530 handler.addProvisioningTask({
531 status: TaskStatuses.Failed,
532 executedAt: '2022-02-03T11:45:35+0200',
533 errorMessage: "T'es mauvais Jacques",
535 renderAuthentication([Feature.GithubProvisioning]);
536 await github.enableProvisioning(user);
537 expect(github.githubProvisioningAlert.get()).toBeInTheDocument();
538 expect(github.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
539 expect(github.githubProvisioningSuccess.query()).not.toBeInTheDocument();
542 it('should display an error alert even when another task is in progress', async () => {
543 handler.addProvisioningTask({
544 status: TaskStatuses.InProgress,
545 executedAt: '2022-02-03T11:55:35+0200',
547 handler.addProvisioningTask({
548 status: TaskStatuses.Failed,
549 executedAt: '2022-02-03T11:45:35+0200',
550 errorMessage: "T'es mauvais Jacques",
552 renderAuthentication([Feature.GithubProvisioning]);
553 await github.enableProvisioning(user);
554 expect(github.githubProvisioningAlert.get()).toBeInTheDocument();
555 expect(github.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
556 expect(github.githubProvisioningSuccess.query()).not.toBeInTheDocument();
557 expect(github.githubProvisioningInProgress.get()).toBeInTheDocument();
560 it('should display that config is valid for both provisioning with 1 org', async () => {
561 renderAuthentication([Feature.GithubProvisioning]);
562 await github.enableConfiguration(user);
564 await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
567 it('should display that config is valid for both provisioning with multiple orgs', async () => {
568 handler.setConfigurationValidity({
571 organization: 'org1',
572 autoProvisioning: { status: GitHubProvisioningStatus.Success },
573 jit: { status: GitHubProvisioningStatus.Success },
576 organization: 'org2',
577 autoProvisioning: { status: GitHubProvisioningStatus.Success },
578 jit: { status: GitHubProvisioningStatus.Success },
582 renderAuthentication([Feature.GithubProvisioning]);
583 await github.enableConfiguration(user);
585 await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
586 expect(github.configurationValiditySuccess.get()).toHaveTextContent('2');
588 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
589 expect(github.getConfigDetailsTitle()).toHaveTextContent(
590 'settings.authentication.github.configuration.validation.details.valid_label',
592 expect(github.getOrgs()[0]).toHaveTextContent(
593 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
595 expect(github.getOrgs()[1]).toHaveTextContent(
596 'settings.authentication.github.configuration.validation.details.valid_labelorg2',
600 it('should display that config is invalid for autoprovisioning if some apps are suspended but valid for jit', async () => {
601 const errorMessage = 'Installation suspended';
602 handler.setConfigurationValidity({
605 organization: 'org1',
607 status: GitHubProvisioningStatus.Failed,
611 status: GitHubProvisioningStatus.Failed,
618 renderAuthentication([Feature.GithubProvisioning]);
619 await github.enableConfiguration(user);
621 await waitFor(() => expect(github.configurationValidityWarning.get()).toBeInTheDocument());
622 expect(github.configurationValidityWarning.get()).toHaveTextContent(errorMessage);
624 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
625 expect(github.getConfigDetailsTitle()).toHaveTextContent(
626 'settings.authentication.github.configuration.validation.details.valid_label',
628 expect(github.getOrgs()[0]).toHaveTextContent(
629 'settings.authentication.github.configuration.validation.details.invalid_labelorg1 - Installation suspended',
633 user.click(within(github.configDetailsDialog.get()).getByRole('button', { name: 'close' })),
636 await user.click(github.githubProvisioningButton.get());
637 await waitFor(() => expect(github.configurationValidityError.get()).toBeInTheDocument());
638 expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
641 it('should display that config is valid but some organizations were not found', async () => {
642 handler.setConfigurationValidity({
645 organization: 'org1',
646 autoProvisioning: { status: GitHubProvisioningStatus.Success },
647 jit: { status: GitHubProvisioningStatus.Success },
652 renderAuthentication([Feature.GithubProvisioning]);
653 await github.enableConfiguration(user);
655 await waitFor(() => expect(github.configurationValiditySuccess.get()).toBeInTheDocument());
656 expect(github.configurationValiditySuccess.get()).toHaveTextContent('1');
658 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
659 expect(github.getConfigDetailsTitle()).toHaveTextContent(
660 'settings.authentication.github.configuration.validation.details.valid_label',
662 expect(github.getOrgs()[0]).toHaveTextContent(
663 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
665 expect(github.getOrgs()[1]).toHaveTextContent(
666 'settings.authentication.github.configuration.validation.details.org_not_found.organization1',
670 it('should display that config is invalid', async () => {
671 const errorMessage = 'Test error';
672 handler.setConfigurationValidity({
675 status: GitHubProvisioningStatus.Failed,
679 status: GitHubProvisioningStatus.Failed,
684 renderAuthentication([Feature.GithubProvisioning]);
685 await github.enableConfiguration(user);
687 await waitFor(() => expect(github.configurationValidityError.query()).toBeInTheDocument());
688 expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
690 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
691 expect(github.getConfigDetailsTitle()).toHaveTextContent(
692 'settings.authentication.github.configuration.validation.details.invalid_label',
694 expect(github.configDetailsDialog.get()).toHaveTextContent(errorMessage);
697 it('should display that config is valid for jit, but not for auto', async () => {
698 const errorMessage = 'Test error';
699 handler.setConfigurationValidity({
702 status: GitHubProvisioningStatus.Success,
705 status: GitHubProvisioningStatus.Failed,
710 renderAuthentication([Feature.GithubProvisioning]);
711 await github.enableConfiguration(user);
713 await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
714 expect(github.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
716 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
717 expect(github.getConfigDetailsTitle()).toHaveTextContent(
718 'settings.authentication.github.configuration.validation.details.valid_label',
721 user.click(within(github.configDetailsDialog.get()).getByRole('button', { name: 'close' })),
724 await act(() => user.click(github.githubProvisioningButton.get()));
726 expect(github.configurationValidityError.get()).toBeInTheDocument();
727 expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
729 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
730 expect(github.getConfigDetailsTitle()).toHaveTextContent(
731 'settings.authentication.github.configuration.validation.details.invalid_label',
735 it('should display that config is invalid because of orgs', async () => {
736 const errorMessage = 'Test error';
737 handler.setConfigurationValidity({
740 organization: 'org1',
741 autoProvisioning: { status: GitHubProvisioningStatus.Success },
742 jit: { status: GitHubProvisioningStatus.Success },
745 organization: 'org2',
746 jit: { status: GitHubProvisioningStatus.Failed, errorMessage },
747 autoProvisioning: { status: GitHubProvisioningStatus.Failed, errorMessage },
751 renderAuthentication([Feature.GithubProvisioning]);
752 await github.enableConfiguration(user);
754 await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
756 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
758 expect(github.getOrgs()[0]).toHaveTextContent(
759 'settings.authentication.github.configuration.validation.details.valid_labelorg1',
761 expect(github.getOrgs()[1]).toHaveTextContent(
762 'settings.authentication.github.configuration.validation.details.invalid_labelorg2 - Test error',
766 user.click(within(github.configDetailsDialog.get()).getByRole('button', { name: 'close' })),
769 await act(() => user.click(github.githubProvisioningButton.get()));
771 expect(github.configurationValidityError.get()).toBeInTheDocument();
772 expect(github.configurationValidityError.get()).toHaveTextContent(
773 `settings.authentication.github.configuration.validation.invalid_org.org2.${errorMessage}`,
775 await act(() => user.click(github.viewConfigValidityDetailsButton.get()));
776 expect(github.getOrgs()[1]).toHaveTextContent(
777 `settings.authentication.github.configuration.validation.details.invalid_labelorg2 - ${errorMessage}`,
781 it('should update provisioning validity after clicking Test Configuration', async () => {
782 const errorMessage = 'Test error';
783 handler.setConfigurationValidity({
786 status: GitHubProvisioningStatus.Failed,
790 status: GitHubProvisioningStatus.Failed,
795 renderAuthentication([Feature.GithubProvisioning]);
796 await github.enableConfiguration(user);
797 handler.setConfigurationValidity({
800 status: GitHubProvisioningStatus.Success,
803 status: GitHubProvisioningStatus.Success,
808 expect(await github.configurationValidityError.find()).toBeInTheDocument();
810 await act(() => user.click(github.checkConfigButton.get()));
812 expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
813 expect(github.configurationValidityError.query()).not.toBeInTheDocument();
816 it('should show warning', async () => {
817 handler.addProvisioningTask({
818 status: TaskStatuses.Success,
819 warnings: ['Warning'],
821 renderAuthentication([Feature.GithubProvisioning]);
822 await github.enableProvisioning(user);
824 expect(await github.syncWarning.find()).toBeInTheDocument();
825 expect(github.syncSummary.get()).toBeInTheDocument();
828 it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
829 const user = userEvent.setup();
830 settingsHandler.presetGithubAutoProvisioning();
831 handler.enableGithubProvisioning();
832 settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
833 renderAuthentication([Feature.GithubProvisioning]);
835 await user.click(await github.tab.find());
837 expect(await github.consentDialog.find()).toBeInTheDocument();
838 await user.click(github.continueAutoButton.get());
840 expect(await github.githubProvisioningButton.find()).toBeChecked();
841 expect(github.consentDialog.query()).not.toBeInTheDocument();
844 it('should display a modal if user was already using auto and switch to JIT', async () => {
845 const user = userEvent.setup();
846 settingsHandler.presetGithubAutoProvisioning();
847 handler.enableGithubProvisioning();
848 settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
849 renderAuthentication([Feature.GithubProvisioning]);
851 await user.click(await github.tab.find());
853 expect(await github.consentDialog.find()).toBeInTheDocument();
854 await user.click(github.switchJitButton.get());
856 expect(await github.jitProvisioningButton.find()).toBeChecked();
857 expect(github.consentDialog.query()).not.toBeInTheDocument();
860 it('should sort mapping rows', async () => {
861 const user = userEvent.setup();
862 settingsHandler.presetGithubAutoProvisioning();
863 handler.enableGithubProvisioning();
864 renderAuthentication([Feature.GithubProvisioning]);
865 await user.click(await github.tab.find());
867 expect(await github.editMappingButton.find()).toBeInTheDocument();
868 await user.click(github.editMappingButton.get());
870 const rows = (await github.mappingRow.findAll()).filter(
871 (row) => within(row).queryAllByRole('checkbox').length > 0,
874 expect(rows).toHaveLength(5);
876 expect(rows[0]).toHaveTextContent('read');
877 expect(rows[1]).toHaveTextContent('triage');
878 expect(rows[2]).toHaveTextContent('write');
879 expect(rows[3]).toHaveTextContent('maintain');
880 expect(rows[4]).toHaveTextContent('admin');
883 it('should apply new mapping and new provisioning type at the same time', async () => {
884 const user = userEvent.setup();
885 renderAuthentication([Feature.GithubProvisioning]);
886 await user.click(await github.tab.find());
888 await github.createConfiguration(user);
889 await user.click(await github.enableConfigButton.find());
891 expect(await github.jitProvisioningButton.find()).toBeChecked();
892 expect(github.editMappingButton.query()).not.toBeInTheDocument();
893 await user.click(github.githubProvisioningButton.get());
894 expect(await github.editMappingButton.find()).toBeInTheDocument();
895 await user.click(github.editMappingButton.get());
897 expect(await github.mappingRow.findAll()).toHaveLength(7);
899 let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'));
900 let adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin'));
902 expect(readCheckboxes[0]).toBeChecked();
903 expect(readCheckboxes[5]).not.toBeChecked();
904 expect(adminCheckboxes[5]).toBeChecked();
906 await user.click(readCheckboxes[0]);
907 await user.click(readCheckboxes[5]);
908 await user.click(adminCheckboxes[5]);
909 await user.click(github.mappingDialogClose.get());
911 await user.click(github.saveGithubProvisioning.get());
912 await act(() => user.click(github.confirmProvisioningButton.get()));
914 // Clean local mapping state
915 await user.click(github.jitProvisioningButton.get());
916 await user.click(github.githubProvisioningButton.get());
918 await user.click(github.editMappingButton.get());
919 readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'));
920 adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin'));
922 expect(readCheckboxes[0]).not.toBeChecked();
923 expect(readCheckboxes[5]).toBeChecked();
924 expect(adminCheckboxes[5]).not.toBeChecked();
925 await user.click(github.mappingDialogClose.get());
928 it('should apply new mapping on auto-provisioning', async () => {
929 const user = userEvent.setup();
930 settingsHandler.presetGithubAutoProvisioning();
931 handler.enableGithubProvisioning();
932 renderAuthentication([Feature.GithubProvisioning]);
933 await user.click(await github.tab.find());
935 expect(await github.saveGithubProvisioning.find()).toBeDisabled();
936 await user.click(github.editMappingButton.get());
938 expect(await github.mappingRow.findAll()).toHaveLength(7);
940 let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0];
942 expect(readCheckboxes).toBeChecked();
944 await user.click(readCheckboxes);
945 await user.click(github.mappingDialogClose.get());
947 expect(await github.saveGithubProvisioning.find()).toBeEnabled();
949 await act(() => user.click(github.saveGithubProvisioning.get()));
951 // Clean local mapping state
952 await user.click(github.jitProvisioningButton.get());
953 await user.click(github.githubProvisioningButton.get());
955 await user.click(github.editMappingButton.get());
956 readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0];
958 expect(readCheckboxes).not.toBeChecked();
959 await user.click(github.mappingDialogClose.get());
962 it('should add/remove/update custom roles', async () => {
963 const user = userEvent.setup();
964 settingsHandler.presetGithubAutoProvisioning();
965 handler.enableGithubProvisioning();
966 handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']);
967 handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
968 renderAuthentication([Feature.GithubProvisioning]);
969 await user.click(await github.tab.find());
971 expect(await github.saveGithubProvisioning.find()).toBeDisabled();
972 await user.click(github.editMappingButton.get());
974 const rows = (await github.mappingRow.findAll()).filter(
975 (row) => within(row).queryAllByRole('checkbox').length > 0,
978 expect(rows).toHaveLength(7);
980 let custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1'));
982 expect(custom1Checkboxes[0]).toBeChecked();
983 expect(custom1Checkboxes[1]).toBeChecked();
984 expect(custom1Checkboxes[2]).not.toBeChecked();
985 expect(custom1Checkboxes[3]).not.toBeChecked();
986 expect(custom1Checkboxes[4]).not.toBeChecked();
987 expect(custom1Checkboxes[5]).toBeChecked();
989 await user.click(custom1Checkboxes[1]);
990 await user.click(custom1Checkboxes[2]);
992 await user.click(github.deleteCustomRoleCustom2.get());
994 expect(github.customRoleInput.get()).toHaveValue('');
995 await user.type(github.customRoleInput.get(), 'read');
996 await user.click(github.customRoleAddBtn.get());
997 expect(await github.roleExistsError.find()).toBeInTheDocument();
998 expect(github.customRoleAddBtn.get()).toBeDisabled();
999 await user.clear(github.customRoleInput.get());
1000 expect(github.roleExistsError.query()).not.toBeInTheDocument();
1001 await user.type(github.customRoleInput.get(), 'custom1');
1002 await user.click(github.customRoleAddBtn.get());
1003 expect(await github.roleExistsError.find()).toBeInTheDocument();
1004 expect(github.customRoleAddBtn.get()).toBeDisabled();
1005 await user.clear(github.customRoleInput.get());
1006 await user.type(github.customRoleInput.get(), 'custom3');
1007 expect(github.roleExistsError.query()).not.toBeInTheDocument();
1008 expect(github.customRoleAddBtn.get()).toBeEnabled();
1009 await user.click(github.customRoleAddBtn.get());
1011 let custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3'));
1012 expect(custom3Checkboxes[0]).toBeChecked();
1013 expect(custom3Checkboxes[1]).not.toBeChecked();
1014 expect(custom3Checkboxes[2]).not.toBeChecked();
1015 expect(custom3Checkboxes[3]).not.toBeChecked();
1016 expect(custom3Checkboxes[4]).not.toBeChecked();
1017 expect(custom3Checkboxes[5]).not.toBeChecked();
1018 await user.click(custom3Checkboxes[0]);
1019 expect(await github.emptyRoleError.find()).toBeInTheDocument();
1020 expect(github.mappingDialogClose.get()).toBeDisabled();
1021 await user.click(custom3Checkboxes[1]);
1022 expect(github.emptyRoleError.query()).not.toBeInTheDocument();
1023 expect(github.mappingDialogClose.get()).toBeEnabled();
1024 await user.click(github.mappingDialogClose.get());
1026 expect(await github.saveGithubProvisioning.find()).toBeEnabled();
1027 await act(() => user.click(github.saveGithubProvisioning.get()));
1029 // Clean local mapping state
1030 await user.click(github.jitProvisioningButton.get());
1031 await user.click(github.githubProvisioningButton.get());
1033 await user.click(github.editMappingButton.get());
1036 (await github.mappingRow.findAll()).filter(
1037 (row) => within(row).queryAllByRole('checkbox').length > 0,
1040 custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1'));
1041 custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3'));
1042 expect(github.getMappingRowByRole('custom2')).toBeUndefined();
1043 expect(custom1Checkboxes[0]).toBeChecked();
1044 expect(custom1Checkboxes[1]).not.toBeChecked();
1045 expect(custom1Checkboxes[2]).toBeChecked();
1046 expect(custom1Checkboxes[3]).not.toBeChecked();
1047 expect(custom1Checkboxes[4]).not.toBeChecked();
1048 expect(custom1Checkboxes[5]).toBeChecked();
1049 expect(custom3Checkboxes[0]).not.toBeChecked();
1050 expect(custom3Checkboxes[1]).toBeChecked();
1051 expect(custom3Checkboxes[2]).not.toBeChecked();
1052 expect(custom3Checkboxes[3]).not.toBeChecked();
1053 expect(custom3Checkboxes[4]).not.toBeChecked();
1054 expect(custom3Checkboxes[5]).not.toBeChecked();
1055 await user.click(github.mappingDialogClose.get());
1060 function renderAuthentication(features: Feature[] = []) {
1062 <AvailableFeaturesContext.Provider value={features}>
1063 <Authentication definitions={definitions} />
1064 </AvailableFeaturesContext.Provider>,