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