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