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