]> source.dussan.org Git - sonarqube.git/blob
759a8d3c8647765f5c5d9e3cf8ab8cc385441775
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 import { 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 ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
25 import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
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 { byLabelText, byRole, byText } from '../../../../../helpers/testSelector';
32 import { AlmKeys } from '../../../../../types/alm-settings';
33 import { Feature } from '../../../../../types/features';
34 import { GitHubProvisioningStatus } from '../../../../../types/provisioning';
35 import { TaskStatuses } from '../../../../../types/tasks';
36 import Authentication from '../Authentication';
37
38 let handler: GithubProvisioningServiceMock;
39 let system: SystemServiceMock;
40 let settingsHandler: SettingsServiceMock;
41 let computeEngineHandler: ComputeEngineServiceMock;
42
43 beforeEach(() => {
44   handler = new GithubProvisioningServiceMock();
45   system = new SystemServiceMock();
46   settingsHandler = new SettingsServiceMock();
47   computeEngineHandler = new ComputeEngineServiceMock();
48 });
49
50 afterEach(() => {
51   handler.reset();
52   settingsHandler.reset();
53   system.reset();
54   computeEngineHandler.reset();
55 });
56
57 const ghContainer = byRole('tabpanel', { name: 'github GitHub' });
58
59 const ui = {
60   saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }),
61   customMessageInformation: byText('settings.authentication.custom_message_information'),
62   enabledToggle: byRole('switch'),
63   testButton: byText('settings.authentication.saml.form.test'),
64   textbox1: byRole('textbox', { name: 'test1' }),
65   textbox2: byRole('textbox', { name: 'test2' }),
66   tab: byRole('tab', { name: 'github GitHub' }),
67   noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
68   createConfigButton: ghContainer.byRole('button', {
69     name: 'settings.authentication.form.create',
70   }),
71   clientId: byRole('textbox', {
72     name: 'property.sonar.auth.github.clientId.secured.name',
73   }),
74   appId: byRole('textbox', { name: 'property.sonar.auth.github.appId.name' }),
75   privateKey: byRole('textbox', {
76     name: 'property.sonar.auth.github.privateKey.secured.name',
77   }),
78   clientSecret: byRole('textbox', {
79     name: 'property.sonar.auth.github.clientSecret.secured.name',
80   }),
81   githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }),
82   githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }),
83   allowUsersToSignUp: byRole('switch', {
84     name: 'property.sonar.auth.github.allowUsersToSignUp.name',
85   }),
86   organizations: byRole('textbox', {
87     name: 'property.sonar.auth.github.organizations.name',
88   }),
89   saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
90   confirmProvisioningButton: byRole('button', {
91     name: 'settings.authentication.github.provisioning_change.confirm_changes',
92   }),
93   saveGithubProvisioning: ghContainer.byRole('button', { name: 'save' }),
94   groupAttribute: byRole('textbox', {
95     name: 'property.sonar.auth.github.group.name.name',
96   }),
97   enableConfigButton: ghContainer.byRole('button', {
98     name: 'settings.authentication.form.enable',
99   }),
100   disableConfigButton: ghContainer.byRole('button', {
101     name: 'settings.authentication.form.disable',
102   }),
103   editConfigButton: ghContainer.byRole('button', {
104     name: 'settings.authentication.form.edit',
105   }),
106   editMappingButton: ghContainer.byRole('button', {
107     name: 'settings.authentication.github.configuration.roles_mapping.button_label',
108   }),
109   mappingRow: byRole('dialog', {
110     name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
111   }).byRole('row'),
112   customRoleInput: byRole('textbox', {
113     name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role',
114   }),
115   customRoleAddBtn: byRole('dialog', {
116     name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
117   }).byRole('button', { name: 'add_verb' }),
118   roleExistsError: byRole('dialog', {
119     name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
120   }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'),
121   emptyRoleError: byRole('dialog', {
122     name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
123   }).byText('settings.authentication.github.configuration.roles_mapping.empty_custom_role'),
124   deleteCustomRoleCustom2: byRole('button', {
125     name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2',
126   }),
127   getMappingRowByRole: (text: string) =>
128     ui.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
129   mappingCheckbox: byRole('checkbox'),
130   mappingDialogClose: byRole('dialog', {
131     name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
132   }).byRole('button', {
133     name: 'close',
134   }),
135   deleteOrg: (org: string) =>
136     byRole('button', {
137       name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
138     }),
139   enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
140   jitProvisioningButton: ghContainer.byRole('radio', {
141     name: /settings.authentication.form.provisioning_at_login/,
142   }),
143   githubProvisioningButton: ghContainer.byRole('radio', {
144     name: /settings.authentication.github.form.provisioning_with_github/,
145   }),
146   githubProvisioningPending: ghContainer
147     .byRole('list')
148     .byRole('status')
149     .byText(/synchronization_pending/),
150   githubProvisioningInProgress: ghContainer
151     .byRole('list')
152     .byRole('status')
153     .byText(/synchronization_in_progress/),
154   githubProvisioningSuccess: ghContainer.byText(/synchronization_successful/),
155   githubProvisioningAlert: ghContainer.byText(/synchronization_failed/),
156   configurationValidityLoading: ghContainer.byRole('status', {
157     name: /github.configuration.validation.loading/,
158   }),
159   configurationValiditySuccess: ghContainer.byRole('status', {
160     name: /github.configuration.validation.valid/,
161   }),
162   configurationValidityError: ghContainer.byRole('status', {
163     name: /github.configuration.validation.invalid/,
164   }),
165   syncWarning: ghContainer.byText(/Warning/),
166   syncSummary: ghContainer.byText(/Test summary/),
167   configurationValidityWarning: ghContainer.byRole('status', {
168     name: /github.configuration.validation.valid.short/,
169   }),
170   checkConfigButton: ghContainer.byRole('button', {
171     name: 'settings.authentication.configuration.test',
172   }),
173   viewConfigValidityDetailsButton: ghContainer.byRole('button', {
174     name: 'settings.authentication.github.configuration.validation.details',
175   }),
176   configDetailsDialog: byRole('dialog', {
177     name: 'settings.authentication.github.configuration.validation.details.title',
178   }),
179   continueAutoButton: byRole('button', {
180     name: 'settings.authentication.github.confirm_auto_provisioning.continue',
181   }),
182   switchJitButton: byRole('button', {
183     name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
184   }),
185   consentDialog: byRole('dialog', {
186     name: 'settings.authentication.github.confirm_auto_provisioning.header',
187   }),
188   getConfigDetailsTitle: () => ui.configDetailsDialog.byRole('heading').get(),
189   getOrgs: () => ui.configDetailsDialog.byRole('listitem').getAll(),
190   getIconForOrg: (text: string, org: HTMLElement) => byLabelText(text).get(org),
191   fillForm: async (user: UserEvent) => {
192     await user.type(await ui.clientId.find(), 'Awsome GITHUB config');
193     await user.type(ui.clientSecret.get(), 'Client shut');
194     await user.type(ui.appId.get(), 'App id');
195     await user.type(ui.privateKey.get(), 'Private Key');
196     await user.type(ui.githubApiUrl.get(), 'API Url');
197     await user.type(ui.githubWebUrl.get(), 'WEb Url');
198     await user.type(ui.organizations.get(), 'organization1');
199   },
200   createConfiguration: async (user: UserEvent) => {
201     await user.click(await ui.createConfigButton.find());
202     await ui.fillForm(user);
203
204     await user.click(ui.saveConfigButton.get());
205   },
206   enableConfiguration: async (user: UserEvent) => {
207     await user.click(await ui.tab.find());
208     await ui.createConfiguration(user);
209     await user.click(await ui.enableConfigButton.find());
210   },
211   enableProvisioning: async (user: UserEvent) => {
212     await user.click(await ui.tab.find());
213
214     await ui.createConfiguration(user);
215
216     await user.click(await ui.enableConfigButton.find());
217     await user.click(await ui.githubProvisioningButton.find());
218     await user.click(ui.saveGithubProvisioning.get());
219     await user.click(ui.confirmProvisioningButton.get());
220   },
221 };
222
223 describe('Github tab', () => {
224   it('should render an empty Github configuration', async () => {
225     renderAuthentication();
226     const user = userEvent.setup();
227     await user.click(await ui.tab.find());
228     expect(await ui.noGithubConfiguration.find()).toBeInTheDocument();
229   });
230
231   it('should be able to create a configuration', async () => {
232     const user = userEvent.setup();
233     renderAuthentication();
234
235     await user.click(await ui.tab.find());
236     await user.click(await ui.createConfigButton.find());
237
238     expect(ui.saveConfigButton.get()).toBeDisabled();
239
240     await ui.fillForm(user);
241     expect(ui.saveConfigButton.get()).toBeEnabled();
242
243     await user.click(ui.saveConfigButton.get());
244
245     expect(await ui.editConfigButton.find()).toBeInTheDocument();
246   });
247
248   it('should be able to edit configuration', async () => {
249     const user = userEvent.setup();
250     renderAuthentication();
251     await user.click(await ui.tab.find());
252
253     await ui.createConfiguration(user);
254
255     await user.click(ui.editConfigButton.get());
256     await user.click(ui.deleteOrg('organization1').get());
257
258     await user.click(ui.saveConfigButton.get());
259
260     await user.click(await ui.editConfigButton.find());
261
262     expect(ui.organizations.get()).toHaveValue('');
263   });
264
265   it('should be able to enable/disable configuration', async () => {
266     const user = userEvent.setup();
267     renderAuthentication();
268     await user.click(await ui.tab.find());
269
270     await ui.createConfiguration(user);
271
272     await user.click(await ui.enableConfigButton.find());
273
274     expect(await ui.disableConfigButton.find()).toBeInTheDocument();
275     await user.click(ui.disableConfigButton.get());
276     await waitFor(() => expect(ui.disableConfigButton.query()).not.toBeInTheDocument());
277
278     expect(await ui.enableConfigButton.find()).toBeInTheDocument();
279   });
280
281   it('should not allow edtion below Enterprise to select Github provisioning', async () => {
282     const user = userEvent.setup();
283
284     renderAuthentication();
285     await user.click(await ui.tab.find());
286
287     await ui.createConfiguration(user);
288     await user.click(await ui.enableConfigButton.find());
289
290     expect(await ui.jitProvisioningButton.find()).toBeChecked();
291     expect(ui.githubProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
292   });
293
294   it('should be able to choose provisioning', async () => {
295     const user = userEvent.setup();
296
297     renderAuthentication([Feature.GithubProvisioning]);
298     await user.click(await ui.tab.find());
299
300     await ui.createConfiguration(user);
301
302     expect(await ui.enableFirstMessage.find()).toBeInTheDocument();
303     await user.click(await ui.enableConfigButton.find());
304
305     expect(await ui.jitProvisioningButton.find()).toBeChecked();
306
307     expect(ui.saveGithubProvisioning.get()).toBeDisabled();
308     await user.click(ui.allowUsersToSignUp.get());
309
310     expect(ui.saveGithubProvisioning.get()).toBeEnabled();
311     await user.click(ui.saveGithubProvisioning.get());
312
313     await waitFor(() => expect(ui.saveGithubProvisioning.query()).toBeDisabled());
314
315     await user.click(ui.githubProvisioningButton.get());
316
317     expect(ui.saveGithubProvisioning.get()).toBeEnabled();
318     await user.click(ui.saveGithubProvisioning.get());
319     await user.click(ui.confirmProvisioningButton.get());
320
321     expect(await ui.githubProvisioningButton.find()).toBeChecked();
322     expect(ui.disableConfigButton.get()).toBeDisabled();
323     expect(ui.saveGithubProvisioning.get()).toBeDisabled();
324   });
325
326   describe('Github Provisioning', () => {
327     let user: UserEvent;
328
329     beforeEach(() => {
330       jest.useFakeTimers({
331         advanceTimers: true,
332         now: new Date('2022-02-04T12:00:59Z'),
333       });
334       user = userEvent.setup();
335     });
336
337     afterEach(() => {
338       jest.runOnlyPendingTimers();
339       jest.useRealTimers();
340     });
341
342     it('should display a success status when the synchronisation is a success', async () => {
343       handler.addProvisioningTask({
344         status: TaskStatuses.Success,
345         executedAt: '2022-02-03T11:45:35+0200',
346       });
347
348       renderAuthentication([Feature.GithubProvisioning]);
349       await ui.enableProvisioning(user);
350       expect(ui.githubProvisioningSuccess.get()).toBeInTheDocument();
351       expect(ui.syncSummary.get()).toBeInTheDocument();
352     });
353
354     it('should display a success status even when another task is pending', async () => {
355       handler.addProvisioningTask({
356         status: TaskStatuses.Pending,
357         executedAt: '2022-02-03T11:55:35+0200',
358       });
359       handler.addProvisioningTask({
360         status: TaskStatuses.Success,
361         executedAt: '2022-02-03T11:45:35+0200',
362       });
363       renderAuthentication([Feature.GithubProvisioning]);
364       await ui.enableProvisioning(user);
365       expect(ui.githubProvisioningSuccess.get()).toBeInTheDocument();
366       expect(ui.githubProvisioningPending.get()).toBeInTheDocument();
367     });
368
369     it('should display an error alert when the synchronisation failed', async () => {
370       handler.addProvisioningTask({
371         status: TaskStatuses.Failed,
372         executedAt: '2022-02-03T11:45:35+0200',
373         errorMessage: "T'es mauvais Jacques",
374       });
375       renderAuthentication([Feature.GithubProvisioning]);
376       await ui.enableProvisioning(user);
377       expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
378       expect(ghContainer.get()).toHaveTextContent("T'es mauvais Jacques");
379       expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
380     });
381
382     it('should display an error alert even when another task is in progress', async () => {
383       handler.addProvisioningTask({
384         status: TaskStatuses.InProgress,
385         executedAt: '2022-02-03T11:55:35+0200',
386       });
387       handler.addProvisioningTask({
388         status: TaskStatuses.Failed,
389         executedAt: '2022-02-03T11:45:35+0200',
390         errorMessage: "T'es mauvais Jacques",
391       });
392       renderAuthentication([Feature.GithubProvisioning]);
393       await ui.enableProvisioning(user);
394       expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
395       expect(ghContainer.get()).toHaveTextContent("T'es mauvais Jacques");
396       expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
397       expect(ui.githubProvisioningInProgress.get()).toBeInTheDocument();
398     });
399
400     it('should display that config is valid for both provisioning with 1 org', async () => {
401       renderAuthentication([Feature.GithubProvisioning]);
402       await ui.enableConfiguration(user);
403
404       await appLoaded();
405
406       await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
407     });
408
409     it('should display that config is valid for both provisioning with multiple orgs', async () => {
410       handler.setConfigurationValidity({
411         installations: [
412           {
413             organization: 'org1',
414             autoProvisioning: { status: GitHubProvisioningStatus.Success },
415             jit: { status: GitHubProvisioningStatus.Success },
416           },
417           {
418             organization: 'org2',
419             autoProvisioning: { status: GitHubProvisioningStatus.Success },
420             jit: { status: GitHubProvisioningStatus.Success },
421           },
422         ],
423       });
424       renderAuthentication([Feature.GithubProvisioning]);
425       await ui.enableConfiguration(user);
426
427       await appLoaded();
428
429       await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
430       expect(ui.configurationValiditySuccess.get()).toHaveTextContent('2');
431
432       await user.click(ui.viewConfigValidityDetailsButton.get());
433       expect(ui.getConfigDetailsTitle()).toBeInTheDocument();
434       expect(ui.getOrgs()).toHaveLength(3);
435       expect(
436         ui.getIconForOrg(
437           'settings.authentication.github.configuration.validation.details.valid_label',
438           ui.getOrgs()[0],
439         ),
440       ).toBeInTheDocument();
441       expect(
442         ui.getIconForOrg(
443           'settings.authentication.github.configuration.validation.details.valid_label',
444           ui.getOrgs()[1],
445         ),
446       ).toBeInTheDocument();
447     });
448
449     it('should display that config is invalid for autoprovisioning if some apps are suspended but valid for jit', async () => {
450       const errorMessage = 'Installation suspended';
451       handler.setConfigurationValidity({
452         installations: [
453           {
454             organization: 'org1',
455             autoProvisioning: {
456               status: GitHubProvisioningStatus.Failed,
457               errorMessage,
458             },
459             jit: {
460               status: GitHubProvisioningStatus.Failed,
461               errorMessage,
462             },
463           },
464         ],
465       });
466
467       renderAuthentication([Feature.GithubProvisioning]);
468       await ui.enableConfiguration(user);
469
470       await appLoaded();
471
472       await waitFor(() => expect(ui.configurationValidityWarning.get()).toBeInTheDocument());
473       expect(ui.configurationValidityWarning.get()).toHaveTextContent(errorMessage);
474
475       await user.click(ui.viewConfigValidityDetailsButton.get());
476       expect(ui.getConfigDetailsTitle()).toBeInTheDocument();
477       expect(
478         ui.configDetailsDialog
479           .byText('settings.authentication.github.configuration.validation.valid.short')
480           .get(),
481       ).toBeInTheDocument();
482       expect(ui.getOrgs()[0]).toHaveTextContent('org1 - Installation suspended');
483       expect(
484         ui.getIconForOrg(
485           'settings.authentication.github.configuration.validation.details.invalid_label',
486           ui.getOrgs()[0],
487         ),
488       ).toBeInTheDocument();
489
490       await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
491
492       await user.click(ui.githubProvisioningButton.get());
493       await waitFor(() => expect(ui.configurationValidityError.get()).toBeInTheDocument());
494       expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
495     });
496
497     it('should display that config is valid but some organizations were not found', async () => {
498       handler.setConfigurationValidity({
499         installations: [
500           {
501             organization: 'org1',
502             autoProvisioning: { status: GitHubProvisioningStatus.Success },
503             jit: { status: GitHubProvisioningStatus.Success },
504           },
505         ],
506       });
507
508       renderAuthentication([Feature.GithubProvisioning]);
509       await ui.enableConfiguration(user);
510
511       await appLoaded();
512
513       await waitFor(() => expect(ui.configurationValiditySuccess.get()).toBeInTheDocument());
514       expect(ui.configurationValiditySuccess.get()).toHaveTextContent('1');
515
516       await user.click(ui.viewConfigValidityDetailsButton.get());
517       expect(ui.getConfigDetailsTitle()).toBeInTheDocument();
518       expect(
519         ui.configDetailsDialog
520           .byText('settings.authentication.github.configuration.validation.valid.short')
521           .get(),
522       ).toBeInTheDocument();
523       expect(ui.getOrgs()[0]).toHaveTextContent('org1');
524       expect(
525         ui.getIconForOrg(
526           'settings.authentication.github.configuration.validation.details.valid_label',
527           ui.getOrgs()[0],
528         ),
529       ).toBeInTheDocument();
530       expect(ui.getOrgs()[1]).toHaveTextContent(
531         'settings.authentication.github.configuration.validation.details.org_not_found.organization1',
532       );
533     });
534
535     it('should display that config is invalid', async () => {
536       const errorMessage = 'Test error';
537       handler.setConfigurationValidity({
538         application: {
539           jit: {
540             status: GitHubProvisioningStatus.Failed,
541             errorMessage,
542           },
543           autoProvisioning: {
544             status: GitHubProvisioningStatus.Failed,
545             errorMessage,
546           },
547         },
548       });
549       renderAuthentication([Feature.GithubProvisioning]);
550       await ui.enableConfiguration(user);
551
552       await appLoaded();
553
554       await waitFor(() => expect(ui.configurationValidityError.query()).toBeInTheDocument());
555       expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
556
557       await user.click(ui.viewConfigValidityDetailsButton.get());
558       expect(ui.getConfigDetailsTitle()).toBeInTheDocument();
559       expect(
560         ui.configDetailsDialog
561           .byText(/settings.authentication.github.configuration.validation.invalid/)
562           .get(),
563       ).toBeInTheDocument();
564       expect(ui.configDetailsDialog.get()).toHaveTextContent(errorMessage);
565     });
566
567     it('should display that config is valid for jit, but not for auto', async () => {
568       const errorMessage = 'Test error';
569       handler.setConfigurationValidity({
570         application: {
571           jit: {
572             status: GitHubProvisioningStatus.Success,
573           },
574           autoProvisioning: {
575             status: GitHubProvisioningStatus.Failed,
576             errorMessage,
577           },
578         },
579       });
580       renderAuthentication([Feature.GithubProvisioning]);
581       await ui.enableConfiguration(user);
582
583       await appLoaded();
584
585       await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
586       expect(ui.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
587
588       await user.click(ui.viewConfigValidityDetailsButton.get());
589       expect(ui.getConfigDetailsTitle()).toBeInTheDocument();
590       expect(
591         ui.configDetailsDialog
592           .byText('settings.authentication.github.configuration.validation.valid.short')
593           .get(),
594       ).toBeInTheDocument();
595       await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
596
597       await user.click(ui.githubProvisioningButton.get());
598
599       expect(ui.configurationValidityError.get()).toBeInTheDocument();
600       expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
601
602       await user.click(ui.viewConfigValidityDetailsButton.get());
603       expect(
604         ui.configDetailsDialog
605           .byText(/settings.authentication.github.configuration.validation.invalid/)
606           .get(),
607       ).toBeInTheDocument();
608     });
609
610     it('should display that config is invalid because of orgs', async () => {
611       const errorMessage = 'Test error';
612       handler.setConfigurationValidity({
613         installations: [
614           {
615             organization: 'org1',
616             autoProvisioning: { status: GitHubProvisioningStatus.Success },
617             jit: { status: GitHubProvisioningStatus.Success },
618           },
619           {
620             organization: 'org2',
621             jit: { status: GitHubProvisioningStatus.Failed, errorMessage },
622             autoProvisioning: { status: GitHubProvisioningStatus.Failed, errorMessage },
623           },
624         ],
625       });
626       renderAuthentication([Feature.GithubProvisioning]);
627       await ui.enableConfiguration(user);
628
629       await appLoaded();
630
631       await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
632
633       await user.click(ui.viewConfigValidityDetailsButton.get());
634
635       expect(ui.getOrgs()[0]).toHaveTextContent('org1');
636       expect(
637         ui.getIconForOrg(
638           'settings.authentication.github.configuration.validation.details.valid_label',
639           ui.getOrgs()[0],
640         ),
641       ).toBeInTheDocument();
642       expect(ui.getOrgs()[1]).toHaveTextContent('org2 - Test error');
643       expect(
644         ui.getIconForOrg(
645           'settings.authentication.github.configuration.validation.details.invalid_label',
646           ui.getOrgs()[1],
647         ),
648       ).toBeInTheDocument();
649
650       await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
651
652       await user.click(ui.githubProvisioningButton.get());
653
654       expect(ui.configurationValidityError.get()).toBeInTheDocument();
655       expect(ui.configurationValidityError.get()).toHaveTextContent(
656         `settings.authentication.github.configuration.validation.invalid_org.org2.${errorMessage}`,
657       );
658       await user.click(ui.viewConfigValidityDetailsButton.get());
659
660       expect(
661         ui.configDetailsDialog
662           .byLabelText(
663             'settings.authentication.github.configuration.validation.details.invalid_label',
664           )
665           .getAll(),
666       ).toHaveLength(1);
667       expect(ui.getOrgs()[1]).toHaveTextContent(`org2 - ${errorMessage}`);
668     });
669
670     it('should update provisioning validity after clicking Test Configuration', async () => {
671       const errorMessage = 'Test error';
672       handler.setConfigurationValidity({
673         application: {
674           jit: {
675             status: GitHubProvisioningStatus.Failed,
676             errorMessage,
677           },
678           autoProvisioning: {
679             status: GitHubProvisioningStatus.Failed,
680             errorMessage,
681           },
682         },
683       });
684       renderAuthentication([Feature.GithubProvisioning]);
685       await ui.enableConfiguration(user);
686       handler.setConfigurationValidity({
687         application: {
688           jit: {
689             status: GitHubProvisioningStatus.Success,
690           },
691           autoProvisioning: {
692             status: GitHubProvisioningStatus.Success,
693           },
694         },
695       });
696
697       await appLoaded();
698
699       expect(await ui.configurationValidityError.find()).toBeInTheDocument();
700
701       await user.click(ui.checkConfigButton.get());
702
703       expect(ui.configurationValiditySuccess.get()).toBeInTheDocument();
704       expect(ui.configurationValidityError.query()).not.toBeInTheDocument();
705     });
706
707     it('should show warning', async () => {
708       handler.addProvisioningTask({
709         status: TaskStatuses.Success,
710         warnings: ['Warning'],
711       });
712       renderAuthentication([Feature.GithubProvisioning]);
713       await ui.enableProvisioning(user);
714
715       expect(await ui.syncWarning.find()).toBeInTheDocument();
716       expect(ui.syncSummary.get()).toBeInTheDocument();
717     });
718
719     it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
720       const user = userEvent.setup();
721       settingsHandler.presetGithubAutoProvisioning();
722       handler.enableGithubProvisioning();
723       settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
724       renderAuthentication([Feature.GithubProvisioning]);
725
726       await user.click(await ui.tab.find());
727
728       expect(await ui.consentDialog.find()).toBeInTheDocument();
729       await user.click(ui.continueAutoButton.get());
730
731       expect(await ui.githubProvisioningButton.find()).toBeChecked();
732       expect(ui.consentDialog.query()).not.toBeInTheDocument();
733     });
734
735     it('should display a modal if user was already using auto and switch to JIT', async () => {
736       const user = userEvent.setup();
737       settingsHandler.presetGithubAutoProvisioning();
738       handler.enableGithubProvisioning();
739       settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
740       renderAuthentication([Feature.GithubProvisioning]);
741
742       await user.click(await ui.tab.find());
743
744       expect(await ui.consentDialog.find()).toBeInTheDocument();
745       await user.click(ui.switchJitButton.get());
746
747       expect(await ui.jitProvisioningButton.find()).toBeChecked();
748       expect(ui.consentDialog.query()).not.toBeInTheDocument();
749     });
750
751     it('should sort mapping rows', async () => {
752       const user = userEvent.setup();
753       settingsHandler.presetGithubAutoProvisioning();
754       handler.enableGithubProvisioning();
755       renderAuthentication([Feature.GithubProvisioning]);
756       await user.click(await ui.tab.find());
757
758       expect(await ui.editMappingButton.find()).toBeInTheDocument();
759       await user.click(ui.editMappingButton.get());
760
761       const rows = (await ui.mappingRow.findAll()).filter(
762         (row) => within(row).queryAllByRole('checkbox').length > 0,
763       );
764
765       expect(rows).toHaveLength(5);
766
767       expect(rows[0]).toHaveTextContent('read');
768       expect(rows[1]).toHaveTextContent('triage');
769       expect(rows[2]).toHaveTextContent('write');
770       expect(rows[3]).toHaveTextContent('maintain');
771       expect(rows[4]).toHaveTextContent('admin');
772     });
773
774     it('should apply new mapping and new provisioning type at the same time', async () => {
775       const user = userEvent.setup();
776       renderAuthentication([Feature.GithubProvisioning]);
777       await user.click(await ui.tab.find());
778
779       await ui.createConfiguration(user);
780       await user.click(await ui.enableConfigButton.find());
781
782       expect(await ui.jitProvisioningButton.find()).toBeChecked();
783       expect(ui.editMappingButton.query()).not.toBeInTheDocument();
784       await user.click(ui.githubProvisioningButton.get());
785       expect(await ui.editMappingButton.find()).toBeInTheDocument();
786       await user.click(ui.editMappingButton.get());
787
788       expect(await ui.mappingRow.findAll()).toHaveLength(7);
789
790       let readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'));
791       let adminCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('admin'));
792
793       expect(readCheckboxes[0]).toBeChecked();
794       expect(readCheckboxes[5]).not.toBeChecked();
795       expect(adminCheckboxes[5]).toBeChecked();
796
797       await user.click(readCheckboxes[0]);
798       await user.click(readCheckboxes[5]);
799       await user.click(adminCheckboxes[5]);
800       await user.click(ui.mappingDialogClose.get());
801
802       await user.click(ui.saveGithubProvisioning.get());
803       await user.click(ui.confirmProvisioningButton.get());
804
805       // Clean local mapping state
806       await user.click(ui.jitProvisioningButton.get());
807       await user.click(ui.githubProvisioningButton.get());
808
809       await user.click(ui.editMappingButton.get());
810       readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'));
811       adminCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('admin'));
812
813       expect(readCheckboxes[0]).not.toBeChecked();
814       expect(readCheckboxes[5]).toBeChecked();
815       expect(adminCheckboxes[5]).not.toBeChecked();
816       await user.click(ui.mappingDialogClose.get());
817     });
818
819     it('should apply new mapping on auto-provisioning', async () => {
820       const user = userEvent.setup();
821       settingsHandler.presetGithubAutoProvisioning();
822       handler.enableGithubProvisioning();
823       renderAuthentication([Feature.GithubProvisioning]);
824       await user.click(await ui.tab.find());
825
826       expect(await ui.saveGithubProvisioning.find()).toBeDisabled();
827       await user.click(ui.editMappingButton.get());
828
829       expect(await ui.mappingRow.findAll()).toHaveLength(7);
830
831       let readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'))[0];
832
833       expect(readCheckboxes).toBeChecked();
834
835       await user.click(readCheckboxes);
836       await user.click(ui.mappingDialogClose.get());
837
838       expect(await ui.saveGithubProvisioning.find()).toBeEnabled();
839
840       await user.click(ui.saveGithubProvisioning.get());
841
842       // Clean local mapping state
843       await user.click(ui.jitProvisioningButton.get());
844       await user.click(ui.githubProvisioningButton.get());
845
846       await user.click(ui.editMappingButton.get());
847       readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'))[0];
848
849       expect(readCheckboxes).not.toBeChecked();
850       await user.click(ui.mappingDialogClose.get());
851     });
852
853     it('should add/remove/update custom roles', async () => {
854       const user = userEvent.setup();
855       settingsHandler.presetGithubAutoProvisioning();
856       handler.enableGithubProvisioning();
857       handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']);
858       handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
859       renderAuthentication([Feature.GithubProvisioning]);
860       await user.click(await ui.tab.find());
861
862       expect(await ui.saveGithubProvisioning.find()).toBeDisabled();
863       await user.click(ui.editMappingButton.get());
864
865       const rows = (await ui.mappingRow.findAll()).filter(
866         (row) => within(row).queryAllByRole('checkbox').length > 0,
867       );
868
869       expect(rows).toHaveLength(7);
870
871       let custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
872
873       expect(custom1Checkboxes[0]).toBeChecked();
874       expect(custom1Checkboxes[1]).toBeChecked();
875       expect(custom1Checkboxes[2]).not.toBeChecked();
876       expect(custom1Checkboxes[3]).not.toBeChecked();
877       expect(custom1Checkboxes[4]).not.toBeChecked();
878       expect(custom1Checkboxes[5]).toBeChecked();
879
880       await user.click(custom1Checkboxes[1]);
881       await user.click(custom1Checkboxes[2]);
882
883       await user.click(ui.deleteCustomRoleCustom2.get());
884
885       expect(ui.customRoleInput.get()).toHaveValue('');
886       await user.type(ui.customRoleInput.get(), 'read');
887       await user.click(ui.customRoleAddBtn.get());
888       expect(await ui.roleExistsError.find()).toBeInTheDocument();
889       expect(ui.customRoleAddBtn.get()).toBeDisabled();
890       await user.clear(ui.customRoleInput.get());
891       expect(ui.roleExistsError.query()).not.toBeInTheDocument();
892       await user.type(ui.customRoleInput.get(), 'custom1');
893       await user.click(ui.customRoleAddBtn.get());
894       expect(await ui.roleExistsError.find()).toBeInTheDocument();
895       expect(ui.customRoleAddBtn.get()).toBeDisabled();
896       await user.clear(ui.customRoleInput.get());
897       await user.type(ui.customRoleInput.get(), 'custom3');
898       expect(ui.roleExistsError.query()).not.toBeInTheDocument();
899       expect(ui.customRoleAddBtn.get()).toBeEnabled();
900       await user.click(ui.customRoleAddBtn.get());
901
902       let custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
903       expect(custom3Checkboxes[0]).toBeChecked();
904       expect(custom3Checkboxes[1]).not.toBeChecked();
905       expect(custom3Checkboxes[2]).not.toBeChecked();
906       expect(custom3Checkboxes[3]).not.toBeChecked();
907       expect(custom3Checkboxes[4]).not.toBeChecked();
908       expect(custom3Checkboxes[5]).not.toBeChecked();
909       await user.click(custom3Checkboxes[0]);
910       expect(await ui.emptyRoleError.find()).toBeInTheDocument();
911       expect(ui.mappingDialogClose.get()).toBeDisabled();
912       await user.click(custom3Checkboxes[1]);
913       expect(ui.emptyRoleError.query()).not.toBeInTheDocument();
914       expect(ui.mappingDialogClose.get()).toBeEnabled();
915       await user.click(ui.mappingDialogClose.get());
916
917       expect(await ui.saveGithubProvisioning.find()).toBeEnabled();
918       await user.click(ui.saveGithubProvisioning.get());
919
920       // Clean local mapping state
921       await user.click(ui.jitProvisioningButton.get());
922       await user.click(ui.githubProvisioningButton.get());
923
924       await user.click(ui.editMappingButton.get());
925
926       expect(
927         (await ui.mappingRow.findAll()).filter(
928           (row) => within(row).queryAllByRole('checkbox').length > 0,
929         ),
930       ).toHaveLength(7);
931       custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
932       custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
933       expect(ui.getMappingRowByRole('custom2')).toBeUndefined();
934       expect(custom1Checkboxes[0]).toBeChecked();
935       expect(custom1Checkboxes[1]).not.toBeChecked();
936       expect(custom1Checkboxes[2]).toBeChecked();
937       expect(custom1Checkboxes[3]).not.toBeChecked();
938       expect(custom1Checkboxes[4]).not.toBeChecked();
939       expect(custom1Checkboxes[5]).toBeChecked();
940       expect(custom3Checkboxes[0]).not.toBeChecked();
941       expect(custom3Checkboxes[1]).toBeChecked();
942       expect(custom3Checkboxes[2]).not.toBeChecked();
943       expect(custom3Checkboxes[3]).not.toBeChecked();
944       expect(custom3Checkboxes[4]).not.toBeChecked();
945       expect(custom3Checkboxes[5]).not.toBeChecked();
946       await user.click(ui.mappingDialogClose.get());
947     });
948   });
949 });
950
951 const appLoaded = async () => {
952   await waitFor(async () => {
953     expect(await screen.findByText('loading')).not.toBeInTheDocument();
954   });
955 };
956
957 function renderAuthentication(features: Feature[] = []) {
958   renderComponent(
959     <AvailableFeaturesContext.Provider value={features}>
960       <Authentication definitions={definitions} />
961     </AvailableFeaturesContext.Provider>,
962     `?tab=${AlmKeys.GitHub}`,
963   );
964 }