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