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