]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21121 Use Rest API for gitlab provisioning
authorViktor Vorona <viktor.vorona@sonarsource.com>
Thu, 14 Dec 2023 13:25:16 +0000 (14:25 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 22 Dec 2023 20:03:02 +0000 (20:03 +0000)
49 files changed:
server/sonar-web/src/main/js/api/github-provisioning.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/gitlab-provisioning.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts [deleted file]
server/sonar-web/src/main/js/api/mocks/GithubProvisioningServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/mocks/GitlabProvisioningServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/mocks/ScimProvisioningServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/api/provisioning.ts [deleted file]
server/sonar-web/src/main/js/api/scim-provisioning.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx
server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx
server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Header.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/Template.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/PermissionsProjectApp.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/__tests__/PermissionsProject-it.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ChangeDefaultVisibilityForm.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectManagementApp-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/AutoProvisionningConsent.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubConfigurationValidity.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GitHubMappingModal.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GitLabConfigurationForm.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useGithubConfiguration.ts
server/sonar-web/src/main/js/apps/settings/components/authentication/hook/useSamlConfiguration.ts
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/components/permissions/GroupHolder.tsx
server/sonar-web/src/main/js/components/permissions/HoldersList.tsx
server/sonar-web/src/main/js/components/permissions/UserHolder.tsx
server/sonar-web/src/main/js/helpers/mocks/alm-integrations.ts
server/sonar-web/src/main/js/queries/identity-provider.ts [deleted file]
server/sonar-web/src/main/js/queries/identity-provider/common.ts [new file with mode: 0644]
server/sonar-web/src/main/js/queries/identity-provider/github.ts [new file with mode: 0644]
server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts [new file with mode: 0644]
server/sonar-web/src/main/js/queries/identity-provider/scim.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/provisioning.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/api/github-provisioning.ts b/server/sonar-web/src/main/js/api/github-provisioning.ts
new file mode 100644 (file)
index 0000000..83f9fc2
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import axios from 'axios';
+import { throwGlobalError } from '../helpers/error';
+import { getJSON, post, postJSON } from '../helpers/request';
+import { GitHubConfigurationStatus, GitHubMapping, GithubStatus } from '../types/provisioning';
+
+const GITHUB_PERMISSION_MAPPINGS = '/api/v2/dop-translation/github-permission-mappings';
+
+export function fetchGithubProvisioningStatus(): Promise<GithubStatus> {
+  return getJSON('/api/github_provisioning/status').catch(throwGlobalError);
+}
+
+export function activateGithubProvisioning(): Promise<void> {
+  return post('/api/github_provisioning/enable').catch(throwGlobalError);
+}
+
+export function deactivateGithubProvisioning(): Promise<void> {
+  return post('/api/github_provisioning/disable').catch(throwGlobalError);
+}
+
+export function checkConfigurationValidity(): Promise<GitHubConfigurationStatus> {
+  return postJSON('/api/github_provisioning/check').catch(throwGlobalError);
+}
+
+export function syncNowGithubProvisioning(): Promise<void> {
+  return post('/api/github_provisioning/sync').catch(throwGlobalError);
+}
+
+export function fetchGithubRolesMapping() {
+  return axios
+    .get<{ githubPermissionsMappings: GitHubMapping[] }>(GITHUB_PERMISSION_MAPPINGS)
+    .then((data) => data.githubPermissionsMappings);
+}
+
+export function updateGithubRolesMapping(
+  role: string,
+  data: Partial<Pick<GitHubMapping, 'permissions'>>,
+) {
+  return axios.patch<GitHubMapping>(
+    `${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`,
+    data,
+  );
+}
+
+export function addGithubRolesMapping(data: Omit<GitHubMapping, 'id'>) {
+  return axios.post<GitHubMapping>(GITHUB_PERMISSION_MAPPINGS, data);
+}
+
+export function deleteGithubRolesMapping(role: string) {
+  return axios.delete(`${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`);
+}
diff --git a/server/sonar-web/src/main/js/api/gitlab-provisioning.ts b/server/sonar-web/src/main/js/api/gitlab-provisioning.ts
new file mode 100644 (file)
index 0000000..d4f3d85
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import axios from 'axios';
+import {
+  GitLabConfigurationCreateBody,
+  GitLabConfigurationUpdateBody,
+  GitlabConfiguration,
+  ProvisioningType,
+} from '../types/provisioning';
+import { Paging } from '../types/types';
+
+const GITLAB_CONFIGURATIONS = '/api/v2/dop-translation/gitlab-configurations';
+
+export function fetchGitLabConfigurations() {
+  return axios.get<{ page: Paging; gitlabConfigurations: GitlabConfiguration[] }>(
+    GITLAB_CONFIGURATIONS,
+  );
+}
+
+export function fetchGitLabConfiguration(id: string): Promise<GitlabConfiguration> {
+  return axios.get<GitlabConfiguration>(`${GITLAB_CONFIGURATIONS}/${id}`);
+}
+
+export function createGitLabConfiguration(
+  data: GitLabConfigurationCreateBody,
+): Promise<GitlabConfiguration> {
+  return axios.post(GITLAB_CONFIGURATIONS, {
+    ...data,
+    synchronizationType: ProvisioningType.jit,
+    allowUsersToSignUp: false,
+    enabled: true,
+  });
+}
+
+export function updateGitLabConfiguration(
+  id: string,
+  data: Partial<GitLabConfigurationUpdateBody>,
+) {
+  return axios.patch<GitlabConfiguration>(`${GITLAB_CONFIGURATIONS}/${id}`, data);
+}
+
+export function deleteGitLabConfiguration(id: string): Promise<void> {
+  return axios.delete(`${GITLAB_CONFIGURATIONS}/${id}`);
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts
deleted file mode 100644 (file)
index d382f32..0000000
+++ /dev/null
@@ -1,314 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { cloneDeep, omit } from 'lodash';
-import { mockGitlabConfiguration } from '../../helpers/mocks/alm-integrations';
-import { mockTask } from '../../helpers/mocks/tasks';
-import { mockPaging } from '../../helpers/testMocks';
-import {
-  GitHubConfigurationStatus,
-  GitHubMapping,
-  GitHubProvisioningStatus,
-  GitlabConfiguration,
-} from '../../types/provisioning';
-import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
-import {
-  activateGithubProvisioning,
-  activateScim,
-  addGithubRolesMapping,
-  checkConfigurationValidity,
-  createGitLabConfiguration,
-  deactivateGithubProvisioning,
-  deactivateScim,
-  deleteGitLabConfiguration,
-  deleteGithubRolesMapping,
-  fetchGitLabConfiguration,
-  fetchGitLabConfigurations,
-  fetchGithubProvisioningStatus,
-  fetchGithubRolesMapping,
-  fetchIsScimEnabled,
-  updateGitLabConfiguration,
-  updateGithubRolesMapping,
-} from '../provisioning';
-
-jest.mock('../provisioning');
-
-const defaultConfigurationStatus: GitHubConfigurationStatus = {
-  application: {
-    jit: {
-      status: GitHubProvisioningStatus.Success,
-    },
-    autoProvisioning: {
-      status: GitHubProvisioningStatus.Success,
-    },
-  },
-  installations: [
-    {
-      organization: 'testOrg',
-      autoProvisioning: {
-        status: GitHubProvisioningStatus.Success,
-      },
-      jit: {
-        status: GitHubProvisioningStatus.Success,
-      },
-    },
-  ],
-};
-
-const defaultGitlabConfiguration: GitlabConfiguration[] = [
-  mockGitlabConfiguration({ id: '1', enabled: true }),
-];
-
-const githubMappingMock = (
-  id: string,
-  permissions: (keyof GitHubMapping['permissions'])[],
-  isBaseRole = false,
-) => ({
-  id,
-  githubRole: id,
-  isBaseRole,
-  permissions: {
-    user: permissions.includes('user'),
-    codeViewer: permissions.includes('codeViewer'),
-    issueAdmin: permissions.includes('issueAdmin'),
-    securityHotspotAdmin: permissions.includes('securityHotspotAdmin'),
-    admin: permissions.includes('admin'),
-    scan: permissions.includes('scan'),
-  },
-});
-
-const defaultMapping: GitHubMapping[] = [
-  githubMappingMock('read', ['user', 'codeViewer'], true),
-  githubMappingMock(
-    'write',
-    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
-    true,
-  ),
-  githubMappingMock('triage', ['user', 'codeViewer'], true),
-  githubMappingMock(
-    'maintain',
-    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
-    true,
-  ),
-  githubMappingMock(
-    'admin',
-    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'admin', 'scan'],
-    true,
-  ),
-];
-
-export default class AuthenticationServiceMock {
-  scimStatus: boolean;
-  githubProvisioningStatus: boolean;
-  githubConfigurationStatus: GitHubConfigurationStatus;
-  githubMapping: GitHubMapping[];
-  tasks: Task[];
-  gitlabConfigurations: GitlabConfiguration[];
-
-  constructor() {
-    this.scimStatus = false;
-    this.githubProvisioningStatus = false;
-    this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
-    this.githubMapping = cloneDeep(defaultMapping);
-    this.tasks = [];
-    this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
-    jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
-    jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
-    jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled);
-    jest
-      .mocked(activateGithubProvisioning)
-      .mockImplementation(this.handleActivateGithubProvisioning);
-    jest
-      .mocked(deactivateGithubProvisioning)
-      .mockImplementation(this.handleDeactivateGithubProvisioning);
-    jest
-      .mocked(fetchGithubProvisioningStatus)
-      .mockImplementation(this.handleFetchGithubProvisioningStatus);
-    jest
-      .mocked(checkConfigurationValidity)
-      .mockImplementation(this.handleCheckConfigurationValidity);
-    jest.mocked(fetchGithubRolesMapping).mockImplementation(this.handleFetchGithubRolesMapping);
-    jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping);
-    jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping);
-    jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping);
-    jest.mocked(fetchGitLabConfigurations).mockImplementation(this.handleFetchGitLabConfigurations);
-    jest.mocked(fetchGitLabConfiguration).mockImplementation(this.handleFetchGitLabConfiguration);
-    jest.mocked(createGitLabConfiguration).mockImplementation(this.handleCreateGitLabConfiguration);
-    jest.mocked(updateGitLabConfiguration).mockImplementation(this.handleUpdateGitLabConfiguration);
-    jest.mocked(deleteGitLabConfiguration).mockImplementation(this.handleDeleteGitLabConfiguration);
-  }
-
-  addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
-    this.tasks.push(
-      mockTask({
-        id: Math.random().toString(),
-        type: TaskTypes.GithubProvisioning,
-        ...overrides,
-      }),
-    );
-  };
-
-  setConfigurationValidity = (overrides: Partial<GitHubConfigurationStatus> = {}) => {
-    this.githubConfigurationStatus = {
-      ...this.githubConfigurationStatus,
-      ...overrides,
-    };
-  };
-
-  enableGithubProvisioning = () => {
-    this.scimStatus = false;
-    this.githubProvisioningStatus = true;
-  };
-
-  handleActivateScim = () => {
-    this.scimStatus = true;
-    return Promise.resolve();
-  };
-
-  handleDeactivateScim = () => {
-    this.scimStatus = false;
-    return Promise.resolve();
-  };
-
-  handleFetchIsScimEnabled = () => {
-    return Promise.resolve(this.scimStatus);
-  };
-
-  handleActivateGithubProvisioning = () => {
-    this.githubProvisioningStatus = true;
-    return Promise.resolve();
-  };
-
-  handleDeactivateGithubProvisioning = () => {
-    this.githubProvisioningStatus = false;
-    return Promise.resolve();
-  };
-
-  handleFetchGithubProvisioningStatus = () => {
-    if (!this.githubProvisioningStatus) {
-      return Promise.resolve({ enabled: false });
-    }
-
-    const nextSync = this.tasks.find((t: Task) =>
-      [TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
-    );
-    const lastSync = this.tasks.find(
-      (t: Task) => ![TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
-    );
-
-    return Promise.resolve({
-      enabled: true,
-      nextSync: nextSync ? { status: nextSync.status } : undefined,
-      lastSync: lastSync
-        ? {
-            status: lastSync.status,
-            finishedAt: lastSync.executedAt,
-            startedAt: lastSync.startedAt,
-            executionTimeMs: lastSync.executionTimeMs,
-            summary: lastSync.status === TaskStatuses.Success ? 'Test summary' : undefined,
-            errorMessage: lastSync.errorMessage,
-            warningMessage: lastSync.warnings?.join() ?? undefined,
-          }
-        : undefined,
-    });
-  };
-
-  handleCheckConfigurationValidity = () => {
-    return Promise.resolve(this.githubConfigurationStatus);
-  };
-
-  handleFetchGithubRolesMapping: typeof fetchGithubRolesMapping = () => {
-    return Promise.resolve(this.githubMapping);
-  };
-
-  handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
-    this.githubMapping = this.githubMapping.map((mapping) =>
-      mapping.id === id ? { ...mapping, ...data } : mapping,
-    );
-
-    return Promise.resolve(
-      this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping,
-    );
-  };
-
-  handleAddGithubRolesMapping: typeof addGithubRolesMapping = (data) => {
-    const newRole = { ...data, id: data.githubRole };
-    this.githubMapping = [...this.githubMapping, newRole];
-
-    return Promise.resolve(newRole);
-  };
-
-  handleDeleteGithubRolesMapping: typeof deleteGithubRolesMapping = (id) => {
-    this.githubMapping = this.githubMapping.filter((el) => el.id !== id);
-    return Promise.resolve();
-  };
-
-  addGitHubCustomRole = (id: string, permissions: (keyof GitHubMapping['permissions'])[]) => {
-    this.githubMapping = [...this.githubMapping, githubMappingMock(id, permissions)];
-  };
-
-  handleFetchGitLabConfigurations: typeof fetchGitLabConfigurations = () => {
-    return Promise.resolve({
-      configurations: this.gitlabConfigurations,
-      page: mockPaging({ total: this.gitlabConfigurations.length }),
-    });
-  };
-
-  handleFetchGitLabConfiguration: typeof fetchGitLabConfiguration = (id: string) => {
-    const configuration = this.gitlabConfigurations.find((c) => c.id === id);
-    if (!configuration) {
-      return Promise.reject();
-    }
-    return Promise.resolve(configuration);
-  };
-
-  handleCreateGitLabConfiguration: typeof createGitLabConfiguration = (data) => {
-    const newConfig = mockGitlabConfiguration({
-      ...omit(data, 'applicationId', 'clientSecret'),
-      id: '1',
-      enabled: true,
-    });
-    this.gitlabConfigurations = [...this.gitlabConfigurations, newConfig];
-    return Promise.resolve(newConfig);
-  };
-
-  handleUpdateGitLabConfiguration: typeof updateGitLabConfiguration = (id, data) => {
-    const index = this.gitlabConfigurations.findIndex((c) => c.id === id);
-    this.gitlabConfigurations[index] = { ...this.gitlabConfigurations[index], ...data };
-    return Promise.resolve(this.gitlabConfigurations[index]);
-  };
-
-  handleDeleteGitLabConfiguration: typeof deleteGitLabConfiguration = (id) => {
-    this.gitlabConfigurations = this.gitlabConfigurations.filter((c) => c.id !== id);
-    return Promise.resolve();
-  };
-
-  setGitlabConfigurations = (gitlabConfigurations: GitlabConfiguration[]) => {
-    this.gitlabConfigurations = gitlabConfigurations;
-  };
-
-  reset = () => {
-    this.scimStatus = false;
-    this.githubProvisioningStatus = false;
-    this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
-    this.githubMapping = cloneDeep(defaultMapping);
-    this.tasks = [];
-    this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
-  };
-}
diff --git a/server/sonar-web/src/main/js/api/mocks/GithubProvisioningServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GithubProvisioningServiceMock.ts
new file mode 100644 (file)
index 0000000..09cc48b
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { cloneDeep } from 'lodash';
+import { mockTask } from '../../helpers/mocks/tasks';
+import {
+  GitHubConfigurationStatus,
+  GitHubMapping,
+  GitHubProvisioningStatus,
+} from '../../types/provisioning';
+import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
+import {
+  activateGithubProvisioning,
+  addGithubRolesMapping,
+  checkConfigurationValidity,
+  deactivateGithubProvisioning,
+  deleteGithubRolesMapping,
+  fetchGithubProvisioningStatus,
+  fetchGithubRolesMapping,
+  updateGithubRolesMapping,
+} from '../github-provisioning';
+
+jest.mock('../github-provisioning');
+
+const defaultConfigurationStatus: GitHubConfigurationStatus = {
+  application: {
+    jit: {
+      status: GitHubProvisioningStatus.Success,
+    },
+    autoProvisioning: {
+      status: GitHubProvisioningStatus.Success,
+    },
+  },
+  installations: [
+    {
+      organization: 'testOrg',
+      autoProvisioning: {
+        status: GitHubProvisioningStatus.Success,
+      },
+      jit: {
+        status: GitHubProvisioningStatus.Success,
+      },
+    },
+  ],
+};
+
+const githubMappingMock = (
+  id: string,
+  permissions: (keyof GitHubMapping['permissions'])[],
+  isBaseRole = false,
+) => ({
+  id,
+  githubRole: id,
+  isBaseRole,
+  permissions: {
+    user: permissions.includes('user'),
+    codeViewer: permissions.includes('codeViewer'),
+    issueAdmin: permissions.includes('issueAdmin'),
+    securityHotspotAdmin: permissions.includes('securityHotspotAdmin'),
+    admin: permissions.includes('admin'),
+    scan: permissions.includes('scan'),
+  },
+});
+
+const defaultMapping: GitHubMapping[] = [
+  githubMappingMock('read', ['user', 'codeViewer'], true),
+  githubMappingMock(
+    'write',
+    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+    true,
+  ),
+  githubMappingMock('triage', ['user', 'codeViewer'], true),
+  githubMappingMock(
+    'maintain',
+    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'scan'],
+    true,
+  ),
+  githubMappingMock(
+    'admin',
+    ['user', 'codeViewer', 'issueAdmin', 'securityHotspotAdmin', 'admin', 'scan'],
+    true,
+  ),
+];
+
+export default class GithubProvisioningServiceMock {
+  githubProvisioningStatus: boolean;
+  githubConfigurationStatus: GitHubConfigurationStatus;
+  githubMapping: GitHubMapping[];
+  tasks: Task[];
+
+  constructor() {
+    this.githubProvisioningStatus = false;
+    this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
+    this.githubMapping = cloneDeep(defaultMapping);
+    this.tasks = [];
+    jest
+      .mocked(activateGithubProvisioning)
+      .mockImplementation(this.handleActivateGithubProvisioning);
+    jest
+      .mocked(deactivateGithubProvisioning)
+      .mockImplementation(this.handleDeactivateGithubProvisioning);
+    jest
+      .mocked(fetchGithubProvisioningStatus)
+      .mockImplementation(this.handleFetchGithubProvisioningStatus);
+    jest
+      .mocked(checkConfigurationValidity)
+      .mockImplementation(this.handleCheckConfigurationValidity);
+    jest.mocked(fetchGithubRolesMapping).mockImplementation(this.handleFetchGithubRolesMapping);
+    jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping);
+    jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping);
+    jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping);
+  }
+
+  addProvisioningTask = (overrides: Partial<Omit<Task, 'type'>> = {}) => {
+    this.tasks.push(
+      mockTask({
+        id: Math.random().toString(),
+        type: TaskTypes.GithubProvisioning,
+        ...overrides,
+      }),
+    );
+  };
+
+  setConfigurationValidity = (overrides: Partial<GitHubConfigurationStatus> = {}) => {
+    this.githubConfigurationStatus = {
+      ...this.githubConfigurationStatus,
+      ...overrides,
+    };
+  };
+
+  enableGithubProvisioning = () => {
+    this.githubProvisioningStatus = true;
+  };
+
+  handleActivateGithubProvisioning = () => {
+    this.githubProvisioningStatus = true;
+    return Promise.resolve();
+  };
+
+  handleDeactivateGithubProvisioning = () => {
+    this.githubProvisioningStatus = false;
+    return Promise.resolve();
+  };
+
+  handleFetchGithubProvisioningStatus = () => {
+    if (!this.githubProvisioningStatus) {
+      return Promise.resolve({ enabled: false });
+    }
+
+    const nextSync = this.tasks.find((t: Task) =>
+      [TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
+    );
+    const lastSync = this.tasks.find(
+      (t: Task) => ![TaskStatuses.InProgress, TaskStatuses.Pending].includes(t.status),
+    );
+
+    return Promise.resolve({
+      enabled: true,
+      nextSync: nextSync ? { status: nextSync.status } : undefined,
+      lastSync: lastSync
+        ? {
+            status: lastSync.status,
+            finishedAt: lastSync.executedAt,
+            startedAt: lastSync.startedAt,
+            executionTimeMs: lastSync.executionTimeMs,
+            summary: lastSync.status === TaskStatuses.Success ? 'Test summary' : undefined,
+            errorMessage: lastSync.errorMessage,
+            warningMessage: lastSync.warnings?.join() ?? undefined,
+          }
+        : undefined,
+    });
+  };
+
+  handleCheckConfigurationValidity = () => {
+    return Promise.resolve(this.githubConfigurationStatus);
+  };
+
+  handleFetchGithubRolesMapping: typeof fetchGithubRolesMapping = () => {
+    return Promise.resolve(this.githubMapping);
+  };
+
+  handleUpdateGithubRolesMapping: typeof updateGithubRolesMapping = (id, data) => {
+    this.githubMapping = this.githubMapping.map((mapping) =>
+      mapping.id === id ? { ...mapping, ...data } : mapping,
+    );
+
+    return Promise.resolve(
+      this.githubMapping.find((mapping) => mapping.id === id) as GitHubMapping,
+    );
+  };
+
+  handleAddGithubRolesMapping: typeof addGithubRolesMapping = (data) => {
+    const newRole = { ...data, id: data.githubRole };
+    this.githubMapping = [...this.githubMapping, newRole];
+
+    return Promise.resolve(newRole);
+  };
+
+  handleDeleteGithubRolesMapping: typeof deleteGithubRolesMapping = (id) => {
+    this.githubMapping = this.githubMapping.filter((el) => el.id !== id);
+    return Promise.resolve();
+  };
+
+  addGitHubCustomRole = (id: string, permissions: (keyof GitHubMapping['permissions'])[]) => {
+    this.githubMapping = [...this.githubMapping, githubMappingMock(id, permissions)];
+  };
+
+  reset = () => {
+    this.githubProvisioningStatus = false;
+    this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus);
+    this.githubMapping = cloneDeep(defaultMapping);
+    this.tasks = [];
+  };
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/GitlabProvisioningServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GitlabProvisioningServiceMock.ts
new file mode 100644 (file)
index 0000000..a2b99c8
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { cloneDeep, omit } from 'lodash';
+import { mockGitlabConfiguration } from '../../helpers/mocks/alm-integrations';
+import { mockPaging } from '../../helpers/testMocks';
+import { GitlabConfiguration } from '../../types/provisioning';
+import {
+  createGitLabConfiguration,
+  deleteGitLabConfiguration,
+  fetchGitLabConfiguration,
+  fetchGitLabConfigurations,
+  updateGitLabConfiguration,
+} from '../gitlab-provisioning';
+
+jest.mock('../gitlab-provisioning');
+
+const defaultGitlabConfiguration: GitlabConfiguration[] = [
+  mockGitlabConfiguration({ id: '1', enabled: true }),
+];
+
+export default class GitlabProvisioningServiceMock {
+  gitlabConfigurations: GitlabConfiguration[];
+
+  constructor() {
+    this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
+    jest.mocked(fetchGitLabConfigurations).mockImplementation(this.handleFetchGitLabConfigurations);
+    jest.mocked(fetchGitLabConfiguration).mockImplementation(this.handleFetchGitLabConfiguration);
+    jest.mocked(createGitLabConfiguration).mockImplementation(this.handleCreateGitLabConfiguration);
+    jest.mocked(updateGitLabConfiguration).mockImplementation(this.handleUpdateGitLabConfiguration);
+    jest.mocked(deleteGitLabConfiguration).mockImplementation(this.handleDeleteGitLabConfiguration);
+  }
+
+  handleFetchGitLabConfigurations: typeof fetchGitLabConfigurations = () => {
+    return Promise.resolve({
+      gitlabConfigurations: this.gitlabConfigurations,
+      page: mockPaging({ total: this.gitlabConfigurations.length }),
+    });
+  };
+
+  handleFetchGitLabConfiguration: typeof fetchGitLabConfiguration = (id: string) => {
+    const configuration = this.gitlabConfigurations.find((c) => c.id === id);
+    if (!configuration) {
+      return Promise.reject();
+    }
+    return Promise.resolve(configuration);
+  };
+
+  handleCreateGitLabConfiguration: typeof createGitLabConfiguration = (data) => {
+    const newConfig = mockGitlabConfiguration({
+      ...omit(data, 'applicationId', 'clientSecret'),
+      id: '1',
+      enabled: true,
+    });
+    this.gitlabConfigurations = [...this.gitlabConfigurations, newConfig];
+    return Promise.resolve(newConfig);
+  };
+
+  handleUpdateGitLabConfiguration: typeof updateGitLabConfiguration = (id, data) => {
+    const index = this.gitlabConfigurations.findIndex((c) => c.id === id);
+    this.gitlabConfigurations[index] = { ...this.gitlabConfigurations[index], ...data };
+    return Promise.resolve(this.gitlabConfigurations[index]);
+  };
+
+  handleDeleteGitLabConfiguration: typeof deleteGitLabConfiguration = (id) => {
+    this.gitlabConfigurations = this.gitlabConfigurations.filter((c) => c.id !== id);
+    return Promise.resolve();
+  };
+
+  setGitlabConfigurations = (gitlabConfigurations: GitlabConfiguration[]) => {
+    this.gitlabConfigurations = gitlabConfigurations;
+  };
+
+  reset = () => {
+    this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration);
+  };
+}
diff --git a/server/sonar-web/src/main/js/api/mocks/ScimProvisioningServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ScimProvisioningServiceMock.ts
new file mode 100644 (file)
index 0000000..aa8b2f6
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { activateScim, deactivateScim, fetchIsScimEnabled } from '../scim-provisioning';
+
+jest.mock('../scim-provisioning');
+
+export default class ScimProvisioningServiceMock {
+  scimStatus: boolean;
+
+  constructor() {
+    this.scimStatus = false;
+    jest.mocked(activateScim).mockImplementation(this.handleActivateScim);
+    jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim);
+    jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled);
+  }
+
+  handleActivateScim = () => {
+    this.scimStatus = true;
+    return Promise.resolve();
+  };
+
+  handleDeactivateScim = () => {
+    this.scimStatus = false;
+    return Promise.resolve();
+  };
+
+  handleFetchIsScimEnabled = () => {
+    return Promise.resolve(this.scimStatus);
+  };
+
+  reset = () => {
+    this.scimStatus = false;
+  };
+}
diff --git a/server/sonar-web/src/main/js/api/provisioning.ts b/server/sonar-web/src/main/js/api/provisioning.ts
deleted file mode 100644 (file)
index 27f5a31..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import axios from 'axios';
-import { keyBy } from 'lodash';
-import { throwGlobalError } from '../helpers/error';
-import { getJSON, post, postJSON } from '../helpers/request';
-import {
-  GitHubConfigurationStatus,
-  GitHubMapping,
-  GitLabConfigurationCreateBody,
-  GitLabConfigurationUpdateBody,
-  GithubStatus,
-  GitlabConfiguration,
-  ProvisioningType,
-} from '../types/provisioning';
-import { Paging } from '../types/types';
-import { getValues, resetSettingValue, setSimpleSettingValue } from './settings';
-
-const GITHUB_PERMISSION_MAPPINGS = '/api/v2/dop-translation/github-permission-mappings';
-
-export function fetchIsScimEnabled(): Promise<boolean> {
-  return getJSON('/api/scim_management/status')
-    .then((r) => r.enabled)
-    .catch(throwGlobalError);
-}
-
-export function activateScim(): Promise<void> {
-  return post('/api/scim_management/enable').catch(throwGlobalError);
-}
-
-export function deactivateScim(): Promise<void> {
-  return post('/api/scim_management/disable').catch(throwGlobalError);
-}
-
-export function fetchGithubProvisioningStatus(): Promise<GithubStatus> {
-  return getJSON('/api/github_provisioning/status').catch(throwGlobalError);
-}
-
-export function activateGithubProvisioning(): Promise<void> {
-  return post('/api/github_provisioning/enable').catch(throwGlobalError);
-}
-
-export function deactivateGithubProvisioning(): Promise<void> {
-  return post('/api/github_provisioning/disable').catch(throwGlobalError);
-}
-
-export function checkConfigurationValidity(): Promise<GitHubConfigurationStatus> {
-  return postJSON('/api/github_provisioning/check').catch(throwGlobalError);
-}
-
-export function syncNowGithubProvisioning(): Promise<void> {
-  return post('/api/github_provisioning/sync').catch(throwGlobalError);
-}
-
-export function fetchGithubRolesMapping() {
-  return axios
-    .get<{ githubPermissionsMappings: GitHubMapping[] }>(GITHUB_PERMISSION_MAPPINGS)
-    .then((data) => data.githubPermissionsMappings);
-}
-
-export function updateGithubRolesMapping(
-  role: string,
-  data: Partial<Pick<GitHubMapping, 'permissions'>>,
-) {
-  return axios.patch<GitHubMapping>(
-    `${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`,
-    data,
-  );
-}
-
-export function addGithubRolesMapping(data: Omit<GitHubMapping, 'id'>) {
-  return axios.post<GitHubMapping>(GITHUB_PERMISSION_MAPPINGS, data);
-}
-
-export function deleteGithubRolesMapping(role: string) {
-  return axios.delete(`${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`);
-}
-
-const GITLAB_SETTING_ENABLED = 'sonar.auth.gitlab.enabled';
-const GITLAB_SETTING_URL = 'sonar.auth.gitlab.url';
-const GITLAB_SETTING_APP_ID = 'sonar.auth.gitlab.applicationId.secured';
-const GITLAB_SETTING_SECRET = 'sonar.auth.gitlab.secret.secured';
-export const GITLAB_SETTING_ALLOW_SIGNUP = 'sonar.auth.gitlab.allowUsersToSignUp';
-const GITLAB_SETTING_GROUPS_SYNC = 'sonar.auth.gitlab.groupsSync';
-const GITLAB_SETTING_PROVISIONING_ENABLED = 'provisioning.gitlab.enabled';
-export const GITLAB_SETTING_GROUP_TOKEN = 'provisioning.gitlab.token.secured';
-export const GITLAB_SETTING_GROUPS = 'provisioning.gitlab.groups';
-
-const gitlabKeys = [
-  GITLAB_SETTING_ENABLED,
-  GITLAB_SETTING_URL,
-  GITLAB_SETTING_APP_ID,
-  GITLAB_SETTING_SECRET,
-  GITLAB_SETTING_ALLOW_SIGNUP,
-  GITLAB_SETTING_GROUPS_SYNC,
-  GITLAB_SETTING_PROVISIONING_ENABLED,
-  GITLAB_SETTING_GROUP_TOKEN,
-  GITLAB_SETTING_GROUPS,
-];
-
-const fieldKeyMap = {
-  enabled: GITLAB_SETTING_ENABLED,
-  url: GITLAB_SETTING_URL,
-  applicationId: GITLAB_SETTING_APP_ID,
-  clientSecret: GITLAB_SETTING_SECRET,
-  allowUsersToSignUp: GITLAB_SETTING_ALLOW_SIGNUP,
-  synchronizeUserGroups: GITLAB_SETTING_GROUPS_SYNC,
-  type: GITLAB_SETTING_PROVISIONING_ENABLED,
-  provisioningToken: GITLAB_SETTING_GROUP_TOKEN,
-  groups: GITLAB_SETTING_GROUPS,
-};
-
-const getGitLabConfiguration = async (): Promise<GitlabConfiguration | null> => {
-  const values = await getValues({
-    keys: gitlabKeys,
-  });
-  const valuesMap = keyBy(values, 'key');
-  if (!valuesMap[GITLAB_SETTING_APP_ID] || !valuesMap[GITLAB_SETTING_SECRET]) {
-    return null;
-  }
-  return {
-    id: '1',
-    enabled: valuesMap[GITLAB_SETTING_ENABLED]?.value === 'true',
-    url: valuesMap[GITLAB_SETTING_URL]?.value ?? 'https://gitlab.com',
-    synchronizeUserGroups: valuesMap[GITLAB_SETTING_GROUPS_SYNC]?.value === 'true',
-    type:
-      valuesMap[GITLAB_SETTING_PROVISIONING_ENABLED]?.value === 'true'
-        ? ProvisioningType.auto
-        : ProvisioningType.jit,
-    groups: valuesMap[GITLAB_SETTING_GROUPS]?.values
-      ? valuesMap[GITLAB_SETTING_GROUPS]?.values
-      : [],
-    allowUsersToSignUp: valuesMap[GITLAB_SETTING_ALLOW_SIGNUP]?.value === 'true',
-  };
-};
-
-export async function fetchGitLabConfigurations(): Promise<{
-  configurations: GitlabConfiguration[];
-  page: Paging;
-}> {
-  const config = await getGitLabConfiguration();
-  return {
-    configurations: config ? [config] : [],
-    page: {
-      pageIndex: 1,
-      pageSize: 1,
-      total: config ? 1 : 0,
-    },
-  };
-}
-
-export async function fetchGitLabConfiguration(_id: string): Promise<GitlabConfiguration> {
-  const configuration = await getGitLabConfiguration();
-  if (!configuration) {
-    return Promise.reject(new Error('GitLab configuration not found'));
-  }
-  return Promise.resolve(configuration);
-}
-
-export async function createGitLabConfiguration(
-  configuration: GitLabConfigurationCreateBody,
-): Promise<GitlabConfiguration> {
-  await Promise.all(
-    Object.entries(configuration).map(
-      ([key, value]: [key: keyof GitLabConfigurationCreateBody, value: string]) =>
-        setSimpleSettingValue({ key: fieldKeyMap[key], value }),
-    ),
-  );
-  await setSimpleSettingValue({ key: fieldKeyMap.enabled, value: 'true' });
-  return fetchGitLabConfiguration('');
-}
-
-export async function updateGitLabConfiguration(
-  _id: string,
-  configuration: Partial<GitLabConfigurationUpdateBody>,
-): Promise<GitlabConfiguration> {
-  await Promise.all(
-    Object.entries(configuration).map(
-      ([key, value]: [key: keyof typeof fieldKeyMap, value: string | string[]]) => {
-        if (fieldKeyMap[key] === GITLAB_SETTING_PROVISIONING_ENABLED) {
-          return setSimpleSettingValue({
-            key: fieldKeyMap[key],
-            value: value === ProvisioningType.auto ? 'true' : 'false',
-          });
-        } else if (typeof value === 'boolean') {
-          return setSimpleSettingValue({ key: fieldKeyMap[key], value: value ? 'true' : 'false' });
-        } else if (Array.isArray(value)) {
-          return setSimpleSettingValue({ key: fieldKeyMap[key], values: value });
-        }
-        return setSimpleSettingValue({ key: fieldKeyMap[key], value });
-      },
-    ),
-  );
-  return fetchGitLabConfiguration('');
-}
-
-export function deleteGitLabConfiguration(_id: string): Promise<void> {
-  return resetSettingValue({ keys: gitlabKeys.join(',') });
-}
diff --git a/server/sonar-web/src/main/js/api/scim-provisioning.ts b/server/sonar-web/src/main/js/api/scim-provisioning.ts
new file mode 100644 (file)
index 0000000..fcb6806
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { throwGlobalError } from '../helpers/error';
+import { getJSON, post } from '../helpers/request';
+
+export function fetchIsScimEnabled(): Promise<boolean> {
+  return getJSON('/api/scim_management/status')
+    .then((r) => r.enabled)
+    .catch(throwGlobalError);
+}
+
+export function activateScim(): Promise<void> {
+  return post('/api/scim_management/enable').catch(throwGlobalError);
+}
+
+export function deactivateScim(): Promise<void> {
+  return post('/api/scim_management/disable').catch(throwGlobalError);
+}
index 22dcf68356e3a9083bc19e845e14f78e2156daa9..c27c817fa209a9c2292725910469b611db354698 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { useGitHubSyncStatusQuery } from '../../queries/identity-provider';
+import { useGitHubSyncStatusQuery } from '../../queries/identity-provider/github';
 import AlmSynchronisationWarning from './AlmSynchronisationWarning';
 import './SystemAnnouncement.css';
 
index 227e2e55129b3ac5f4494b99c2ba17ce28d79436..bc114cff1c33b0dac3f45f766b8430ac29d49c1c 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { useGitLabSyncStatusQuery } from '../../queries/identity-provider';
+import { useGitLabSyncStatusQuery } from '../../queries/identity-provider/gitlab';
 import AlmSynchronisationWarning from './AlmSynchronisationWarning';
 import './SystemAnnouncement.css';
 
index 4927e76985c6086792ac61c37e2f3ecdbad44adc..d041a45e106df2d73fb3542e762dc80972fc273c 100644 (file)
@@ -27,7 +27,7 @@ import SearchBox from '../../components/controls/SearchBox';
 import Suggestions from '../../components/embed-docs-modal/Suggestions';
 import { translate } from '../../helpers/l10n';
 import { useGroupsQueries } from '../../queries/groups';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
 import { Provider } from '../../types/types';
 import Header from './components/Header';
 import List from './components/List';
index f1fbe9bdaaee395671ffb851c4addcc0664a13f6..b1d64c35f157c7901ffb3395715fbe0a8ef42619 100644 (file)
@@ -21,7 +21,7 @@
 import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
 import GroupMembershipsServiceMock from '../../../api/mocks/GroupMembersipsServiceMock';
 import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
 import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
@@ -38,7 +38,7 @@ const systemHandler = new SystemServiceMock();
 const handler = new GroupsServiceMock();
 const groupMembershipsHandler = new GroupMembershipsServiceMock();
 const userHandler = new UsersServiceMock(groupMembershipsHandler);
-const authenticationHandler = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
 
 const ui = {
   createGroupButton: byRole('button', { name: 'groups.create_group' }),
@@ -100,7 +100,7 @@ const ui = {
 beforeEach(() => {
   handler.reset();
   systemHandler.reset();
-  authenticationHandler.reset();
+  githubHandler.reset();
   userHandler.reset();
   groupMembershipsHandler.reset();
   groupMembershipsHandler.memberships = [
@@ -355,12 +355,12 @@ describe('in manage mode', () => {
 
   describe('Github Provisioning', () => {
     beforeEach(() => {
-      authenticationHandler.handleActivateGithubProvisioning();
+      githubHandler.handleActivateGithubProvisioning();
       systemHandler.setProvider(Provider.Github);
     });
 
     it('should display a success status when the synchronisation is a success', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Success,
         executedAt: '2022-02-03T11:45:35+0200',
       });
@@ -370,11 +370,11 @@ describe('in manage mode', () => {
     });
 
     it('should display a success status even when another task is pending', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Pending,
         executedAt: '2022-02-03T11:55:35+0200',
       });
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Success,
         executedAt: '2022-02-03T11:45:35+0200',
       });
@@ -385,7 +385,7 @@ describe('in manage mode', () => {
     });
 
     it('should display an error alert when the synchronisation failed', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Failed,
         executedAt: '2022-02-03T11:45:35+0200',
         errorMessage: "T'es mauvais Jacques",
@@ -398,11 +398,11 @@ describe('in manage mode', () => {
     });
 
     it('should display an error alert even when another task is in progress', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.InProgress,
         executedAt: '2022-02-03T11:55:35+0200',
       });
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Failed,
         executedAt: '2022-02-03T11:45:35+0200',
         errorMessage: "T'es mauvais Jacques",
index 8ba5375730f6698e9a75fcbb643133a90dd60921..c3cd95ebff5134690d748783845906a0471bb4e9 100644 (file)
@@ -25,7 +25,7 @@ import { Alert } from '../../../components/ui/Alert';
 import Spinner from '../../../components/ui/Spinner';
 import { throwGlobalError } from '../../../helpers/error';
 import { translate } from '../../../helpers/l10n';
-import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider/github';
 import { PERMISSION_TEMPLATES_PATH } from '../utils';
 import Form from './Form';
 
index 58f4fde62069cfd257a8e2c2f16a2519a0ca1ebd..abfd687cf2e6520655d5dc78c162e8a9b526ba02 100644 (file)
@@ -30,7 +30,7 @@ import {
   PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE,
   convertToPermissionDefinitions,
 } from '../../../helpers/permissions';
-import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../queries/identity-provider/github';
 import { Paging, PermissionGroup, PermissionTemplate, PermissionUser } from '../../../types/types';
 import TemplateDetails from './TemplateDetails';
 import TemplateHeader from './TemplateHeader';
index 5e808477cae8c5bf259a13ae3886c5d428cae3df..f27048415beed0f6e7852c35642af4cc1fca885e 100644 (file)
@@ -21,7 +21,7 @@ import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import { uniq } from 'lodash';
-import AuthenticationServiceMock from '../../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../../api/mocks/GithubProvisioningServiceMock';
 import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock';
 import { mockPermissionGroup, mockPermissionUser } from '../../../../helpers/mocks/permissions';
 import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../../../helpers/permissions';
@@ -35,11 +35,11 @@ import { PermissionGroup, PermissionUser } from '../../../../types/types';
 import routes from '../../routes';
 
 const serviceMock = new PermissionsServiceMock();
-const authServiceMock = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
 
 beforeEach(() => {
   serviceMock.reset();
-  authServiceMock.reset();
+  githubHandler.reset();
 });
 
 describe('rendering', () => {
@@ -394,7 +394,7 @@ it.each([ComponentQualifier.Project, ComponentQualifier.Application, ComponentQu
 it('should show github warning', async () => {
   const user = userEvent.setup();
   const ui = getPageObject(user);
-  authServiceMock.githubProvisioningStatus = true;
+  githubHandler.githubProvisioningStatus = true;
   renderPermissionTemplatesApp(undefined, [Feature.GithubProvisioning]);
 
   expect(await ui.githubWarning.find()).toBeInTheDocument();
index 6169fef0977778010acd13e6d7c51dd496c3b064..1c37a4e99448fde43a16866853037fd54f1eefc8 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
 import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/system';
-import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
 import { isApplication, isPortfolioLike, isProject } from '../../../../types/component';
 import { Component } from '../../../../types/types';
 import ApplyTemplate from './ApplyTemplate';
index a93520936053bad639ac1b851ced6474481bbbe3..556b1c1e1327977e9e957a9e908d4e3ea5ce2133 100644 (file)
@@ -33,7 +33,7 @@ import {
   convertToPermissionDefinitions,
 } from '../../../../helpers/permissions';
 import { useIsGitHubProjectQuery } from '../../../../queries/devops-integration';
-import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
 import { ComponentContextShape, Visibility } from '../../../../types/component';
 import { Permissions } from '../../../../types/permissions';
 import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
index a36e2ca6b33a40cd84ecd99985e2392659fdea11..493113842f58fb001f49bf1056227bc9a0539be1 100644 (file)
@@ -21,7 +21,7 @@
 import { act, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
-import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
 import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
 import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
 import { mockComponent } from '../../../../../helpers/mocks/component';
@@ -47,19 +47,19 @@ import { projectPermissionsRoutes } from '../../../routes';
 import { getPageObject } from '../../../test-utils';
 
 let serviceMock: PermissionsServiceMock;
-let authHandler: AuthenticationServiceMock;
+let githubHandler: GithubProvisioningServiceMock;
 let almHandler: AlmSettingsServiceMock;
 let systemHandler: SystemServiceMock;
 beforeAll(() => {
   serviceMock = new PermissionsServiceMock();
-  authHandler = new AuthenticationServiceMock();
+  githubHandler = new GithubProvisioningServiceMock();
   almHandler = new AlmSettingsServiceMock();
   systemHandler = new SystemServiceMock();
 });
 
 afterEach(() => {
   serviceMock.reset();
-  authHandler.reset();
+  githubHandler.reset();
   almHandler.reset();
 });
 
@@ -248,7 +248,7 @@ describe('GH provisioning', () => {
   it('should not allow to change visibility for GH Project with auto-provisioning', async () => {
     const user = userEvent.setup();
     const ui = getPageObject(user);
-    authHandler.githubProvisioningStatus = true;
+    githubHandler.githubProvisioningStatus = true;
     almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
       almSetting: 'test',
       repository: 'test',
@@ -270,7 +270,7 @@ describe('GH provisioning', () => {
   it('should allow to change visibility for non-GH Project', async () => {
     const user = userEvent.setup();
     const ui = getPageObject(user);
-    authHandler.githubProvisioningStatus = true;
+    githubHandler.githubProvisioningStatus = true;
     almHandler.handleSetProjectBinding(AlmKeys.Azure, {
       almSetting: 'test',
       repository: 'test',
@@ -292,7 +292,7 @@ describe('GH provisioning', () => {
   it('should allow to change visibility for GH Project with disabled auto-provisioning', async () => {
     const user = userEvent.setup();
     const ui = getPageObject(user);
-    authHandler.githubProvisioningStatus = false;
+    githubHandler.githubProvisioningStatus = false;
     almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
       almSetting: 'test',
       repository: 'test',
@@ -314,7 +314,7 @@ describe('GH provisioning', () => {
   it('should have disabled permissions for GH Project', async () => {
     const user = userEvent.setup();
     const ui = getPageObject(user);
-    authHandler.githubProvisioningStatus = true;
+    githubHandler.githubProvisioningStatus = true;
     almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
       almSetting: 'test',
       repository: 'test',
@@ -393,7 +393,7 @@ describe('GH provisioning', () => {
   it('should allow to change permissions for GH Project without auto-provisioning', async () => {
     const user = userEvent.setup();
     const ui = getPageObject(user);
-    authHandler.githubProvisioningStatus = false;
+    githubHandler.githubProvisioningStatus = false;
     almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
       almSetting: 'test',
       repository: 'test',
@@ -422,7 +422,7 @@ describe('GH provisioning', () => {
   it('should allow to change permissions for non-GH Project', async () => {
     const user = userEvent.setup();
     const ui = getPageObject(user);
-    authHandler.githubProvisioningStatus = true;
+    githubHandler.githubProvisioningStatus = true;
     renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
     await ui.appLoaded();
 
index 5d922d8e0bb3bf44e5802b6f29e8b87244eff698..2706db7009a84ffde44f0f1ac3c1e1efed3a2cab 100644 (file)
@@ -23,7 +23,7 @@ import Radio from '../../components/controls/Radio';
 import { Button, ResetButtonLink } from '../../components/controls/buttons';
 import { Alert } from '../../components/ui/Alert';
 import { translate } from '../../helpers/l10n';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
 import { Visibility } from '../../types/component';
 
 export interface Props {
index 9ced6b325454a02ffe0a5b18beeefbd36c06fbf1..270a65a0f334acec71caba1a23b231e79a3afdd5 100644 (file)
@@ -27,7 +27,7 @@ import QualifierIcon from '../../components/icons/QualifierIcon';
 import DateFormatter from '../../components/intl/DateFormatter';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { getComponentOverviewUrl } from '../../helpers/urls';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
 import { ComponentQualifier } from '../../types/component';
 import { LoggedInUser } from '../../types/users';
 import './ProjectRow.css';
index 2f221c82070309c5b27b330980f20eb8c0e60a9d..edca0610fc1072f0fe3687c735732a4713bbab28 100644 (file)
@@ -25,7 +25,7 @@ import Spinner from '../../components/ui/Spinner';
 import { throwGlobalError } from '../../helpers/error';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 import { getComponentPermissionsUrl } from '../../helpers/urls';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
 import { LoggedInUser } from '../../types/users';
 import ApplyTemplate from '../permissions/project/components/ApplyTemplate';
 import RestoreAccessModal from './RestoreAccessModal';
index ab5cae86f46a0fc8a8757749eee9bff1c81e9d36..7c52894a1b8bd17b55a5899b233319c574a955fd 100644 (file)
@@ -20,7 +20,7 @@
 import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import selectEvent from 'react-select-event';
-import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
+import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
 import PermissionsServiceMock from '../../../api/mocks/PermissionsServiceMock';
 import ProjectManagementServiceMock from '../../../api/mocks/ProjectsManagementServiceMock';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
@@ -41,7 +41,7 @@ let login: string;
 
 const permissionsHandler = new PermissionsServiceMock();
 const settingsHandler = new SettingsServiceMock();
-const authHandler = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
 const handler = new ProjectManagementServiceMock(settingsHandler);
 
 jest.mock('../../../api/navigation', () => ({
@@ -167,7 +167,7 @@ afterEach(() => {
 
   permissionsHandler.reset();
   settingsHandler.reset();
-  authHandler.reset();
+  githubHandler.reset();
   handler.reset();
 });
 
@@ -483,7 +483,7 @@ it('should restore access to admin', async () => {
 
 it('should restore access for github project', async () => {
   const user = userEvent.setup();
-  authHandler.enableGithubProvisioning();
+  githubHandler.enableGithubProvisioning();
   renderProjectManagementApp(
     {},
     { login: 'gooduser2', local: true },
@@ -504,7 +504,7 @@ it('should restore access for github project', async () => {
 
 it('should not allow to restore access on github project for GH user', async () => {
   const user = userEvent.setup();
-  authHandler.githubProvisioningStatus = true;
+  githubHandler.githubProvisioningStatus = true;
   renderProjectManagementApp(
     {},
     { login: 'gooduser2', local: false },
@@ -519,7 +519,7 @@ it('should not allow to restore access on github project for GH user', async ()
 
 it('should show github warning on changing default visibility to admin', async () => {
   const user = userEvent.setup();
-  authHandler.githubProvisioningStatus = true;
+  githubHandler.githubProvisioningStatus = true;
   renderProjectManagementApp({}, {}, { featureList: [Feature.GithubProvisioning] });
   await user.click(ui.editDefaultVisibility.get());
   expect(await ui.changeDefaultVisibilityDialog.find()).toBeInTheDocument();
@@ -546,7 +546,7 @@ it('should not apply permissions for github projects', async () => {
 });
 
 it('should not show local badge for applications and portfolios', async () => {
-  authHandler.enableGithubProvisioning();
+  githubHandler.enableGithubProvisioning();
   renderProjectManagementApp({}, {}, { featureList: [Feature.GithubProvisioning] });
   await waitFor(() => expect(screen.getAllByText('local')).toHaveLength(3));
 
index 12d5d723408031d3fdbc25ad1dfd432a158c3952..063386d3d343d3dbb8846c35dd746c7ff6d7f12d 100644 (file)
@@ -77,7 +77,7 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
 
   const [query, setSearchParams] = useSearchParams();
 
-  const currentTab = (query.get('tab') || SAML) as AuthenticationTabs;
+  const currentTab = (query.get('tab') ?? SAML) as AuthenticationTabs;
 
   const tabs = [
     {
@@ -113,11 +113,10 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
     },
   ] as const;
 
-  const [samlDefinitions, githubDefinitions, gitlabDefinitions] = React.useMemo(
+  const [samlDefinitions, githubDefinitions] = React.useMemo(
     () => [
       definitions.filter((def) => def.subCategory === SAML),
       definitions.filter((def) => def.subCategory === AlmKeys.GitHub),
-      definitions.filter((def) => def.subCategory === AlmKeys.GitLab),
     ],
     [definitions],
   );
@@ -175,48 +174,48 @@ export function Authentication(props: Props & WithAvailableFeaturesProps) {
                 aria-labelledby={getTabId(tab.key)}
                 id={getTabPanelId(tab.key)}
               >
-                <div className="big-padded-top big-padded-left big-padded-right">
-                  {tab.key === SAML && <SamlAuthenticationTab definitions={samlDefinitions} />}
-
-                  {tab.key === AlmKeys.GitHub && (
-                    <GithubAuthenticationTab
-                      currentTab={currentTab}
-                      definitions={githubDefinitions}
-                    />
-                  )}
-
-                  {tab.key === AlmKeys.GitLab && (
-                    <GitLabAuthenticationTab definitions={gitlabDefinitions} />
-                  )}
-
-                  {tab.key === AlmKeys.BitbucketServer && (
-                    <>
-                      <Alert variant="info">
-                        <FormattedMessage
-                          id="settings.authentication.help"
-                          defaultMessage={translate('settings.authentication.help')}
-                          values={{
-                            link: (
-                              <DocLink
-                                to={`/instance-administration/authentication/${
-                                  DOCUMENTATION_LINK_SUFFIXES[tab.key]
-                                }/`}
-                              >
-                                {translate('settings.authentication.help.link')}
-                              </DocLink>
-                            ),
-                          }}
-                        />
-                      </Alert>
-                      <CategoryDefinitionsList
-                        category={AUTHENTICATION_CATEGORY}
-                        definitions={definitions}
-                        subCategory={tab.key}
-                        displaySubCategoryTitle={false}
+                {currentTab === tab.key && (
+                  <div className="big-padded-top big-padded-left big-padded-right">
+                    {tab.key === SAML && <SamlAuthenticationTab definitions={samlDefinitions} />}
+
+                    {tab.key === AlmKeys.GitHub && (
+                      <GithubAuthenticationTab
+                        currentTab={currentTab}
+                        definitions={githubDefinitions}
                       />
-                    </>
-                  )}
-                </div>
+                    )}
+
+                    {tab.key === AlmKeys.GitLab && <GitLabAuthenticationTab />}
+
+                    {tab.key === AlmKeys.BitbucketServer && (
+                      <>
+                        <Alert variant="info">
+                          <FormattedMessage
+                            id="settings.authentication.help"
+                            defaultMessage={translate('settings.authentication.help')}
+                            values={{
+                              link: (
+                                <DocLink
+                                  to={`/instance-administration/authentication/${
+                                    DOCUMENTATION_LINK_SUFFIXES[tab.key]
+                                  }/`}
+                                >
+                                  {translate('settings.authentication.help.link')}
+                                </DocLink>
+                              ),
+                            }}
+                          />
+                        </Alert>
+                        <CategoryDefinitionsList
+                          category={AUTHENTICATION_CATEGORY}
+                          definitions={definitions}
+                          subCategory={tab.key}
+                          displaySubCategoryTitle={false}
+                        />
+                      </>
+                    )}
+                  </div>
+                )}
               </div>
             ))}
           </>
index 16b932ae22383ae5fe01da65d37eb3740d142c06..627fba41c6b31db493addc92556be74280c2d8f8 100644 (file)
@@ -23,7 +23,7 @@ import DocLink from '../../../../components/common/DocLink';
 import Modal from '../../../../components/controls/Modal';
 import { Button } from '../../../../components/controls/buttons';
 import { translate } from '../../../../helpers/l10n';
-import { useToggleGithubProvisioningMutation } from '../../../../queries/identity-provider';
+import { useToggleGithubProvisioningMutation } from '../../../../queries/identity-provider/github';
 import { useGetValueQuery, useResetSettingsMutation } from '../../../../queries/settings';
 
 const GITHUB_PERMISSION_USER_CONSENT =
index 90e210bfd84ce0cf84bce92a1c1cb29ec617a3d3..8b0c0987a5bbd69143707edc38d8ef41b8833a7b 100644 (file)
@@ -27,7 +27,7 @@ import ClearIcon from '../../../../components/icons/ClearIcon';
 import HelpIcon from '../../../../components/icons/HelpIcon';
 import { Alert, AlertVariant } from '../../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { useCheckGitHubConfigQuery } from '../../../../queries/identity-provider';
+import { useCheckGitHubConfigQuery } from '../../../../queries/identity-provider/github';
 import { GitHubProvisioningStatus } from '../../../../types/provisioning';
 
 const intlPrefix = 'settings.authentication.github.configuration.validation';
index e889bcda3a0b6e5ca7510c83503b2ec8f5f2c2f6..d2b462e76da53440a2d6aa37914e2ffb8e60ae48 100644 (file)
@@ -30,7 +30,7 @@ import {
   convertToPermissionDefinitions,
   isPermissionDefinitionGroup,
 } from '../../../../helpers/permissions';
-import { useGithubRolesMappingQuery } from '../../../../queries/identity-provider';
+import { useGithubRolesMappingQuery } from '../../../../queries/identity-provider/github';
 import { GitHubMapping } from '../../../../types/provisioning';
 
 interface Props {
index 03fa8f27e11b2f94980972d0bef2e49f2c50de5a..832702ddd33abeaad73677b16542826fe6ef98dd 100644 (file)
 import { isEqual, omitBy } from 'lodash';
 import React, { FormEvent, useContext } from 'react';
 import { FormattedMessage } from 'react-intl';
-import {
-  GITLAB_SETTING_ALLOW_SIGNUP,
-  GITLAB_SETTING_GROUPS,
-  GITLAB_SETTING_GROUP_TOKEN,
-} from '../../../../api/provisioning';
 import GitLabSynchronisationWarning from '../../../../app/components/GitLabSynchronisationWarning';
 import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
 import DocLink from '../../../../components/common/DocLink';
@@ -37,35 +32,52 @@ import EditIcon from '../../../../components/icons/EditIcon';
 import { Alert } from '../../../../components/ui/Alert';
 import Spinner from '../../../../components/ui/Spinner';
 import { translate } from '../../../../helpers/l10n';
+import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
 import {
   useDeleteGitLabConfigurationMutation,
   useGitLabConfigurationsQuery,
-  useIdentityProviderQuery,
   useUpdateGitLabConfigurationMutation,
-} from '../../../../queries/identity-provider';
+} from '../../../../queries/identity-provider/gitlab';
 import { AlmKeys } from '../../../../types/alm-settings';
 import { Feature } from '../../../../types/features';
 import { GitLabConfigurationUpdateBody, ProvisioningType } from '../../../../types/provisioning';
-import { ExtendedSettingDefinition } from '../../../../types/settings';
+import { DefinitionV2, SettingType } from '../../../../types/settings';
 import { Provider } from '../../../../types/types';
 import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
 import AuthenticationFormField from './AuthenticationFormField';
 import GitLabConfigurationForm from './GitLabConfigurationForm';
 
-interface GitLabAuthenticationTab {
-  definitions: ExtendedSettingDefinition[];
-}
-
 interface ChangesForm {
-  type?: GitLabConfigurationUpdateBody['type'];
+  synchronizationType?: GitLabConfigurationUpdateBody['synchronizationType'];
   allowUsersToSignUp?: GitLabConfigurationUpdateBody['allowUsersToSignUp'];
   provisioningToken?: GitLabConfigurationUpdateBody['provisioningToken'];
-  groups?: GitLabConfigurationUpdateBody['groups'];
+  provisioningGroups?: GitLabConfigurationUpdateBody['provisioningGroups'];
 }
 
-export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthenticationTab>) {
-  const { definitions } = props;
+const definitions: Record<keyof Omit<ChangesForm, 'synchronizationType'>, DefinitionV2> = {
+  provisioningGroups: {
+    name: translate('settings.authentication.gitlab.form.provisioningGroups.name'),
+    key: 'provisioningGroups',
+    description: translate('settings.authentication.gitlab.form.provisioningGroups.description'),
+    secured: false,
+    multiValues: true,
+  },
+  allowUsersToSignUp: {
+    name: translate('settings.authentication.gitlab.form.allowUsersToSignUp.name'),
+    secured: false,
+    key: 'allowUsersToSignUp',
+    description: translate('settings.authentication.gitlab.form.allowUsersToSignUp.description'),
+    type: SettingType.BOOLEAN,
+  },
+  provisioningToken: {
+    name: translate('settings.authentication.gitlab.form.provisioningToken.name'),
+    secured: true,
+    key: 'provisioningToken',
+    description: translate('settings.authentication.gitlab.form.provisioningToken.description'),
+  },
+};
 
+export default function GitLabAuthenticationTab() {
   const [openForm, setOpenForm] = React.useState(false);
   const [changes, setChanges] = React.useState<ChangesForm | undefined>(undefined);
   const [tokenKey, setTokenKey] = React.useState<number>(0);
@@ -77,7 +89,7 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
 
   const { data: identityProvider } = useIdentityProviderQuery();
   const { data: list, isLoading: isLoadingList } = useGitLabConfigurationsQuery();
-  const configuration = list?.configurations[0];
+  const configuration = list?.gitlabConfigurations[0];
 
   const { mutate: updateConfig, isLoading: isUpdating } = useUpdateGitLabConfigurationMutation();
   const { mutate: deleteConfig, isLoading: isDeleting } = useDeleteGitLabConfigurationMutation();
@@ -98,7 +110,7 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
 
   const handleSubmit = (e: FormEvent) => {
     e.preventDefault();
-    if (changes?.type !== undefined) {
+    if (changes?.synchronizationType !== undefined) {
       setShowConfirmProvisioningModal(true);
     } else {
       updateProvisioning();
@@ -123,38 +135,39 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
 
   const setJIT = () =>
     setChangesWithCheck({
-      type: ProvisioningType.jit,
+      synchronizationType: ProvisioningType.jit,
       provisioningToken: undefined,
-      groups: undefined,
+      provisioningGroups: undefined,
     });
 
   const setAuto = () =>
     setChangesWithCheck({
-      type: ProvisioningType.auto,
+      synchronizationType: ProvisioningType.auto,
       allowUsersToSignUp: undefined,
     });
 
   const hasDifferentProvider =
     identityProvider?.provider !== undefined && identityProvider.provider !== Provider.Gitlab;
-  const allowUsersToSignUpDefinition = definitions.find(
-    (d) => d.key === GITLAB_SETTING_ALLOW_SIGNUP,
-  );
-  const provisioningTokenDefinition = definitions.find((d) => d.key === GITLAB_SETTING_GROUP_TOKEN);
-  const provisioningGroupDefinition = definitions.find((d) => d.key === GITLAB_SETTING_GROUPS);
+  const allowUsersToSignUpDefinition = definitions.allowUsersToSignUp;
+  const provisioningTokenDefinition = definitions.provisioningToken;
+  const provisioningGroupDefinition = definitions.provisioningGroups;
 
-  const provisioningType = changes?.type ?? configuration?.type;
+  const provisioningType = changes?.synchronizationType ?? configuration?.synchronizationType;
   const allowUsersToSignUp = changes?.allowUsersToSignUp ?? configuration?.allowUsersToSignUp;
   const provisioningToken = changes?.provisioningToken;
-  const groups = changes?.groups ?? configuration?.groups;
+  const groups = changes?.provisioningGroups ?? configuration?.provisioningGroups;
 
   const canSave = () => {
     if (!configuration || changes === undefined) {
       return false;
     }
-    const type = changes.type ?? configuration.type;
+    const type = changes.synchronizationType ?? configuration.synchronizationType;
     if (type === ProvisioningType.auto) {
-      const hasConfigGroups = configuration.groups && configuration.groups.length > 0;
-      const hasGroups = changes.groups ? changes.groups.length > 0 : hasConfigGroups;
+      const hasConfigGroups =
+        configuration.provisioningGroups && configuration.provisioningGroups.length > 0;
+      const hasGroups = changes.provisioningGroups
+        ? changes.provisioningGroups.length > 0
+        : hasConfigGroups;
       const hasToken = hasConfigGroups
         ? changes.provisioningToken !== ''
         : !!changes.provisioningToken;
@@ -165,13 +178,18 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
 
   const setChangesWithCheck = (newChanges: ChangesForm) => {
     const newValue = {
-      type: configuration?.type === newChanges.type ? undefined : newChanges.type,
+      synchronizationType:
+        configuration?.synchronizationType === newChanges.synchronizationType
+          ? undefined
+          : newChanges.synchronizationType,
       allowUsersToSignUp:
         configuration?.allowUsersToSignUp === newChanges.allowUsersToSignUp
           ? undefined
           : newChanges.allowUsersToSignUp,
       provisioningToken: newChanges.provisioningToken,
-      groups: isEqual(configuration?.groups, newChanges.groups) ? undefined : newChanges.groups,
+      provisioningGroups: isEqual(configuration?.provisioningGroups, newChanges.provisioningGroups)
+        ? undefined
+        : newChanges.provisioningGroups,
     };
     if (Object.values(newValue).some((v) => v !== undefined)) {
       setChanges(newValue);
@@ -204,7 +222,7 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
               <p>{configuration.url}</p>
               <Tooltip
                 overlay={
-                  configuration.type === ProvisioningType.auto
+                  configuration.synchronizationType === ProvisioningType.auto
                     ? translate('settings.authentication.form.disable.tooltip')
                     : null
                 }
@@ -212,7 +230,9 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
                 <Button
                   className="spacer-top"
                   onClick={toggleEnable}
-                  disabled={isUpdating || configuration.type === ProvisioningType.auto}
+                  disabled={
+                    isUpdating || configuration.synchronizationType === ProvisioningType.auto
+                  }
                 >
                   {configuration.enabled
                     ? translate('settings.authentication.form.disable')
@@ -289,7 +309,7 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
                               allowUsersToSignUp: value as boolean,
                             })
                           }
-                          isNotSet={configuration.type !== ProvisioningType.auto}
+                          isNotSet={configuration.synchronizationType !== ProvisioningType.auto}
                         />
                       )}
                   </RadioCard>
@@ -334,44 +354,45 @@ export default function GitLabAuthenticationTab(props: Readonly<GitLabAuthentica
                           />
                         </p>
 
-                        {configuration?.type === ProvisioningType.auto && (
+                        {configuration?.synchronizationType === ProvisioningType.auto && (
                           <>
                             <GitLabSynchronisationWarning />
                             <hr className="spacer-top" />
                           </>
                         )}
 
-                        {provisioningType === ProvisioningType.auto &&
-                          provisioningTokenDefinition !== undefined &&
-                          provisioningGroupDefinition !== undefined && (
-                            <>
-                              <AuthenticationFormField
-                                settingValue={provisioningToken}
-                                key={tokenKey}
-                                definition={provisioningTokenDefinition}
-                                mandatory
-                                onFieldChange={(_, value) =>
-                                  setChangesWithCheck({
-                                    ...changes,
-                                    provisioningToken: value as string,
-                                  })
-                                }
-                                isNotSet={
-                                  configuration.type !== ProvisioningType.auto &&
-                                  configuration.groups?.length === 0
-                                }
-                              />
-                              <AuthenticationFormField
-                                settingValue={groups}
-                                definition={provisioningGroupDefinition}
-                                mandatory
-                                onFieldChange={(_, values) =>
-                                  setChangesWithCheck({ ...changes, groups: values as string[] })
-                                }
-                                isNotSet={configuration.type !== ProvisioningType.auto}
-                              />
-                            </>
-                          )}
+                        {provisioningType === ProvisioningType.auto && (
+                          <>
+                            <AuthenticationFormField
+                              settingValue={provisioningToken}
+                              key={tokenKey}
+                              definition={provisioningTokenDefinition}
+                              mandatory
+                              onFieldChange={(_, value) =>
+                                setChangesWithCheck({
+                                  ...changes,
+                                  provisioningToken: value as string,
+                                })
+                              }
+                              isNotSet={
+                                configuration.synchronizationType !== ProvisioningType.auto &&
+                                configuration.provisioningGroups?.length === 0
+                              }
+                            />
+                            <AuthenticationFormField
+                              settingValue={groups}
+                              definition={provisioningGroupDefinition}
+                              mandatory
+                              onFieldChange={(_, values) =>
+                                setChangesWithCheck({
+                                  ...changes,
+                                  provisioningGroups: values as string[],
+                                })
+                              }
+                              isNotSet={configuration.synchronizationType !== ProvisioningType.auto}
+                            />
+                          </>
+                        )}
                       </>
                     ) : (
                       <p>
index 0aa4379247c1466d79705af39ef3e5af85f3334c..42e87d9f865bc9c4b7b3442d73ae75e60a6433db 100644 (file)
@@ -29,7 +29,7 @@ import { translate } from '../../../../helpers/l10n';
 import {
   useCreateGitLabConfigurationMutation,
   useUpdateGitLabConfigurationMutation,
-} from '../../../../queries/identity-provider';
+} from '../../../../queries/identity-provider/gitlab';
 import { GitLabConfigurationCreateBody, GitlabConfiguration } from '../../../../types/provisioning';
 import { DefinitionV2, SettingType } from '../../../../types/settings';
 import { DOCUMENTATION_LINK_SUFFIXES } from './Authentication';
@@ -64,13 +64,13 @@ export default function GitLabConfigurationForm(props: Readonly<Props>) {
     Record<keyof GitLabConfigurationCreateBody, FormData>
   >({
     applicationId: {
-      value: '',
+      value: data?.applicationId ?? '',
       required: true,
       definition: {
         name: translate('settings.authentication.gitlab.form.applicationId.name'),
         key: 'applicationId',
         description: translate('settings.authentication.gitlab.form.applicationId.description'),
-        secured: true,
+        secured: false,
       },
     },
     url: {
@@ -83,26 +83,24 @@ export default function GitLabConfigurationForm(props: Readonly<Props>) {
         description: translate('settings.authentication.gitlab.form.url.description'),
       },
     },
-    clientSecret: {
+    secret: {
       value: '',
       required: true,
       definition: {
-        name: translate('settings.authentication.gitlab.form.clientSecret.name'),
+        name: translate('settings.authentication.gitlab.form.secret.name'),
         secured: true,
-        key: 'clientSecret',
-        description: translate('settings.authentication.gitlab.form.clientSecret.description'),
+        key: 'secret',
+        description: translate('settings.authentication.gitlab.form.secret.description'),
       },
     },
-    synchronizeUserGroups: {
-      value: data?.synchronizeUserGroups ?? false,
+    synchronizeGroups: {
+      value: data?.synchronizeGroups ?? false,
       required: false,
       definition: {
-        name: translate('settings.authentication.gitlab.form.synchronizeUserGroups.name'),
+        name: translate('settings.authentication.gitlab.form.synchronizeGroups.name'),
         secured: false,
-        key: 'synchronizeUserGroups',
-        description: translate(
-          'settings.authentication.gitlab.form.synchronizeUserGroups.description',
-        ),
+        key: 'synchronizeGroups',
+        description: translate('settings.authentication.gitlab.form.synchronizeGroups.description'),
         type: SettingType.BOOLEAN,
       },
     },
index 4c2e271de2625381456af804424b61810b6fc1ba..20ab41da045a29cc06b738414e07219df35faf90 100644 (file)
@@ -28,12 +28,13 @@ import { Button, ResetButtonLink, SubmitButton } from '../../../../components/co
 import DeleteIcon from '../../../../components/icons/DeleteIcon';
 import EditIcon from '../../../../components/icons/EditIcon';
 import { Alert } from '../../../../components/ui/Alert';
+import Spinner from '../../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../../helpers/l10n';
+import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
 import {
   useCheckGitHubConfigQuery,
-  useIdentityProviderQuery,
   useSyncWithGitHubNow,
-} from '../../../../queries/identity-provider';
+} from '../../../../queries/identity-provider/github';
 import { AlmKeys } from '../../../../types/alm-settings';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Provider } from '../../../../types/types';
@@ -112,337 +113,343 @@ export default function GithubAuthenticationTab(props: GithubAuthenticationProps
   };
 
   return (
-    <div className="authentication-configuration">
-      <div className="spacer-bottom display-flex-space-between display-flex-center">
-        <h4>{translate('settings.authentication.github.configuration')}</h4>
+    <Spinner loading={isLoading}>
+      <div className="authentication-configuration">
+        <div className="spacer-bottom display-flex-space-between display-flex-center">
+          <h4>{translate('settings.authentication.github.configuration')}</h4>
 
-        {!hasConfiguration && (
-          <div>
-            <Button onClick={handleCreateConfiguration}>
-              {translate('settings.authentication.form.create')}
-            </Button>
+          {!hasConfiguration && (
+            <div>
+              <Button onClick={handleCreateConfiguration}>
+                {translate('settings.authentication.form.create')}
+              </Button>
+            </div>
+          )}
+        </div>
+        {enabled && !hasLegacyConfiguration && (
+          <GitHubConfigurationValidity
+            selectedOrganizations={
+              (values['sonar.auth.github.organizations']?.value as string[]) ?? []
+            }
+            isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+          />
+        )}
+        {!hasConfiguration && !hasLegacyConfiguration && (
+          <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
+            {translate('settings.authentication.github.form.not_configured')}
           </div>
         )}
-      </div>
-      {enabled && !hasLegacyConfiguration && (
-        <GitHubConfigurationValidity
-          selectedOrganizations={
-            (values['sonar.auth.github.organizations']?.value as string[]) ?? []
-          }
-          isAutoProvisioning={!!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
-        />
-      )}
-      {!hasConfiguration && !hasLegacyConfiguration && (
-        <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
-          {translate('settings.authentication.github.form.not_configured')}
-        </div>
-      )}
-      {!hasConfiguration && hasLegacyConfiguration && (
-        <div className="big-padded">
-          <Alert variant="warning">
-            <FormattedMessage
-              id="settings.authentication.github.form.legacy_configured"
-              defaultMessage={translate('settings.authentication.github.form.legacy_configured')}
-              values={{
-                documentation: (
-                  <DocLink to="/instance-administration/authentication/github">
-                    {translate('settings.authentication.github.form.legacy_configured.link')}
-                  </DocLink>
-                ),
-              }}
-            />
-          </Alert>
-        </div>
-      )}
-      {hasConfiguration && (
-        <>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <div>
-              <h5>{translateWithParameters('settings.authentication.github.appid_x', appId)}</h5>
-              <p>{url}</p>
-              <Tooltip
-                overlay={
-                  githubProvisioningStatus
-                    ? translate('settings.authentication.form.disable.tooltip')
-                    : null
-                }
-              >
-                <Button
-                  className="spacer-top"
-                  onClick={toggleEnable}
-                  disabled={githubProvisioningStatus}
+        {!hasConfiguration && hasLegacyConfiguration && (
+          <div className="big-padded">
+            <Alert variant="warning">
+              <FormattedMessage
+                id="settings.authentication.github.form.legacy_configured"
+                defaultMessage={translate('settings.authentication.github.form.legacy_configured')}
+                values={{
+                  documentation: (
+                    <DocLink to="/instance-administration/authentication/github">
+                      {translate('settings.authentication.github.form.legacy_configured.link')}
+                    </DocLink>
+                  ),
+                }}
+              />
+            </Alert>
+          </div>
+        )}
+        {hasConfiguration && (
+          <>
+            <div className="spacer-bottom big-padded bordered display-flex-space-between">
+              <div>
+                <h5>{translateWithParameters('settings.authentication.github.appid_x', appId)}</h5>
+                <p>{url}</p>
+                <Tooltip
+                  overlay={
+                    githubProvisioningStatus
+                      ? translate('settings.authentication.form.disable.tooltip')
+                      : null
+                  }
                 >
-                  {enabled
-                    ? translate('settings.authentication.form.disable')
-                    : translate('settings.authentication.form.enable')}
+                  <Button
+                    className="spacer-top"
+                    onClick={toggleEnable}
+                    disabled={githubProvisioningStatus}
+                  >
+                    {enabled
+                      ? translate('settings.authentication.form.disable')
+                      : translate('settings.authentication.form.enable')}
+                  </Button>
+                </Tooltip>
+              </div>
+              <div>
+                <Button className="spacer-right" onClick={handleCreateConfiguration}>
+                  <EditIcon />
+                  {translate('settings.authentication.form.edit')}
                 </Button>
-              </Tooltip>
-            </div>
-            <div>
-              <Button className="spacer-right" onClick={handleCreateConfiguration}>
-                <EditIcon />
-                {translate('settings.authentication.form.edit')}
-              </Button>
-              <Tooltip
-                overlay={
-                  enabled || isDeleting
-                    ? translate('settings.authentication.form.delete.tooltip')
-                    : null
-                }
-              >
-                <Button
-                  className="button-red"
-                  disabled={enabled || isDeleting}
-                  onClick={deleteConfiguration}
+                <Tooltip
+                  overlay={
+                    enabled || isDeleting
+                      ? translate('settings.authentication.form.delete.tooltip')
+                      : null
+                  }
                 >
-                  <DeleteIcon />
-                  {translate('settings.authentication.form.delete')}
-                </Button>
-              </Tooltip>
+                  <Button
+                    className="button-red"
+                    disabled={enabled || isDeleting}
+                    onClick={deleteConfiguration}
+                  >
+                    <DeleteIcon />
+                    {translate('settings.authentication.form.delete')}
+                  </Button>
+                </Tooltip>
+              </div>
             </div>
-          </div>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <form onSubmit={handleSubmit}>
-              <fieldset className="display-flex-column big-spacer-bottom">
-                <label className="h5">
-                  {translate('settings.authentication.form.provisioning')}
-                </label>
+            <div className="spacer-bottom big-padded bordered display-flex-space-between">
+              <form onSubmit={handleSubmit}>
+                <fieldset className="display-flex-column big-spacer-bottom">
+                  <label className="h5">
+                    {translate('settings.authentication.form.provisioning')}
+                  </label>
 
-                {enabled ? (
-                  <div className="display-flex-column spacer-top">
-                    <RadioCard
-                      className="sw-min-h-0"
-                      label={translate('settings.authentication.form.provisioning_at_login')}
-                      title={translate('settings.authentication.form.provisioning_at_login')}
-                      selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
-                      onClick={() => setProvisioningType(false)}
-                    >
-                      <p className="spacer-bottom">
-                        <FormattedMessage id="settings.authentication.github.form.provisioning_at_login.description" />
-                      </p>
-                      <p className="spacer-bottom">
-                        <FormattedMessage
-                          id="settings.authentication.github.form.description.doc"
-                          values={{
-                            documentation: (
-                              <DocLink
-                                to={`/instance-administration/authentication/${
-                                  DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
-                                }/`}
-                              >
-                                {translate('documentation')}
-                              </DocLink>
-                            ),
-                          }}
-                        />
-                      </p>
+                  {enabled ? (
+                    <div className="display-flex-column spacer-top">
+                      <RadioCard
+                        className="sw-min-h-0"
+                        label={translate('settings.authentication.form.provisioning_at_login')}
+                        title={translate('settings.authentication.form.provisioning_at_login')}
+                        selected={!(newGithubProvisioningStatus ?? githubProvisioningStatus)}
+                        onClick={() => setProvisioningType(false)}
+                      >
+                        <p className="spacer-bottom">
+                          <FormattedMessage id="settings.authentication.github.form.provisioning_at_login.description" />
+                        </p>
+                        <p className="spacer-bottom">
+                          <FormattedMessage
+                            id="settings.authentication.github.form.description.doc"
+                            values={{
+                              documentation: (
+                                <DocLink
+                                  to={`/instance-administration/authentication/${
+                                    DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+                                  }/`}
+                                >
+                                  {translate('documentation')}
+                                </DocLink>
+                              ),
+                            }}
+                          />
+                        </p>
 
-                      {!(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
-                        <>
-                          <hr />
-                          {Object.values(values).map((val) => {
-                            if (!GITHUB_JIT_FIELDS.includes(val.key)) {
-                              return null;
-                            }
-                            return (
-                              <div key={val.key}>
-                                <AuthenticationFormField
-                                  settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
-                                  definition={val.definition}
-                                  mandatory={val.mandatory}
-                                  onFieldChange={setNewValue}
-                                  isNotSet={val.isNotSet}
-                                />
-                              </div>
-                            );
-                          })}
-                        </>
-                      )}
-                    </RadioCard>
-                    <RadioCard
-                      className="spacer-top sw-min-h-0"
-                      label={translate(
-                        'settings.authentication.github.form.provisioning_with_github',
-                      )}
-                      title={translate(
-                        'settings.authentication.github.form.provisioning_with_github',
-                      )}
-                      selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
-                      onClick={() => setProvisioningType(true)}
-                      disabled={!hasGithubProvisioning || hasDifferentProvider}
-                    >
-                      {hasGithubProvisioning ? (
-                        <>
-                          {hasDifferentProvider && (
-                            <p className="spacer-bottom text-bold ">
-                              {translate('settings.authentication.form.other_provisioning_enabled')}
+                        {!(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
+                          <>
+                            <hr />
+                            {Object.values(values).map((val) => {
+                              if (!GITHUB_JIT_FIELDS.includes(val.key)) {
+                                return null;
+                              }
+                              return (
+                                <div key={val.key}>
+                                  <AuthenticationFormField
+                                    settingValue={
+                                      values[val.key]?.newValue ?? values[val.key]?.value
+                                    }
+                                    definition={val.definition}
+                                    mandatory={val.mandatory}
+                                    onFieldChange={setNewValue}
+                                    isNotSet={val.isNotSet}
+                                  />
+                                </div>
+                              );
+                            })}
+                          </>
+                        )}
+                      </RadioCard>
+                      <RadioCard
+                        className="spacer-top sw-min-h-0"
+                        label={translate(
+                          'settings.authentication.github.form.provisioning_with_github',
+                        )}
+                        title={translate(
+                          'settings.authentication.github.form.provisioning_with_github',
+                        )}
+                        selected={newGithubProvisioningStatus ?? githubProvisioningStatus}
+                        onClick={() => setProvisioningType(true)}
+                        disabled={!hasGithubProvisioning || hasDifferentProvider}
+                      >
+                        {hasGithubProvisioning ? (
+                          <>
+                            {hasDifferentProvider && (
+                              <p className="spacer-bottom text-bold ">
+                                {translate(
+                                  'settings.authentication.form.other_provisioning_enabled',
+                                )}
+                              </p>
+                            )}
+                            <p className="spacer-bottom">
+                              {translate(
+                                'settings.authentication.github.form.provisioning_with_github.description',
+                              )}
+                            </p>
+                            <p className="spacer-bottom">
+                              <FormattedMessage
+                                id="settings.authentication.github.form.description.doc"
+                                values={{
+                                  documentation: (
+                                    <DocLink
+                                      to={`/instance-administration/authentication/${
+                                        DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
+                                      }/`}
+                                    >
+                                      {translate('documentation')}
+                                    </DocLink>
+                                  ),
+                                }}
+                              />
                             </p>
-                          )}
-                          <p className="spacer-bottom">
-                            {translate(
-                              'settings.authentication.github.form.provisioning_with_github.description',
+
+                            {githubProvisioningStatus && <GitHubSynchronisationWarning />}
+                            {(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
+                              <>
+                                <div className="sw-flex sw-flex-1 spacer-bottom">
+                                  <Button
+                                    className="spacer-top width-30"
+                                    onClick={synchronizeNow}
+                                    disabled={!canSyncNow}
+                                  >
+                                    {translate('settings.authentication.github.synchronize_now')}
+                                  </Button>
+                                </div>
+                                <hr />
+                                {Object.values(values).map((val) => {
+                                  if (!GITHUB_PROVISIONING_FIELDS.includes(val.key)) {
+                                    return null;
+                                  }
+                                  return (
+                                    <div key={val.key}>
+                                      <AuthenticationFormField
+                                        settingValue={
+                                          values[val.key]?.newValue ?? values[val.key]?.value
+                                        }
+                                        definition={val.definition}
+                                        mandatory={val.mandatory}
+                                        onFieldChange={setNewValue}
+                                        isNotSet={val.isNotSet}
+                                      />
+                                    </div>
+                                  );
+                                })}
+                                <AuthenticationFormFieldWrapper
+                                  title={translate(
+                                    'settings.authentication.github.configuration.roles_mapping.title',
+                                  )}
+                                  description={translate(
+                                    'settings.authentication.github.configuration.roles_mapping.description',
+                                  )}
+                                >
+                                  <Button
+                                    className="spacer-top"
+                                    onClick={() => setShowMappingModal(true)}
+                                  >
+                                    {translate(
+                                      'settings.authentication.github.configuration.roles_mapping.button_label',
+                                    )}
+                                  </Button>
+                                </AuthenticationFormFieldWrapper>
+                              </>
                             )}
-                          </p>
-                          <p className="spacer-bottom">
+                          </>
+                        ) : (
+                          <p>
                             <FormattedMessage
-                              id="settings.authentication.github.form.description.doc"
+                              id="settings.authentication.github.form.provisioning.disabled"
+                              defaultMessage={translate(
+                                'settings.authentication.github.form.provisioning.disabled',
+                              )}
                               values={{
                                 documentation: (
-                                  <DocLink
-                                    to={`/instance-administration/authentication/${
-                                      DOCUMENTATION_LINK_SUFFIXES[AlmKeys.GitHub]
-                                    }/`}
-                                  >
+                                  // Documentation page not ready yet.
+                                  <DocLink to="/instance-administration/authentication/github">
                                     {translate('documentation')}
                                   </DocLink>
                                 ),
                               }}
                             />
                           </p>
-
-                          {githubProvisioningStatus && <GitHubSynchronisationWarning />}
-                          {(newGithubProvisioningStatus ?? githubProvisioningStatus) && (
-                            <>
-                              <div className="sw-flex sw-flex-1 spacer-bottom">
-                                <Button
-                                  className="spacer-top width-30"
-                                  onClick={synchronizeNow}
-                                  disabled={!canSyncNow}
-                                >
-                                  {translate('settings.authentication.github.synchronize_now')}
-                                </Button>
-                              </div>
-                              <hr />
-                              {Object.values(values).map((val) => {
-                                if (!GITHUB_PROVISIONING_FIELDS.includes(val.key)) {
-                                  return null;
-                                }
-                                return (
-                                  <div key={val.key}>
-                                    <AuthenticationFormField
-                                      settingValue={
-                                        values[val.key]?.newValue ?? values[val.key]?.value
-                                      }
-                                      definition={val.definition}
-                                      mandatory={val.mandatory}
-                                      onFieldChange={setNewValue}
-                                      isNotSet={val.isNotSet}
-                                    />
-                                  </div>
-                                );
-                              })}
-                              <AuthenticationFormFieldWrapper
-                                title={translate(
-                                  'settings.authentication.github.configuration.roles_mapping.title',
-                                )}
-                                description={translate(
-                                  'settings.authentication.github.configuration.roles_mapping.description',
-                                )}
-                              >
-                                <Button
-                                  className="spacer-top"
-                                  onClick={() => setShowMappingModal(true)}
-                                >
-                                  {translate(
-                                    'settings.authentication.github.configuration.roles_mapping.button_label',
-                                  )}
-                                </Button>
-                              </AuthenticationFormFieldWrapper>
-                            </>
-                          )}
-                        </>
-                      ) : (
-                        <p>
-                          <FormattedMessage
-                            id="settings.authentication.github.form.provisioning.disabled"
-                            defaultMessage={translate(
-                              'settings.authentication.github.form.provisioning.disabled',
-                            )}
-                            values={{
-                              documentation: (
-                                // Documentation page not ready yet.
-                                <DocLink to="/instance-administration/authentication/github">
-                                  {translate('documentation')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
-                        </p>
-                      )}
-                    </RadioCard>
+                        )}
+                      </RadioCard>
+                    </div>
+                  ) : (
+                    <Alert className="big-spacer-top" variant="info">
+                      {translate('settings.authentication.github.enable_first')}
+                    </Alert>
+                  )}
+                </fieldset>
+                {enabled && (
+                  <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
+                    <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
+                      {translate('save')}
+                    </SubmitButton>
+                    <ResetButtonLink
+                      onClick={() => {
+                        setProvisioningType(undefined);
+                        resetJitSetting();
+                      }}
+                      disabled={!hasGithubProvisioningConfigChange}
+                    >
+                      {translate('cancel')}
+                    </ResetButtonLink>
+                    <Alert variant="warning" className="sw-mb-0">
+                      {hasGithubProvisioningConfigChange &&
+                        translate('settings.authentication.github.configuration.unsaved_changes')}
+                    </Alert>
                   </div>
-                ) : (
-                  <Alert className="big-spacer-top" variant="info">
-                    {translate('settings.authentication.github.enable_first')}
-                  </Alert>
                 )}
-              </fieldset>
-              {enabled && (
-                <div className="sw-flex sw-gap-2 sw-h-8 sw-items-center">
-                  <SubmitButton disabled={!hasGithubProvisioningConfigChange}>
-                    {translate('save')}
-                  </SubmitButton>
-                  <ResetButtonLink
-                    onClick={() => {
-                      setProvisioningType(undefined);
-                      resetJitSetting();
-                    }}
-                    disabled={!hasGithubProvisioningConfigChange}
+                {showConfirmProvisioningModal && (
+                  <ConfirmModal
+                    onConfirm={() => changeProvisioning()}
+                    header={translate(
+                      'settings.authentication.github.confirm',
+                      newGithubProvisioningStatus ? 'auto' : 'jit',
+                    )}
+                    onClose={() => setShowConfirmProvisioningModal(false)}
+                    confirmButtonText={translate(
+                      'settings.authentication.github.provisioning_change.confirm_changes',
+                    )}
                   >
-                    {translate('cancel')}
-                  </ResetButtonLink>
-                  <Alert variant="warning" className="sw-mb-0">
-                    {hasGithubProvisioningConfigChange &&
-                      translate('settings.authentication.github.configuration.unsaved_changes')}
-                  </Alert>
-                </div>
-              )}
-              {showConfirmProvisioningModal && (
-                <ConfirmModal
-                  onConfirm={() => changeProvisioning()}
-                  header={translate(
-                    'settings.authentication.github.confirm',
-                    newGithubProvisioningStatus ? 'auto' : 'jit',
-                  )}
-                  onClose={() => setShowConfirmProvisioningModal(false)}
-                  confirmButtonText={translate(
-                    'settings.authentication.github.provisioning_change.confirm_changes',
-                  )}
-                >
-                  {translate(
-                    'settings.authentication.github.confirm',
-                    newGithubProvisioningStatus ? 'auto' : 'jit',
-                    'description',
-                  )}
-                </ConfirmModal>
+                    {translate(
+                      'settings.authentication.github.confirm',
+                      newGithubProvisioningStatus ? 'auto' : 'jit',
+                      'description',
+                    )}
+                  </ConfirmModal>
+                )}
+              </form>
+              {showMappingModal && (
+                <GitHubMappingModal
+                  mapping={rolesMapping}
+                  setMapping={setRolesMapping}
+                  onClose={() => setShowMappingModal(false)}
+                />
               )}
-            </form>
-            {showMappingModal && (
-              <GitHubMappingModal
-                mapping={rolesMapping}
-                setMapping={setRolesMapping}
-                onClose={() => setShowMappingModal(false)}
-              />
-            )}
-          </div>
-        </>
-      )}
+            </div>
+          </>
+        )}
 
-      {showEditModal && (
-        <ConfigurationForm
-          tab={AlmKeys.GitHub}
-          excludedField={GITHUB_EXCLUDED_FIELD}
-          loading={isLoading}
-          values={values}
-          setNewValue={setNewValue}
-          canBeSave={canBeSave}
-          onClose={handleCloseConfiguration}
-          create={!hasConfiguration}
-          hasLegacyConfiguration={hasLegacyConfiguration}
-        />
-      )}
+        {showEditModal && (
+          <ConfigurationForm
+            tab={AlmKeys.GitHub}
+            excludedField={GITHUB_EXCLUDED_FIELD}
+            loading={isLoading}
+            values={values}
+            setNewValue={setNewValue}
+            canBeSave={canBeSave}
+            onClose={handleCloseConfiguration}
+            create={!hasConfiguration}
+            hasLegacyConfiguration={hasLegacyConfiguration}
+          />
+        )}
 
-      {currentTab === AlmKeys.GitHub && <AutoProvisioningConsent />}
-    </div>
+        {currentTab === AlmKeys.GitHub && <AutoProvisioningConsent />}
+      </div>
+    </Spinner>
   );
 }
index a191a859e525ffb26ef0e0402d303144069efd19..ab7721b6137b10da3121b949606c9f6eeb20fcef 100644 (file)
@@ -28,11 +28,10 @@ import CheckIcon from '../../../../components/icons/CheckIcon';
 import DeleteIcon from '../../../../components/icons/DeleteIcon';
 import EditIcon from '../../../../components/icons/EditIcon';
 import { Alert } from '../../../../components/ui/Alert';
+import Spinner from '../../../../components/ui/Spinner';
 import { translate } from '../../../../helpers/l10n';
-import {
-  useIdentityProviderQuery,
-  useToggleScimMutation,
-} from '../../../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../../../queries/identity-provider/common';
+import { useToggleScimMutation } from '../../../../queries/identity-provider/scim';
 import { useSaveValueMutation } from '../../../../queries/settings';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { Provider } from '../../../../types/types';
@@ -108,136 +107,109 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
   };
 
   return (
-    <div className="authentication-configuration">
-      <div className="spacer-bottom display-flex-space-between display-flex-center">
-        <h4>{translate('settings.authentication.saml.configuration')}</h4>
+    <Spinner loading={isLoading}>
+      <div className="authentication-configuration">
+        <div className="spacer-bottom display-flex-space-between display-flex-center">
+          <h4>{translate('settings.authentication.saml.configuration')}</h4>
 
+          {!hasConfiguration && (
+            <div>
+              <Button onClick={handleCreateConfiguration}>
+                {translate('settings.authentication.form.create')}
+              </Button>
+            </div>
+          )}
+        </div>
         {!hasConfiguration && (
-          <div>
-            <Button onClick={handleCreateConfiguration}>
-              {translate('settings.authentication.form.create')}
-            </Button>
+          <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
+            {translate('settings.authentication.saml.form.not_configured')}
           </div>
         )}
-      </div>
-      {!hasConfiguration && (
-        <div className="big-padded text-center huge-spacer-bottom authentication-no-config">
-          {translate('settings.authentication.saml.form.not_configured')}
-        </div>
-      )}
 
-      {hasConfiguration && (
-        <>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <div>
-              <h5>{name}</h5>
-              <p>{url}</p>
-              <p className="big-spacer-top big-spacer-bottom">
-                {samlEnabled ? (
-                  <span className="authentication-enabled spacer-left">
-                    <CheckIcon className="spacer-right" />
-                    {translate('settings.authentication.form.enabled')}
-                  </span>
-                ) : (
-                  translate('settings.authentication.form.not_enabled')
-                )}
-              </p>
-              <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
-                {samlEnabled
-                  ? translate('settings.authentication.form.disable')
-                  : translate('settings.authentication.form.enable')}
-              </Button>
+        {hasConfiguration && (
+          <>
+            <div className="spacer-bottom big-padded bordered display-flex-space-between">
+              <div>
+                <h5>{name}</h5>
+                <p>{url}</p>
+                <p className="big-spacer-top big-spacer-bottom">
+                  {samlEnabled ? (
+                    <span className="authentication-enabled spacer-left">
+                      <CheckIcon className="spacer-right" />
+                      {translate('settings.authentication.form.enabled')}
+                    </span>
+                  ) : (
+                    translate('settings.authentication.form.not_enabled')
+                  )}
+                </p>
+                <Button className="spacer-top" disabled={scimStatus} onClick={handleToggleEnable}>
+                  {samlEnabled
+                    ? translate('settings.authentication.form.disable')
+                    : translate('settings.authentication.form.enable')}
+                </Button>
+              </div>
+              <div>
+                <Link className="button spacer-right" target="_blank" to={CONFIG_TEST_PATH}>
+                  {translate('settings.authentication.saml.form.test')}
+                </Link>
+                <Button className="spacer-right" onClick={handleCreateConfiguration}>
+                  <EditIcon />
+                  {translate('settings.authentication.form.edit')}
+                </Button>
+                <Button
+                  className="button-red"
+                  disabled={samlEnabled || isDeleting}
+                  onClick={deleteConfiguration}
+                >
+                  <DeleteIcon />
+                  {translate('settings.authentication.form.delete')}
+                </Button>
+              </div>
             </div>
-            <div>
-              <Link className="button spacer-right" target="_blank" to={CONFIG_TEST_PATH}>
-                {translate('settings.authentication.saml.form.test')}
-              </Link>
-              <Button className="spacer-right" onClick={handleCreateConfiguration}>
-                <EditIcon />
-                {translate('settings.authentication.form.edit')}
-              </Button>
-              <Button
-                className="button-red"
-                disabled={samlEnabled || isDeleting}
-                onClick={deleteConfiguration}
+            <div className="spacer-bottom big-padded bordered display-flex-space-between">
+              <form
+                onSubmit={(e) => {
+                  e.preventDefault();
+                  if (hasScimTypeChange) {
+                    setShowConfirmProvisioningModal(true);
+                  } else {
+                    handleSaveGroup();
+                  }
+                }}
               >
-                <DeleteIcon />
-                {translate('settings.authentication.form.delete')}
-              </Button>
-            </div>
-          </div>
-          <div className="spacer-bottom big-padded bordered display-flex-space-between">
-            <form
-              onSubmit={(e) => {
-                e.preventDefault();
-                if (hasScimTypeChange) {
-                  setShowConfirmProvisioningModal(true);
-                } else {
-                  handleSaveGroup();
-                }
-              }}
-            >
-              <fieldset className="display-flex-column big-spacer-bottom">
-                <label className="h5">
-                  {translate('settings.authentication.form.provisioning')}
-                </label>
-                {samlEnabled ? (
-                  <div className="display-flex-column spacer-top">
-                    <RadioCard
-                      className="sw-min-h-0"
-                      label={translate('settings.authentication.saml.form.provisioning_at_login')}
-                      title={translate('settings.authentication.saml.form.provisioning_at_login')}
-                      selected={!(newScimStatus ?? scimStatus)}
-                      onClick={() => setNewScimStatus(false)}
-                    >
-                      <p>
-                        {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
-                      </p>
-                    </RadioCard>
-                    <RadioCard
-                      className="spacer-top sw-min-h-0"
-                      label={translate('settings.authentication.saml.form.provisioning_with_scim')}
-                      title={translate('settings.authentication.saml.form.provisioning_with_scim')}
-                      selected={newScimStatus ?? scimStatus}
-                      onClick={() => setNewScimStatus(true)}
-                      disabled={!hasScim || hasDifferentProvider}
-                    >
-                      {!hasScim ? (
+                <fieldset className="display-flex-column big-spacer-bottom">
+                  <label className="h5">
+                    {translate('settings.authentication.form.provisioning')}
+                  </label>
+                  {samlEnabled ? (
+                    <div className="display-flex-column spacer-top">
+                      <RadioCard
+                        className="sw-min-h-0"
+                        label={translate('settings.authentication.saml.form.provisioning_at_login')}
+                        title={translate('settings.authentication.saml.form.provisioning_at_login')}
+                        selected={!(newScimStatus ?? scimStatus)}
+                        onClick={() => setNewScimStatus(false)}
+                      >
                         <p>
-                          <FormattedMessage
-                            id="settings.authentication.saml.form.provisioning.disabled"
-                            values={{
-                              documentation: (
-                                <DocLink to="/instance-administration/authentication/saml/scim/overview">
-                                  {translate('documentation')}
-                                </DocLink>
-                              ),
-                            }}
-                          />
+                          {translate('settings.authentication.saml.form.provisioning_at_login.sub')}
                         </p>
-                      ) : (
-                        <>
-                          {hasDifferentProvider && (
-                            <p className="spacer-bottom text-bold">
-                              {translate('settings.authentication.form.other_provisioning_enabled')}
-                            </p>
-                          )}
-                          <p className="spacer-bottom ">
-                            {translate(
-                              'settings.authentication.saml.form.provisioning_with_scim.sub',
-                            )}
-                          </p>
-                          <p className="spacer-bottom ">
-                            {translate(
-                              'settings.authentication.saml.form.provisioning_with_scim.description',
-                            )}
-                          </p>
+                      </RadioCard>
+                      <RadioCard
+                        className="spacer-top sw-min-h-0"
+                        label={translate(
+                          'settings.authentication.saml.form.provisioning_with_scim',
+                        )}
+                        title={translate(
+                          'settings.authentication.saml.form.provisioning_with_scim',
+                        )}
+                        selected={newScimStatus ?? scimStatus}
+                        onClick={() => setNewScimStatus(true)}
+                        disabled={!hasScim || hasDifferentProvider}
+                      >
+                        {!hasScim ? (
                           <p>
                             <FormattedMessage
-                              id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
-                              defaultMessage={translate(
-                                'settings.authentication.saml.form.provisioning_with_scim.description.doc',
-                              )}
+                              id="settings.authentication.saml.form.provisioning.disabled"
                               values={{
                                 documentation: (
                                   <DocLink to="/instance-administration/authentication/saml/scim/overview">
@@ -247,65 +219,100 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) {
                               }}
                             />
                           </p>
-                        </>
-                      )}
-                    </RadioCard>
-                  </div>
-                ) : (
-                  <Alert className="big-spacer-top" variant="info">
-                    {translate('settings.authentication.saml.enable_first')}
-                  </Alert>
+                        ) : (
+                          <>
+                            {hasDifferentProvider && (
+                              <p className="spacer-bottom text-bold">
+                                {translate(
+                                  'settings.authentication.form.other_provisioning_enabled',
+                                )}
+                              </p>
+                            )}
+                            <p className="spacer-bottom ">
+                              {translate(
+                                'settings.authentication.saml.form.provisioning_with_scim.sub',
+                              )}
+                            </p>
+                            <p className="spacer-bottom ">
+                              {translate(
+                                'settings.authentication.saml.form.provisioning_with_scim.description',
+                              )}
+                            </p>
+                            <p>
+                              <FormattedMessage
+                                id="settings.authentication.saml.form.provisioning_with_scim.description.doc"
+                                defaultMessage={translate(
+                                  'settings.authentication.saml.form.provisioning_with_scim.description.doc',
+                                )}
+                                values={{
+                                  documentation: (
+                                    <DocLink to="/instance-administration/authentication/saml/scim/overview">
+                                      {translate('documentation')}
+                                    </DocLink>
+                                  ),
+                                }}
+                              />
+                            </p>
+                          </>
+                        )}
+                      </RadioCard>
+                    </div>
+                  ) : (
+                    <Alert className="big-spacer-top" variant="info">
+                      {translate('settings.authentication.saml.enable_first')}
+                    </Alert>
+                  )}
+                </fieldset>
+                {samlEnabled && (
+                  <>
+                    <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
+                    <ResetButtonLink
+                      className="spacer-left"
+                      onClick={() => {
+                        setNewScimStatus(undefined);
+                        setNewGroupSetting();
+                      }}
+                      disabled={!hasScimConfigChange}
+                    >
+                      {translate('cancel')}
+                    </ResetButtonLink>
+                  </>
                 )}
-              </fieldset>
-              {samlEnabled && (
-                <>
-                  <SubmitButton disabled={!hasScimConfigChange}>{translate('save')}</SubmitButton>
-                  <ResetButtonLink
-                    className="spacer-left"
-                    onClick={() => {
-                      setNewScimStatus(undefined);
-                      setNewGroupSetting();
-                    }}
-                    disabled={!hasScimConfigChange}
+                {showConfirmProvisioningModal && (
+                  <ConfirmModal
+                    onConfirm={() => handleConfirmChangeProvisioning()}
+                    header={translate(
+                      'settings.authentication.saml.confirm',
+                      newScimStatus ? 'scim' : 'jit',
+                    )}
+                    onClose={() => setShowConfirmProvisioningModal(false)}
+                    isDestructive={!newScimStatus}
+                    confirmButtonText={translate('yes')}
                   >
-                    {translate('cancel')}
-                  </ResetButtonLink>
-                </>
-              )}
-              {showConfirmProvisioningModal && (
-                <ConfirmModal
-                  onConfirm={() => handleConfirmChangeProvisioning()}
-                  header={translate(
-                    'settings.authentication.saml.confirm',
-                    newScimStatus ? 'scim' : 'jit',
-                  )}
-                  onClose={() => setShowConfirmProvisioningModal(false)}
-                  isDestructive={!newScimStatus}
-                  confirmButtonText={translate('yes')}
-                >
-                  {translate(
-                    'settings.authentication.saml.confirm',
-                    newScimStatus ? 'scim' : 'jit',
-                    'description',
-                  )}
-                </ConfirmModal>
-              )}
-            </form>
-          </div>
-        </>
-      )}
-      {showEditModal && (
-        <ConfigurationForm
-          tab={SAML}
-          excludedField={SAML_EXCLUDED_FIELD}
-          loading={isLoading}
-          values={values}
-          setNewValue={setNewValue}
-          canBeSave={canBeSave}
-          onClose={handleCancelConfiguration}
-          create={!hasConfiguration}
-        />
-      )}
-    </div>
+                    {translate(
+                      'settings.authentication.saml.confirm',
+                      newScimStatus ? 'scim' : 'jit',
+                      'description',
+                    )}
+                  </ConfirmModal>
+                )}
+              </form>
+            </div>
+          </>
+        )}
+        {showEditModal && (
+          <ConfigurationForm
+            tab={SAML}
+            excludedField={SAML_EXCLUDED_FIELD}
+            loading={isLoading}
+            values={values}
+            setNewValue={setNewValue}
+            canBeSave={canBeSave}
+            onClose={handleCancelConfiguration}
+            create={!hasConfiguration}
+          />
+        )}
+      </div>
+    </Spinner>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Github-it.tsx
new file mode 100644 (file)
index 0000000..f6e4ab0
--- /dev/null
@@ -0,0 +1,916 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import React from 'react';
+import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
+import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
+import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { Feature } from '../../../../../types/features';
+import { GitHubProvisioningStatus } from '../../../../../types/provisioning';
+import { TaskStatuses } from '../../../../../types/tasks';
+import Authentication from '../Authentication';
+
+let handler: GithubProvisioningServiceMock;
+let system: SystemServiceMock;
+let settingsHandler: SettingsServiceMock;
+let computeEngineHandler: ComputeEngineServiceMock;
+
+beforeEach(() => {
+  handler = new GithubProvisioningServiceMock();
+  system = new SystemServiceMock();
+  settingsHandler = new SettingsServiceMock();
+  computeEngineHandler = new ComputeEngineServiceMock();
+});
+
+afterEach(() => {
+  handler.reset();
+  settingsHandler.reset();
+  system.reset();
+  computeEngineHandler.reset();
+});
+
+const ghContainer = byRole('tabpanel', { name: 'github GitHub' });
+
+const ui = {
+  saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }),
+  customMessageInformation: byText('settings.authentication.custom_message_information'),
+  enabledToggle: byRole('switch'),
+  testButton: byText('settings.authentication.saml.form.test'),
+  textbox1: byRole('textbox', { name: 'test1' }),
+  textbox2: byRole('textbox', { name: 'test2' }),
+  tab: byRole('tab', { name: 'github GitHub' }),
+  noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
+  createConfigButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.form.create',
+  }),
+  clientId: byRole('textbox', {
+    name: 'property.sonar.auth.github.clientId.secured.name',
+  }),
+  appId: byRole('textbox', { name: 'property.sonar.auth.github.appId.name' }),
+  privateKey: byRole('textbox', {
+    name: 'property.sonar.auth.github.privateKey.secured.name',
+  }),
+  clientSecret: byRole('textbox', {
+    name: 'property.sonar.auth.github.clientSecret.secured.name',
+  }),
+  githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }),
+  githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }),
+  allowUsersToSignUp: byRole('switch', {
+    name: 'sonar.auth.github.allowUsersToSignUp',
+  }),
+  organizations: byRole('textbox', {
+    name: 'property.sonar.auth.github.organizations.name',
+  }),
+  saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
+  confirmProvisioningButton: byRole('button', {
+    name: 'settings.authentication.github.provisioning_change.confirm_changes',
+  }),
+  saveGithubProvisioning: ghContainer.byRole('button', { name: 'save' }),
+  groupAttribute: byRole('textbox', {
+    name: 'property.sonar.auth.github.group.name.name',
+  }),
+  enableConfigButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.form.enable',
+  }),
+  disableConfigButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.form.disable',
+  }),
+  editConfigButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.form.edit',
+  }),
+  editMappingButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.github.configuration.roles_mapping.button_label',
+  }),
+  mappingRow: byRole('dialog', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+  }).byRole('row'),
+  customRoleInput: byRole('textbox', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role',
+  }),
+  customRoleAddBtn: byRole('dialog', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+  }).byRole('button', { name: 'add_verb' }),
+  roleExistsError: byRole('dialog', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+  }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'),
+  emptyRoleError: byRole('dialog', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+  }).byText('settings.authentication.github.configuration.roles_mapping.empty_custom_role'),
+  deleteCustomRoleCustom2: byRole('button', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2',
+  }),
+  getMappingRowByRole: (text: string) =>
+    ui.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
+  mappingCheckbox: byRole('checkbox'),
+  mappingDialogClose: byRole('dialog', {
+    name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
+  }).byRole('button', {
+    name: 'close',
+  }),
+  deleteOrg: (org: string) =>
+    byRole('button', {
+      name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
+    }),
+  enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
+  jitProvisioningButton: ghContainer.byRole('radio', {
+    name: 'settings.authentication.form.provisioning_at_login',
+  }),
+  githubProvisioningButton: ghContainer.byRole('radio', {
+    name: 'settings.authentication.github.form.provisioning_with_github',
+  }),
+  githubProvisioningPending: ghContainer.byText(/synchronization_pending/),
+  githubProvisioningInProgress: ghContainer.byText(/synchronization_in_progress/),
+  githubProvisioningSuccess: ghContainer.byText(/synchronization_successful/),
+  githubProvisioningAlert: ghContainer.byText(/synchronization_failed/),
+  configurationValidityLoading: ghContainer.byRole('status', {
+    name: /github.configuration.validation.loading/,
+  }),
+  configurationValiditySuccess: ghContainer.byRole('status', {
+    name: /github.configuration.validation.valid/,
+  }),
+  configurationValidityError: ghContainer.byRole('status', {
+    name: /github.configuration.validation.invalid/,
+  }),
+  syncWarning: ghContainer.byText(/Warning/),
+  syncSummary: ghContainer.byText(/Test summary/),
+  configurationValidityWarning: ghContainer.byRole('status', {
+    name: /github.configuration.validation.valid.short/,
+  }),
+  checkConfigButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.github.configuration.validation.test',
+  }),
+  viewConfigValidityDetailsButton: ghContainer.byRole('button', {
+    name: 'settings.authentication.github.configuration.validation.details',
+  }),
+  configDetailsDialog: byRole('dialog', {
+    name: 'settings.authentication.github.configuration.validation.details.title',
+  }),
+  continueAutoButton: byRole('button', {
+    name: 'settings.authentication.github.confirm_auto_provisioning.continue',
+  }),
+  switchJitButton: byRole('button', {
+    name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
+  }),
+  consentDialog: byRole('dialog', {
+    name: 'settings.authentication.github.confirm_auto_provisioning.header',
+  }),
+  getConfigDetailsTitle: () => within(ui.configDetailsDialog.get()).getByRole('heading'),
+  getOrgs: () => within(ui.configDetailsDialog.get()).getAllByRole('listitem'),
+  fillForm: async (user: UserEvent) => {
+    await user.type(await ui.clientId.find(), 'Awsome GITHUB config');
+    await user.type(ui.clientSecret.get(), 'Client shut');
+    await user.type(ui.appId.get(), 'App id');
+    await user.type(ui.privateKey.get(), 'Private Key');
+    await user.type(ui.githubApiUrl.get(), 'API Url');
+    await user.type(ui.githubWebUrl.get(), 'WEb Url');
+    await user.type(ui.organizations.get(), 'organization1');
+  },
+  createConfiguration: async (user: UserEvent) => {
+    await user.click(await ui.createConfigButton.find());
+    await ui.fillForm(user);
+
+    await user.click(ui.saveConfigButton.get());
+  },
+  enableConfiguration: async (user: UserEvent) => {
+    await user.click(await ui.tab.find());
+    await ui.createConfiguration(user);
+    await user.click(await ui.enableConfigButton.find());
+  },
+  enableProvisioning: async (user: UserEvent) => {
+    await user.click(await ui.tab.find());
+
+    await ui.createConfiguration(user);
+
+    await user.click(await ui.enableConfigButton.find());
+    await user.click(await ui.githubProvisioningButton.find());
+    await user.click(ui.saveGithubProvisioning.get());
+    await user.click(ui.confirmProvisioningButton.get());
+  },
+};
+
+describe('Github tab', () => {
+  it('should render an empty Github configuration', async () => {
+    renderAuthentication();
+    const user = userEvent.setup();
+    await user.click(await ui.tab.find());
+    expect(await ui.noGithubConfiguration.find()).toBeInTheDocument();
+  });
+
+  it('should be able to create a configuration', async () => {
+    const user = userEvent.setup();
+    renderAuthentication();
+
+    await user.click(await ui.tab.find());
+    await user.click(await ui.createConfigButton.find());
+
+    expect(ui.saveConfigButton.get()).toBeDisabled();
+
+    await ui.fillForm(user);
+    expect(ui.saveConfigButton.get()).toBeEnabled();
+
+    await user.click(ui.saveConfigButton.get());
+
+    expect(await ui.editConfigButton.find()).toBeInTheDocument();
+  });
+
+  it('should be able to edit configuration', async () => {
+    const user = userEvent.setup();
+    renderAuthentication();
+    await user.click(await ui.tab.find());
+
+    await ui.createConfiguration(user);
+
+    await user.click(ui.editConfigButton.get());
+    await user.click(ui.deleteOrg('organization1').get());
+
+    await user.click(ui.saveConfigButton.get());
+
+    await user.click(await ui.editConfigButton.find());
+
+    expect(ui.organizations.get()).toHaveValue('');
+  });
+
+  it('should be able to enable/disable configuration', async () => {
+    const user = userEvent.setup();
+    renderAuthentication();
+    await user.click(await ui.tab.find());
+
+    await ui.createConfiguration(user);
+
+    await user.click(await ui.enableConfigButton.find());
+
+    expect(await ui.disableConfigButton.find()).toBeInTheDocument();
+    await user.click(ui.disableConfigButton.get());
+    await waitFor(() => expect(ui.disableConfigButton.query()).not.toBeInTheDocument());
+
+    expect(await ui.enableConfigButton.find()).toBeInTheDocument();
+  });
+
+  it('should not allow edtion below Enterprise to select Github provisioning', async () => {
+    const user = userEvent.setup();
+
+    renderAuthentication();
+    await user.click(await ui.tab.find());
+
+    await ui.createConfiguration(user);
+    await user.click(await ui.enableConfigButton.find());
+
+    expect(await ui.jitProvisioningButton.find()).toBeChecked();
+    expect(ui.githubProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
+  });
+
+  it('should be able to choose provisioning', async () => {
+    const user = userEvent.setup();
+
+    renderAuthentication([Feature.GithubProvisioning]);
+    await user.click(await ui.tab.find());
+
+    await ui.createConfiguration(user);
+
+    expect(await ui.enableFirstMessage.find()).toBeInTheDocument();
+    await user.click(await ui.enableConfigButton.find());
+
+    expect(await ui.jitProvisioningButton.find()).toBeChecked();
+
+    expect(ui.saveGithubProvisioning.get()).toBeDisabled();
+    await user.click(ui.allowUsersToSignUp.get());
+
+    expect(ui.saveGithubProvisioning.get()).toBeEnabled();
+    await user.click(ui.saveGithubProvisioning.get());
+
+    await waitFor(() => expect(ui.saveGithubProvisioning.query()).toBeDisabled());
+
+    await user.click(ui.githubProvisioningButton.get());
+
+    expect(ui.saveGithubProvisioning.get()).toBeEnabled();
+    await user.click(ui.saveGithubProvisioning.get());
+    await user.click(ui.confirmProvisioningButton.get());
+
+    expect(await ui.githubProvisioningButton.find()).toBeChecked();
+    expect(ui.disableConfigButton.get()).toBeDisabled();
+    expect(ui.saveGithubProvisioning.get()).toBeDisabled();
+  });
+
+  describe('Github Provisioning', () => {
+    let user: UserEvent;
+
+    beforeEach(() => {
+      jest.useFakeTimers({
+        advanceTimers: true,
+        now: new Date('2022-02-04T12:00:59Z'),
+      });
+      user = userEvent.setup();
+    });
+
+    afterEach(() => {
+      jest.runOnlyPendingTimers();
+      jest.useRealTimers();
+    });
+
+    it('should display a success status when the synchronisation is a success', async () => {
+      handler.addProvisioningTask({
+        status: TaskStatuses.Success,
+        executedAt: '2022-02-03T11:45:35+0200',
+      });
+
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableProvisioning(user);
+      expect(ui.githubProvisioningSuccess.get()).toBeInTheDocument();
+      expect(ui.syncSummary.get()).toBeInTheDocument();
+    });
+
+    it('should display a success status even when another task is pending', async () => {
+      handler.addProvisioningTask({
+        status: TaskStatuses.Pending,
+        executedAt: '2022-02-03T11:55:35+0200',
+      });
+      handler.addProvisioningTask({
+        status: TaskStatuses.Success,
+        executedAt: '2022-02-03T11:45:35+0200',
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableProvisioning(user);
+      expect(ui.githubProvisioningSuccess.get()).toBeInTheDocument();
+      expect(ui.githubProvisioningPending.get()).toBeInTheDocument();
+    });
+
+    it('should display an error alert when the synchronisation failed', async () => {
+      handler.addProvisioningTask({
+        status: TaskStatuses.Failed,
+        executedAt: '2022-02-03T11:45:35+0200',
+        errorMessage: "T'es mauvais Jacques",
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableProvisioning(user);
+      expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
+      expect(ui.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
+      expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
+    });
+
+    it('should display an error alert even when another task is in progress', async () => {
+      handler.addProvisioningTask({
+        status: TaskStatuses.InProgress,
+        executedAt: '2022-02-03T11:55:35+0200',
+      });
+      handler.addProvisioningTask({
+        status: TaskStatuses.Failed,
+        executedAt: '2022-02-03T11:45:35+0200',
+        errorMessage: "T'es mauvais Jacques",
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableProvisioning(user);
+      expect(ui.githubProvisioningAlert.get()).toBeInTheDocument();
+      expect(ui.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
+      expect(ui.githubProvisioningSuccess.query()).not.toBeInTheDocument();
+      expect(ui.githubProvisioningInProgress.get()).toBeInTheDocument();
+    });
+
+    it('should display that config is valid for both provisioning with 1 org', async () => {
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+    });
+
+    it('should display that config is valid for both provisioning with multiple orgs', async () => {
+      handler.setConfigurationValidity({
+        installations: [
+          {
+            organization: 'org1',
+            autoProvisioning: { status: GitHubProvisioningStatus.Success },
+            jit: { status: GitHubProvisioningStatus.Success },
+          },
+          {
+            organization: 'org2',
+            autoProvisioning: { status: GitHubProvisioningStatus.Success },
+            jit: { status: GitHubProvisioningStatus.Success },
+          },
+        ],
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+      expect(ui.configurationValiditySuccess.get()).toHaveTextContent('2');
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_label',
+      );
+      expect(ui.getOrgs()[0]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_labelorg1',
+      );
+      expect(ui.getOrgs()[1]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_labelorg2',
+      );
+    });
+
+    it('should display that config is invalid for autoprovisioning if some apps are suspended but valid for jit', async () => {
+      const errorMessage = 'Installation suspended';
+      handler.setConfigurationValidity({
+        installations: [
+          {
+            organization: 'org1',
+            autoProvisioning: {
+              status: GitHubProvisioningStatus.Failed,
+              errorMessage,
+            },
+            jit: {
+              status: GitHubProvisioningStatus.Failed,
+              errorMessage,
+            },
+          },
+        ],
+      });
+
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValidityWarning.get()).toBeInTheDocument());
+      expect(ui.configurationValidityWarning.get()).toHaveTextContent(errorMessage);
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_label',
+      );
+      expect(ui.getOrgs()[0]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.invalid_labelorg1 - Installation suspended',
+      );
+
+      await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
+
+      await user.click(ui.githubProvisioningButton.get());
+      await waitFor(() => expect(ui.configurationValidityError.get()).toBeInTheDocument());
+      expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
+    });
+
+    it('should display that config is valid but some organizations were not found', async () => {
+      handler.setConfigurationValidity({
+        installations: [
+          {
+            organization: 'org1',
+            autoProvisioning: { status: GitHubProvisioningStatus.Success },
+            jit: { status: GitHubProvisioningStatus.Success },
+          },
+        ],
+      });
+
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValiditySuccess.get()).toBeInTheDocument());
+      expect(ui.configurationValiditySuccess.get()).toHaveTextContent('1');
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_label',
+      );
+      expect(ui.getOrgs()[0]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_labelorg1',
+      );
+      expect(ui.getOrgs()[1]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.org_not_found.organization1',
+      );
+    });
+
+    it('should display that config is invalid', async () => {
+      const errorMessage = 'Test error';
+      handler.setConfigurationValidity({
+        application: {
+          jit: {
+            status: GitHubProvisioningStatus.Failed,
+            errorMessage,
+          },
+          autoProvisioning: {
+            status: GitHubProvisioningStatus.Failed,
+            errorMessage,
+          },
+        },
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValidityError.query()).toBeInTheDocument());
+      expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.invalid_label',
+      );
+      expect(ui.configDetailsDialog.get()).toHaveTextContent(errorMessage);
+    });
+
+    it('should display that config is valid for jit, but not for auto', async () => {
+      const errorMessage = 'Test error';
+      handler.setConfigurationValidity({
+        application: {
+          jit: {
+            status: GitHubProvisioningStatus.Success,
+          },
+          autoProvisioning: {
+            status: GitHubProvisioningStatus.Failed,
+            errorMessage,
+          },
+        },
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+      expect(ui.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_label',
+      );
+      await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
+
+      await user.click(ui.githubProvisioningButton.get());
+
+      expect(ui.configurationValidityError.get()).toBeInTheDocument();
+      expect(ui.configurationValidityError.get()).toHaveTextContent(errorMessage);
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getConfigDetailsTitle()).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.invalid_label',
+      );
+    });
+
+    it('should display that config is invalid because of orgs', async () => {
+      const errorMessage = 'Test error';
+      handler.setConfigurationValidity({
+        installations: [
+          {
+            organization: 'org1',
+            autoProvisioning: { status: GitHubProvisioningStatus.Success },
+            jit: { status: GitHubProvisioningStatus.Success },
+          },
+          {
+            organization: 'org2',
+            jit: { status: GitHubProvisioningStatus.Failed, errorMessage },
+            autoProvisioning: { status: GitHubProvisioningStatus.Failed, errorMessage },
+          },
+        ],
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+
+      await appLoaded();
+
+      await waitFor(() => expect(ui.configurationValiditySuccess.query()).toBeInTheDocument());
+
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+
+      expect(ui.getOrgs()[0]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.valid_labelorg1',
+      );
+      expect(ui.getOrgs()[1]).toHaveTextContent(
+        'settings.authentication.github.configuration.validation.details.invalid_labelorg2 - Test error',
+      );
+
+      await user.click(ui.configDetailsDialog.byRole('button', { name: 'close' }).get());
+
+      await user.click(ui.githubProvisioningButton.get());
+
+      expect(ui.configurationValidityError.get()).toBeInTheDocument();
+      expect(ui.configurationValidityError.get()).toHaveTextContent(
+        `settings.authentication.github.configuration.validation.invalid_org.org2.${errorMessage}`,
+      );
+      await user.click(ui.viewConfigValidityDetailsButton.get());
+      expect(ui.getOrgs()[1]).toHaveTextContent(
+        `settings.authentication.github.configuration.validation.details.invalid_labelorg2 - ${errorMessage}`,
+      );
+    });
+
+    it('should update provisioning validity after clicking Test Configuration', async () => {
+      const errorMessage = 'Test error';
+      handler.setConfigurationValidity({
+        application: {
+          jit: {
+            status: GitHubProvisioningStatus.Failed,
+            errorMessage,
+          },
+          autoProvisioning: {
+            status: GitHubProvisioningStatus.Failed,
+            errorMessage,
+          },
+        },
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableConfiguration(user);
+      handler.setConfigurationValidity({
+        application: {
+          jit: {
+            status: GitHubProvisioningStatus.Success,
+          },
+          autoProvisioning: {
+            status: GitHubProvisioningStatus.Success,
+          },
+        },
+      });
+
+      await appLoaded();
+
+      expect(await ui.configurationValidityError.find()).toBeInTheDocument();
+
+      await user.click(ui.checkConfigButton.get());
+
+      expect(ui.configurationValiditySuccess.get()).toBeInTheDocument();
+      expect(ui.configurationValidityError.query()).not.toBeInTheDocument();
+    });
+
+    it('should show warning', async () => {
+      handler.addProvisioningTask({
+        status: TaskStatuses.Success,
+        warnings: ['Warning'],
+      });
+      renderAuthentication([Feature.GithubProvisioning]);
+      await ui.enableProvisioning(user);
+
+      expect(await ui.syncWarning.find()).toBeInTheDocument();
+      expect(ui.syncSummary.get()).toBeInTheDocument();
+    });
+
+    it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
+      renderAuthentication([Feature.GithubProvisioning]);
+
+      await user.click(await ui.tab.find());
+
+      expect(await ui.consentDialog.find()).toBeInTheDocument();
+      await user.click(ui.continueAutoButton.get());
+
+      expect(await ui.githubProvisioningButton.find()).toBeChecked();
+      expect(ui.consentDialog.query()).not.toBeInTheDocument();
+    });
+
+    it('should display a modal if user was already using auto and switch to JIT', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
+      renderAuthentication([Feature.GithubProvisioning]);
+
+      await user.click(await ui.tab.find());
+
+      expect(await ui.consentDialog.find()).toBeInTheDocument();
+      await user.click(ui.switchJitButton.get());
+
+      expect(await ui.jitProvisioningButton.find()).toBeChecked();
+      expect(ui.consentDialog.query()).not.toBeInTheDocument();
+    });
+
+    it('should sort mapping rows', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      renderAuthentication([Feature.GithubProvisioning]);
+      await user.click(await ui.tab.find());
+
+      expect(await ui.editMappingButton.find()).toBeInTheDocument();
+      await user.click(ui.editMappingButton.get());
+
+      const rows = (await ui.mappingRow.findAll()).filter(
+        (row) => within(row).queryAllByRole('checkbox').length > 0,
+      );
+
+      expect(rows).toHaveLength(5);
+
+      expect(rows[0]).toHaveTextContent('read');
+      expect(rows[1]).toHaveTextContent('triage');
+      expect(rows[2]).toHaveTextContent('write');
+      expect(rows[3]).toHaveTextContent('maintain');
+      expect(rows[4]).toHaveTextContent('admin');
+    });
+
+    it('should apply new mapping and new provisioning type at the same time', async () => {
+      const user = userEvent.setup();
+      renderAuthentication([Feature.GithubProvisioning]);
+      await user.click(await ui.tab.find());
+
+      await ui.createConfiguration(user);
+      await user.click(await ui.enableConfigButton.find());
+
+      expect(await ui.jitProvisioningButton.find()).toBeChecked();
+      expect(ui.editMappingButton.query()).not.toBeInTheDocument();
+      await user.click(ui.githubProvisioningButton.get());
+      expect(await ui.editMappingButton.find()).toBeInTheDocument();
+      await user.click(ui.editMappingButton.get());
+
+      expect(await ui.mappingRow.findAll()).toHaveLength(7);
+
+      let readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'));
+      let adminCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('admin'));
+
+      expect(readCheckboxes[0]).toBeChecked();
+      expect(readCheckboxes[5]).not.toBeChecked();
+      expect(adminCheckboxes[5]).toBeChecked();
+
+      await user.click(readCheckboxes[0]);
+      await user.click(readCheckboxes[5]);
+      await user.click(adminCheckboxes[5]);
+      await user.click(ui.mappingDialogClose.get());
+
+      await user.click(ui.saveGithubProvisioning.get());
+      await user.click(ui.confirmProvisioningButton.get());
+
+      // Clean local mapping state
+      await user.click(ui.jitProvisioningButton.get());
+      await user.click(ui.githubProvisioningButton.get());
+
+      await user.click(ui.editMappingButton.get());
+      readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'));
+      adminCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('admin'));
+
+      expect(readCheckboxes[0]).not.toBeChecked();
+      expect(readCheckboxes[5]).toBeChecked();
+      expect(adminCheckboxes[5]).not.toBeChecked();
+      await user.click(ui.mappingDialogClose.get());
+    });
+
+    it('should apply new mapping on auto-provisioning', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      renderAuthentication([Feature.GithubProvisioning]);
+      await user.click(await ui.tab.find());
+
+      expect(await ui.saveGithubProvisioning.find()).toBeDisabled();
+      await user.click(ui.editMappingButton.get());
+
+      expect(await ui.mappingRow.findAll()).toHaveLength(7);
+
+      let readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'))[0];
+
+      expect(readCheckboxes).toBeChecked();
+
+      await user.click(readCheckboxes);
+      await user.click(ui.mappingDialogClose.get());
+
+      expect(await ui.saveGithubProvisioning.find()).toBeEnabled();
+
+      await user.click(ui.saveGithubProvisioning.get());
+
+      // Clean local mapping state
+      await user.click(ui.jitProvisioningButton.get());
+      await user.click(ui.githubProvisioningButton.get());
+
+      await user.click(ui.editMappingButton.get());
+      readCheckboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('read'))[0];
+
+      expect(readCheckboxes).not.toBeChecked();
+      await user.click(ui.mappingDialogClose.get());
+    });
+
+    it('should add/remove/update custom roles', async () => {
+      const user = userEvent.setup();
+      settingsHandler.presetGithubAutoProvisioning();
+      handler.enableGithubProvisioning();
+      handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']);
+      handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
+      renderAuthentication([Feature.GithubProvisioning]);
+      await user.click(await ui.tab.find());
+
+      expect(await ui.saveGithubProvisioning.find()).toBeDisabled();
+      await user.click(ui.editMappingButton.get());
+
+      const rows = (await ui.mappingRow.findAll()).filter(
+        (row) => within(row).queryAllByRole('checkbox').length > 0,
+      );
+
+      expect(rows).toHaveLength(7);
+
+      let custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+
+      expect(custom1Checkboxes[0]).toBeChecked();
+      expect(custom1Checkboxes[1]).toBeChecked();
+      expect(custom1Checkboxes[2]).not.toBeChecked();
+      expect(custom1Checkboxes[3]).not.toBeChecked();
+      expect(custom1Checkboxes[4]).not.toBeChecked();
+      expect(custom1Checkboxes[5]).toBeChecked();
+
+      await user.click(custom1Checkboxes[1]);
+      await user.click(custom1Checkboxes[2]);
+
+      await user.click(ui.deleteCustomRoleCustom2.get());
+
+      expect(ui.customRoleInput.get()).toHaveValue('');
+      await user.type(ui.customRoleInput.get(), 'read');
+      await user.click(ui.customRoleAddBtn.get());
+      expect(await ui.roleExistsError.find()).toBeInTheDocument();
+      expect(ui.customRoleAddBtn.get()).toBeDisabled();
+      await user.clear(ui.customRoleInput.get());
+      expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+      await user.type(ui.customRoleInput.get(), 'custom1');
+      await user.click(ui.customRoleAddBtn.get());
+      expect(await ui.roleExistsError.find()).toBeInTheDocument();
+      expect(ui.customRoleAddBtn.get()).toBeDisabled();
+      await user.clear(ui.customRoleInput.get());
+      await user.type(ui.customRoleInput.get(), 'custom3');
+      expect(ui.roleExistsError.query()).not.toBeInTheDocument();
+      expect(ui.customRoleAddBtn.get()).toBeEnabled();
+      await user.click(ui.customRoleAddBtn.get());
+
+      let custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+      expect(custom3Checkboxes[0]).toBeChecked();
+      expect(custom3Checkboxes[1]).not.toBeChecked();
+      expect(custom3Checkboxes[2]).not.toBeChecked();
+      expect(custom3Checkboxes[3]).not.toBeChecked();
+      expect(custom3Checkboxes[4]).not.toBeChecked();
+      expect(custom3Checkboxes[5]).not.toBeChecked();
+      await user.click(custom3Checkboxes[0]);
+      expect(await ui.emptyRoleError.find()).toBeInTheDocument();
+      expect(ui.mappingDialogClose.get()).toBeDisabled();
+      await user.click(custom3Checkboxes[1]);
+      expect(ui.emptyRoleError.query()).not.toBeInTheDocument();
+      expect(ui.mappingDialogClose.get()).toBeEnabled();
+      await user.click(ui.mappingDialogClose.get());
+
+      expect(await ui.saveGithubProvisioning.find()).toBeEnabled();
+      await user.click(ui.saveGithubProvisioning.get());
+
+      // Clean local mapping state
+      await user.click(ui.jitProvisioningButton.get());
+      await user.click(ui.githubProvisioningButton.get());
+
+      await user.click(ui.editMappingButton.get());
+
+      expect(
+        (await ui.mappingRow.findAll()).filter(
+          (row) => within(row).queryAllByRole('checkbox').length > 0,
+        ),
+      ).toHaveLength(7);
+      custom1Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom1'));
+      custom3Checkboxes = ui.mappingCheckbox.getAll(ui.getMappingRowByRole('custom3'));
+      expect(ui.getMappingRowByRole('custom2')).toBeUndefined();
+      expect(custom1Checkboxes[0]).toBeChecked();
+      expect(custom1Checkboxes[1]).not.toBeChecked();
+      expect(custom1Checkboxes[2]).toBeChecked();
+      expect(custom1Checkboxes[3]).not.toBeChecked();
+      expect(custom1Checkboxes[4]).not.toBeChecked();
+      expect(custom1Checkboxes[5]).toBeChecked();
+      expect(custom3Checkboxes[0]).not.toBeChecked();
+      expect(custom3Checkboxes[1]).toBeChecked();
+      expect(custom3Checkboxes[2]).not.toBeChecked();
+      expect(custom3Checkboxes[3]).not.toBeChecked();
+      expect(custom3Checkboxes[4]).not.toBeChecked();
+      expect(custom3Checkboxes[5]).not.toBeChecked();
+      await user.click(ui.mappingDialogClose.get());
+    });
+  });
+});
+
+const appLoaded = async () => {
+  await waitFor(async () => {
+    expect(await screen.findByText('loading')).not.toBeInTheDocument();
+  });
+};
+
+function renderAuthentication(features: Feature[] = []) {
+  renderComponent(
+    <AvailableFeaturesContext.Provider value={features}>
+      <Authentication definitions={definitions} />
+    </AvailableFeaturesContext.Provider>,
+    `?tab=${AlmKeys.GitHub}`,
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Gitlab-it.tsx
new file mode 100644 (file)
index 0000000..0946708
--- /dev/null
@@ -0,0 +1,478 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
+import GitlabProvisioningServiceMock from '../../../../../api/mocks/GitlabProvisioningServiceMock';
+import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { mockGitlabConfiguration } from '../../../../../helpers/mocks/alm-integrations';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../../types/alm-settings';
+import { Feature } from '../../../../../types/features';
+import { ProvisioningType } from '../../../../../types/provisioning';
+import { TaskStatuses, TaskTypes } from '../../../../../types/tasks';
+import Authentication from '../Authentication';
+
+let handler: GitlabProvisioningServiceMock;
+let system: SystemServiceMock;
+let settingsHandler: SettingsServiceMock;
+let computeEngineHandler: ComputeEngineServiceMock;
+
+beforeEach(() => {
+  handler = new GitlabProvisioningServiceMock();
+  system = new SystemServiceMock();
+  settingsHandler = new SettingsServiceMock();
+  computeEngineHandler = new ComputeEngineServiceMock();
+});
+
+afterEach(() => {
+  handler.reset();
+  settingsHandler.reset();
+  system.reset();
+  computeEngineHandler.reset();
+});
+
+const glContainer = byRole('tabpanel', { name: 'gitlab GitLab' });
+
+const ui = {
+  noGitlabConfiguration: glContainer.byText('settings.authentication.gitlab.form.not_configured'),
+  createConfigButton: glContainer.byRole('button', {
+    name: 'settings.authentication.form.create',
+  }),
+  editConfigButton: glContainer.byRole('button', {
+    name: 'settings.authentication.form.edit',
+  }),
+  deleteConfigButton: glContainer.byRole('button', {
+    name: 'settings.authentication.form.delete',
+  }),
+  enableConfigButton: glContainer.byRole('button', {
+    name: 'settings.authentication.form.enable',
+  }),
+  disableConfigButton: glContainer.byRole('button', {
+    name: 'settings.authentication.form.disable',
+  }),
+  createDialog: byRole('dialog', {
+    name: 'settings.authentication.gitlab.form.create',
+  }),
+  editDialog: byRole('dialog', {
+    name: 'settings.authentication.gitlab.form.edit',
+  }),
+  applicationId: byRole('textbox', {
+    name: 'property.applicationId.name',
+  }),
+  url: byRole('textbox', { name: 'property.url.name' }),
+  secret: byRole('textbox', {
+    name: 'property.secret.name',
+  }),
+  synchronizeGroups: byRole('switch', {
+    name: 'synchronizeGroups',
+  }),
+  saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
+  jitProvisioningRadioButton: glContainer.byRole('radio', {
+    name: 'settings.authentication.gitlab.provisioning_at_login',
+  }),
+  autoProvisioningRadioButton: glContainer.byRole('radio', {
+    name: 'settings.authentication.gitlab.form.provisioning_with_gitlab',
+  }),
+  jitAllowUsersToSignUpToggle: byRole('switch', { name: 'allowUsersToSignUp' }),
+  autoProvisioningToken: byRole('textbox', {
+    name: 'property.provisioningToken.name',
+  }),
+  autoProvisioningUpdateTokenButton: byRole('button', {
+    name: 'settings.almintegration.form.secret.update_field',
+  }),
+  autoProvisioningGroupsInput: byRole('textbox', {
+    name: 'property.provisioningGroups.name',
+  }),
+  removeProvisioniongGroup: byRole('button', {
+    name: /settings.definition.delete_value.property.provisioningGroups.name./,
+  }),
+  saveProvisioning: glContainer.byRole('button', { name: 'save' }),
+  cancelProvisioningChanges: glContainer.byRole('button', { name: 'cancel' }),
+  confirmAutoProvisioningDialog: byRole('dialog', {
+    name: 'settings.authentication.gitlab.confirm.AUTO_PROVISIONING',
+  }),
+  confirmJitProvisioningDialog: byRole('dialog', {
+    name: 'settings.authentication.gitlab.confirm.JIT',
+  }),
+  confirmProvisioningChange: byRole('button', {
+    name: 'settings.authentication.gitlab.provisioning_change.confirm_changes',
+  }),
+  syncSummary: glContainer.byText(/Test summary/),
+  syncWarning: glContainer.byText(/Warning/),
+  gitlabProvisioningPending: glContainer.byText(/synchronization_pending/),
+  gitlabProvisioningInProgress: glContainer.byText(/synchronization_in_progress/),
+  gitlabProvisioningSuccess: glContainer.byText(/synchronization_successful/),
+  gitlabProvisioningAlert: glContainer.byText(/synchronization_failed/),
+};
+
+it('should create a Gitlab configuration and disable it', async () => {
+  handler.setGitlabConfigurations([]);
+  renderAuthentication();
+  const user = userEvent.setup();
+
+  expect(await ui.noGitlabConfiguration.find()).toBeInTheDocument();
+  expect(ui.createConfigButton.get()).toBeInTheDocument();
+
+  await user.click(ui.createConfigButton.get());
+  expect(await ui.createDialog.find()).toBeInTheDocument();
+  await user.type(ui.applicationId.get(), '123');
+  await user.type(ui.url.get(), 'https://company.ui.com');
+  await user.type(ui.secret.get(), '123');
+  await user.click(ui.synchronizeGroups.get());
+  await user.click(ui.saveConfigButton.get());
+
+  expect(await ui.editConfigButton.find()).toBeInTheDocument();
+  expect(ui.noGitlabConfiguration.query()).not.toBeInTheDocument();
+  expect(glContainer.get()).toHaveTextContent('https://company.ui.com');
+
+  expect(ui.disableConfigButton.get()).toBeInTheDocument();
+  await user.click(ui.disableConfigButton.get());
+  expect(ui.enableConfigButton.get()).toBeInTheDocument();
+  expect(ui.disableConfigButton.query()).not.toBeInTheDocument();
+});
+
+it('should edit/delete configuration', async () => {
+  const user = userEvent.setup();
+  renderAuthentication();
+
+  expect(await ui.editConfigButton.find()).toBeInTheDocument();
+  expect(glContainer.get()).toHaveTextContent('URL');
+  expect(ui.disableConfigButton.get()).toBeInTheDocument();
+  expect(ui.deleteConfigButton.get()).toBeInTheDocument();
+  expect(ui.deleteConfigButton.get()).toBeDisabled();
+
+  await user.click(ui.editConfigButton.get());
+  expect(await ui.editDialog.find()).toBeInTheDocument();
+  expect(ui.url.get()).toHaveValue('URL');
+  expect(ui.applicationId.get()).toBeInTheDocument();
+  expect(ui.secret.query()).not.toBeInTheDocument();
+  expect(ui.synchronizeGroups.get()).toBeChecked();
+  await user.clear(ui.url.get());
+  await user.type(ui.url.get(), 'https://company.ui.com');
+  await user.click(ui.saveConfigButton.get());
+
+  expect(glContainer.get()).not.toHaveTextContent('URL');
+  expect(glContainer.get()).toHaveTextContent('https://company.ui.com');
+
+  expect(ui.disableConfigButton.get()).toBeInTheDocument();
+  await user.click(ui.disableConfigButton.get());
+  expect(await ui.enableConfigButton.find()).toBeInTheDocument();
+  expect(ui.deleteConfigButton.get()).toBeEnabled();
+  await user.click(ui.deleteConfigButton.get());
+  expect(await ui.noGitlabConfiguration.find()).toBeInTheDocument();
+  expect(ui.editConfigButton.query()).not.toBeInTheDocument();
+});
+
+it('should change from just-in-time to Auto Provisioning with proper validation', async () => {
+  const user = userEvent.setup();
+  renderAuthentication([Feature.GitlabProvisioning]);
+
+  expect(await ui.editConfigButton.find()).toBeInTheDocument();
+  expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+
+  user.click(ui.autoProvisioningRadioButton.get());
+  expect(await ui.autoProvisioningRadioButton.find()).toBeEnabled();
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+
+  await user.type(ui.autoProvisioningToken.get(), 'JRR Tolkien');
+  expect(await ui.saveProvisioning.find()).toBeDisabled();
+
+  await user.type(ui.autoProvisioningGroupsInput.get(), 'NWA');
+  user.click(ui.autoProvisioningRadioButton.get());
+  expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+  await user.click(ui.removeProvisioniongGroup.get());
+  expect(await ui.saveProvisioning.find()).toBeDisabled();
+  await user.type(ui.autoProvisioningGroupsInput.get(), 'Wu-Tang Clan');
+  expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+  await user.clear(ui.autoProvisioningToken.get());
+  expect(await ui.saveProvisioning.find()).toBeDisabled();
+  await user.type(ui.autoProvisioningToken.get(), 'tiktoken');
+  expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+  await user.click(ui.saveProvisioning.get());
+  expect(ui.confirmAutoProvisioningDialog.get()).toBeInTheDocument();
+  await user.click(ui.confirmProvisioningChange.get());
+  expect(ui.confirmAutoProvisioningDialog.query()).not.toBeInTheDocument();
+
+  expect(ui.autoProvisioningRadioButton.get()).toBeChecked();
+  expect(await ui.saveProvisioning.find()).toBeDisabled();
+});
+
+it('should change from auto provisioning to JIT with proper validation', async () => {
+  handler.setGitlabConfigurations([
+    mockGitlabConfiguration({
+      allowUsersToSignUp: false,
+      enabled: true,
+      synchronizationType: ProvisioningType.auto,
+      provisioningGroups: ['D12'],
+    }),
+  ]);
+  const user = userEvent.setup();
+  renderAuthentication([Feature.GitlabProvisioning]);
+
+  expect(await ui.editConfigButton.find()).toBeInTheDocument();
+
+  expect(ui.jitProvisioningRadioButton.get()).not.toBeChecked();
+  expect(ui.autoProvisioningRadioButton.get()).toBeChecked();
+  expect(ui.autoProvisioningGroupsInput.get()).toHaveValue('D12');
+
+  expect(ui.autoProvisioningToken.query()).not.toBeInTheDocument();
+  expect(ui.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+
+  await user.click(ui.jitProvisioningRadioButton.get());
+  expect(await ui.jitProvisioningRadioButton.find()).toBeChecked();
+
+  expect(await ui.saveProvisioning.find()).toBeEnabled();
+
+  expect(ui.jitAllowUsersToSignUpToggle.get()).toBeInTheDocument();
+
+  await user.click(ui.saveProvisioning.get());
+  expect(ui.confirmJitProvisioningDialog.get()).toBeInTheDocument();
+  await user.click(ui.confirmProvisioningChange.get());
+  expect(ui.confirmJitProvisioningDialog.query()).not.toBeInTheDocument();
+
+  expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+  expect(await ui.saveProvisioning.find()).toBeDisabled();
+});
+
+it('should be able to allow user to sign up for JIT with proper validation', async () => {
+  handler.setGitlabConfigurations([
+    mockGitlabConfiguration({
+      allowUsersToSignUp: false,
+      enabled: true,
+      synchronizationType: ProvisioningType.jit,
+    }),
+  ]);
+  const user = userEvent.setup();
+  renderAuthentication([Feature.GitlabProvisioning]);
+
+  expect(await ui.editConfigButton.find()).toBeInTheDocument();
+
+  expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+  expect(ui.autoProvisioningRadioButton.get()).not.toBeChecked();
+
+  expect(ui.jitAllowUsersToSignUpToggle.get()).not.toBeChecked();
+
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+  await user.click(ui.jitAllowUsersToSignUpToggle.get());
+  expect(ui.saveProvisioning.get()).toBeEnabled();
+  await user.click(ui.jitAllowUsersToSignUpToggle.get());
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+  await user.click(ui.jitAllowUsersToSignUpToggle.get());
+
+  await user.click(ui.saveProvisioning.get());
+
+  expect(ui.jitProvisioningRadioButton.get()).toBeChecked();
+  expect(ui.jitAllowUsersToSignUpToggle.get()).toBeChecked();
+  expect(await ui.saveProvisioning.find()).toBeDisabled();
+});
+
+it('should be able to edit groups and token for Auto provisioning with proper validation', async () => {
+  handler.setGitlabConfigurations([
+    mockGitlabConfiguration({
+      allowUsersToSignUp: false,
+      enabled: true,
+      synchronizationType: ProvisioningType.auto,
+      provisioningGroups: ['Cypress Hill', 'Public Enemy'],
+    }),
+  ]);
+  const user = userEvent.setup();
+  renderAuthentication([Feature.GitlabProvisioning]);
+
+  expect(await ui.autoProvisioningRadioButton.find()).toBeChecked();
+  expect(ui.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+  expect(ui.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
+
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+
+  // Changing the Provisioning token should enable save
+  await user.click(ui.autoProvisioningUpdateTokenButton.get());
+  await user.type(ui.autoProvisioningGroupsInput.get(), 'Tok Token!');
+  expect(ui.saveProvisioning.get()).toBeEnabled();
+  await user.click(ui.cancelProvisioningChanges.get());
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+
+  // Adding a group should enable save
+  await user.click(ui.autoProvisioningGroupsInput.get());
+  await user.tab();
+  await user.tab();
+  await user.tab();
+  await user.tab();
+  await user.keyboard('Run DMC');
+  expect(ui.saveProvisioning.get()).toBeEnabled();
+  await user.tab();
+  await user.keyboard('{Enter}');
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+
+  // Removing a group should enable save
+  await user.click(ui.autoProvisioningGroupsInput.get());
+  await user.tab();
+  await user.keyboard('{Enter}');
+  expect(ui.saveProvisioning.get()).toBeEnabled();
+
+  // Removing all groups should disable save
+  await user.click(ui.autoProvisioningGroupsInput.get());
+  await user.tab();
+  await user.keyboard('{Enter}');
+  expect(ui.saveProvisioning.get()).toBeDisabled();
+});
+
+it('should be able to reset Auto Provisioning changes', async () => {
+  handler.setGitlabConfigurations([
+    mockGitlabConfiguration({
+      allowUsersToSignUp: false,
+      enabled: true,
+      synchronizationType: ProvisioningType.auto,
+      provisioningGroups: ['Cypress Hill', 'Public Enemy'],
+    }),
+  ]);
+  const user = userEvent.setup();
+  renderAuthentication([Feature.GitlabProvisioning]);
+
+  expect(await ui.autoProvisioningRadioButton.find()).toBeChecked();
+
+  // Cancel doesn't fully work yet as the AuthenticationFormField needs to be worked on
+  await user.click(ui.autoProvisioningGroupsInput.get());
+  await user.tab();
+  await user.tab();
+  await user.tab();
+  await user.tab();
+  await user.keyboard('A Tribe Called Quest');
+  await user.click(ui.autoProvisioningGroupsInput.get());
+  await user.tab();
+  await user.keyboard('{Enter}');
+  await user.click(ui.autoProvisioningUpdateTokenButton.get());
+  await user.type(ui.autoProvisioningGroupsInput.get(), 'ToToken!');
+  expect(ui.saveProvisioning.get()).toBeEnabled();
+  await user.click(ui.cancelProvisioningChanges.get());
+  // expect(ui.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
+  expect(ui.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
+});
+
+describe('Gitlab Provisioning', () => {
+  beforeEach(() => {
+    jest.useFakeTimers({
+      advanceTimers: true,
+      now: new Date('2022-02-04T12:00:59Z'),
+    });
+    handler.setGitlabConfigurations([
+      mockGitlabConfiguration({
+        enabled: true,
+        synchronizationType: ProvisioningType.auto,
+        provisioningGroups: ['Test'],
+      }),
+    ]);
+  });
+
+  afterEach(() => {
+    jest.runOnlyPendingTimers();
+    jest.useRealTimers();
+  });
+
+  it('should display a success status when the synchronisation is a success', async () => {
+    computeEngineHandler.addTask({
+      status: TaskStatuses.Success,
+      executedAt: '2022-02-03T11:45:35+0200',
+      infoMessages: ['Test summary'],
+      type: TaskTypes.GitlabProvisioning,
+    });
+
+    renderAuthentication([Feature.GitlabProvisioning]);
+    expect(await ui.gitlabProvisioningSuccess.find()).toBeInTheDocument();
+    expect(ui.syncSummary.get()).toBeInTheDocument();
+  });
+
+  it('should display a success status even when another task is pending', async () => {
+    computeEngineHandler.addTask({
+      status: TaskStatuses.Pending,
+      executedAt: '2022-02-03T11:55:35+0200',
+      type: TaskTypes.GitlabProvisioning,
+    });
+    computeEngineHandler.addTask({
+      status: TaskStatuses.Success,
+      executedAt: '2022-02-03T11:45:35+0200',
+      type: TaskTypes.GitlabProvisioning,
+    });
+    renderAuthentication([Feature.GitlabProvisioning]);
+    expect(await ui.gitlabProvisioningSuccess.find()).toBeInTheDocument();
+    expect(ui.gitlabProvisioningPending.get()).toBeInTheDocument();
+  });
+
+  it('should display an error alert when the synchronisation failed', async () => {
+    computeEngineHandler.addTask({
+      status: TaskStatuses.Failed,
+      executedAt: '2022-02-03T11:45:35+0200',
+      errorMessage: "T'es mauvais Jacques",
+      type: TaskTypes.GitlabProvisioning,
+    });
+    renderAuthentication([Feature.GitlabProvisioning]);
+    expect(await ui.gitlabProvisioningAlert.find()).toBeInTheDocument();
+    expect(ui.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+    expect(ui.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
+  });
+
+  it('should display an error alert even when another task is in progress', async () => {
+    computeEngineHandler.addTask({
+      status: TaskStatuses.InProgress,
+      executedAt: '2022-02-03T11:55:35+0200',
+      type: TaskTypes.GitlabProvisioning,
+    });
+    computeEngineHandler.addTask({
+      status: TaskStatuses.Failed,
+      executedAt: '2022-02-03T11:45:35+0200',
+      errorMessage: "T'es mauvais Jacques",
+      type: TaskTypes.GitlabProvisioning,
+    });
+    renderAuthentication([Feature.GitlabProvisioning]);
+    expect(await ui.gitlabProvisioningAlert.find()).toBeInTheDocument();
+    expect(ui.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
+    expect(ui.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
+    expect(ui.gitlabProvisioningInProgress.get()).toBeInTheDocument();
+  });
+
+  it('should show warning', async () => {
+    computeEngineHandler.addTask({
+      status: TaskStatuses.Success,
+      warnings: ['Warning'],
+      infoMessages: ['Test summary'],
+      type: TaskTypes.GitlabProvisioning,
+    });
+    renderAuthentication([Feature.GitlabProvisioning]);
+
+    expect(await ui.syncWarning.find()).toBeInTheDocument();
+    expect(ui.syncSummary.get()).toBeInTheDocument();
+  });
+});
+
+function renderAuthentication(features: Feature[] = []) {
+  renderComponent(
+    <AvailableFeaturesContext.Provider value={features}>
+      <Authentication definitions={definitions} />
+    </AvailableFeaturesContext.Provider>,
+    `?tab=${AlmKeys.GitLab}`,
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-Scim-it.tsx
new file mode 100644 (file)
index 0000000..5f613d5
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
+import React from 'react';
+import ScimProvisioningServiceMock from '../../../../../api/mocks/ScimProvisioningServiceMock';
+import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
+import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../../helpers/testSelector';
+import { Feature } from '../../../../../types/features';
+import Authentication from '../Authentication';
+
+let handler: ScimProvisioningServiceMock;
+let system: SystemServiceMock;
+let settingsHandler: SettingsServiceMock;
+
+beforeEach(() => {
+  handler = new ScimProvisioningServiceMock();
+  system = new SystemServiceMock();
+  settingsHandler = new SettingsServiceMock();
+  [
+    {
+      key: 'sonar.auth.saml.signature.enabled',
+      value: 'false',
+    },
+    {
+      key: 'sonar.auth.saml.enabled',
+      value: 'false',
+    },
+    {
+      key: 'sonar.auth.saml.applicationId',
+      value: 'sonarqube',
+    },
+    {
+      key: 'sonar.auth.saml.providerName',
+      value: 'SAML',
+    },
+  ].forEach((setting: any) => settingsHandler.set(setting.key, setting.value));
+});
+
+afterEach(() => {
+  handler.reset();
+  settingsHandler.reset();
+  system.reset();
+});
+
+const samlContainer = byRole('tabpanel', { name: 'SAML' });
+
+const ui = {
+  noSamlConfiguration: byText('settings.authentication.saml.form.not_configured'),
+  createConfigButton: samlContainer.byRole('button', {
+    name: 'settings.authentication.form.create',
+  }),
+  providerName: byRole('textbox', { name: 'property.sonar.auth.saml.providerName.name' }),
+  providerId: byRole('textbox', { name: 'property.sonar.auth.saml.providerId.name' }),
+  providerCertificate: byRole('textbox', {
+    name: 'property.sonar.auth.saml.certificate.secured.name',
+  }),
+  loginUrl: byRole('textbox', { name: 'property.sonar.auth.saml.loginUrl.name' }),
+  userLoginAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.login.name' }),
+  userNameAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.name.name' }),
+  saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
+  confirmProvisioningButton: byRole('button', {
+    name: 'yes',
+  }),
+  saveScim: samlContainer.byRole('button', { name: 'save' }),
+  enableConfigButton: samlContainer.byRole('button', {
+    name: 'settings.authentication.form.enable',
+  }),
+  disableConfigButton: samlContainer.byRole('button', {
+    name: 'settings.authentication.form.disable',
+  }),
+  editConfigButton: samlContainer.byRole('button', {
+    name: 'settings.authentication.form.edit',
+  }),
+  enableFirstMessage: byText('settings.authentication.saml.enable_first'),
+  jitProvisioningButton: byRole('radio', {
+    name: 'settings.authentication.saml.form.provisioning_at_login',
+  }),
+  scimProvisioningButton: byRole('radio', {
+    name: 'settings.authentication.saml.form.provisioning_with_scim',
+  }),
+  fillForm: async (user: UserEvent) => {
+    await user.clear(ui.providerName.get());
+    await user.type(ui.providerName.get(), 'Awsome SAML config');
+    await user.type(ui.providerId.get(), 'okta-1234');
+    await user.type(ui.loginUrl.get(), 'http://test.org');
+    await user.type(ui.providerCertificate.get(), '-secret-');
+    await user.type(ui.userLoginAttribute.get(), 'login');
+    await user.type(ui.userNameAttribute.get(), 'name');
+  },
+  createConfiguration: async (user: UserEvent) => {
+    await user.click(await ui.createConfigButton.find());
+    await ui.fillForm(user);
+    await user.click(ui.saveConfigButton.get());
+  },
+};
+
+it('should render an empty SAML configuration', async () => {
+  renderAuthentication();
+  expect(await ui.noSamlConfiguration.find()).toBeInTheDocument();
+});
+
+it('should be able to create a configuration', async () => {
+  const user = userEvent.setup();
+  renderAuthentication();
+
+  await user.click(await ui.createConfigButton.find());
+
+  expect(ui.saveConfigButton.get()).toBeDisabled();
+  await ui.fillForm(user);
+  expect(ui.saveConfigButton.get()).toBeEnabled();
+
+  await user.click(ui.saveConfigButton.get());
+
+  expect(await ui.editConfigButton.find()).toBeInTheDocument();
+});
+
+it('should be able to enable/disable configuration', async () => {
+  const user = userEvent.setup();
+  renderAuthentication();
+
+  await ui.createConfiguration(user);
+  await user.click(await ui.enableConfigButton.find());
+
+  expect(await ui.disableConfigButton.find()).toBeInTheDocument();
+  await user.click(ui.disableConfigButton.get());
+  await waitFor(() => expect(ui.disableConfigButton.query()).not.toBeInTheDocument());
+
+  expect(await ui.enableConfigButton.find()).toBeInTheDocument();
+});
+
+it('should be able to choose provisioning', async () => {
+  const user = userEvent.setup();
+
+  renderAuthentication([Feature.Scim]);
+
+  await ui.createConfiguration(user);
+
+  expect(await ui.enableFirstMessage.find()).toBeInTheDocument();
+  await user.click(await ui.enableConfigButton.find());
+
+  expect(await ui.jitProvisioningButton.find()).toBeChecked();
+  expect(ui.saveScim.get()).toBeDisabled();
+
+  await user.click(ui.scimProvisioningButton.get());
+  expect(ui.saveScim.get()).toBeEnabled();
+  await user.click(ui.saveScim.get());
+  await user.click(ui.confirmProvisioningButton.get());
+
+  expect(await ui.scimProvisioningButton.find()).toBeChecked();
+  expect(await ui.saveScim.find()).toBeDisabled();
+});
+
+it('should not allow editions below Enterprise to select SCIM provisioning', async () => {
+  const user = userEvent.setup();
+
+  renderAuthentication();
+
+  await ui.createConfiguration(user);
+  await user.click(await ui.enableConfigButton.find());
+
+  expect(await ui.jitProvisioningButton.find()).toBeChecked();
+  expect(ui.scimProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
+});
+
+function renderAuthentication(features: Feature[] = []) {
+  renderComponent(
+    <AvailableFeaturesContext.Provider value={features}>
+      <Authentication definitions={definitions} />
+    </AvailableFeaturesContext.Provider>,
+  );
+}
index f8d1a8684d54517eae11d3e229d4638b50a3a53d..dc9fce41488eabc624a318b4cca9d5b2693f5410 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { screen, waitFor, within } from '@testing-library/react';
+import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import React from 'react';
-import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
-import ComputeEngineServiceMock from '../../../../../api/mocks/ComputeEngineServiceMock';
+import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
+import GitlabProvisioningServiceMock from '../../../../../api/mocks/GitlabProvisioningServiceMock';
+import ScimProvisioningServiceMock from '../../../../../api/mocks/ScimProvisioningServiceMock';
 import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock';
 import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
 import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
-import { mockGitlabConfiguration } from '../../../../../helpers/mocks/alm-integrations';
 import { definitions } from '../../../../../helpers/mocks/definitions-list';
 import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
 import { byRole, byText } from '../../../../../helpers/testSelector';
 import { Feature } from '../../../../../types/features';
-import { GitHubProvisioningStatus, ProvisioningType } from '../../../../../types/provisioning';
-import { TaskStatuses, TaskTypes } from '../../../../../types/tasks';
 import Authentication from '../Authentication';
 
-let handler: AuthenticationServiceMock;
+let scimHandler: ScimProvisioningServiceMock;
+let githubHandler: GithubProvisioningServiceMock;
+let gitlabHandler: GitlabProvisioningServiceMock;
 let system: SystemServiceMock;
 let settingsHandler: SettingsServiceMock;
-let computeEngineHandler: ComputeEngineServiceMock;
 
 beforeEach(() => {
-  handler = new AuthenticationServiceMock();
+  scimHandler = new ScimProvisioningServiceMock();
+  githubHandler = new GithubProvisioningServiceMock();
+  gitlabHandler = new GitlabProvisioningServiceMock();
   system = new SystemServiceMock();
   settingsHandler = new SettingsServiceMock();
-  computeEngineHandler = new ComputeEngineServiceMock();
-  [
-    {
-      key: 'sonar.auth.saml.signature.enabled',
-      value: 'false',
-    },
-    {
-      key: 'sonar.auth.saml.enabled',
-      value: 'false',
-    },
-    {
-      key: 'sonar.auth.saml.applicationId',
-      value: 'sonarqube',
-    },
-    {
-      key: 'sonar.auth.saml.providerName',
-      value: 'SAML',
-    },
-  ].forEach((setting: any) => settingsHandler.set(setting.key, setting.value));
 });
 
 afterEach(() => {
-  handler.reset();
+  scimHandler.reset();
+  githubHandler.reset();
+  gitlabHandler.reset();
   settingsHandler.reset();
   system.reset();
-  computeEngineHandler.reset();
 });
 
-const ghContainer = byRole('tabpanel', { name: 'github GitHub' });
-const glContainer = byRole('tabpanel', { name: 'gitlab GitLab' });
-const samlContainer = byRole('tabpanel', { name: 'SAML' });
-
 const ui = {
   saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }),
   customMessageInformation: byText('settings.authentication.custom_message_information'),
   enabledToggle: byRole('switch'),
-  testButton: byText('settings.authentication.saml.form.test'),
-  textbox1: byRole('textbox', { name: 'test1' }),
-  textbox2: byRole('textbox', { name: 'test2' }),
-  saml: {
-    noSamlConfiguration: byText('settings.authentication.saml.form.not_configured'),
-    createConfigButton: samlContainer.byRole('button', {
-      name: 'settings.authentication.form.create',
-    }),
-    providerName: byRole('textbox', { name: 'property.sonar.auth.saml.providerName.name' }),
-    providerId: byRole('textbox', { name: 'property.sonar.auth.saml.providerId.name' }),
-    providerCertificate: byRole('textbox', {
-      name: 'property.sonar.auth.saml.certificate.secured.name',
-    }),
-    loginUrl: byRole('textbox', { name: 'property.sonar.auth.saml.loginUrl.name' }),
-    userLoginAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.login.name' }),
-    userNameAttribute: byRole('textbox', { name: 'property.sonar.auth.saml.user.name.name' }),
-    saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
-    confirmProvisioningButton: byRole('button', {
-      name: 'yes',
-    }),
-    saveScim: samlContainer.byRole('button', { name: 'save' }),
-    enableConfigButton: samlContainer.byRole('button', {
-      name: 'settings.authentication.form.enable',
-    }),
-    disableConfigButton: samlContainer.byRole('button', {
-      name: 'settings.authentication.form.disable',
-    }),
-    editConfigButton: samlContainer.byRole('button', {
-      name: 'settings.authentication.form.edit',
-    }),
-    enableFirstMessage: byText('settings.authentication.saml.enable_first'),
-    jitProvisioningButton: byRole('radio', {
-      name: 'settings.authentication.saml.form.provisioning_at_login',
-    }),
-    scimProvisioningButton: byRole('radio', {
-      name: 'settings.authentication.saml.form.provisioning_with_scim',
-    }),
-    fillForm: async (user: UserEvent) => {
-      const { saml } = ui;
-
-      await user.clear(saml.providerName.get());
-      await user.type(saml.providerName.get(), 'Awsome SAML config');
-      await user.type(saml.providerId.get(), 'okta-1234');
-      await user.type(saml.loginUrl.get(), 'http://test.org');
-      await user.type(saml.providerCertificate.get(), '-secret-');
-      await user.type(saml.userLoginAttribute.get(), 'login');
-      await user.type(saml.userNameAttribute.get(), 'name');
-    },
-    createConfiguration: async (user: UserEvent) => {
-      const { saml } = ui;
-
-      await user.click(await saml.createConfigButton.find());
-      await saml.fillForm(user);
-      await user.click(saml.saveConfigButton.get());
-    },
-  },
-  github: {
-    tab: byRole('tab', { name: 'github GitHub' }),
-    noGithubConfiguration: byText('settings.authentication.github.form.not_configured'),
-    createConfigButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.form.create',
-    }),
-    clientId: byRole('textbox', {
-      name: 'property.sonar.auth.github.clientId.secured.name',
-    }),
-    appId: byRole('textbox', { name: 'property.sonar.auth.github.appId.name' }),
-    privateKey: byRole('textbox', {
-      name: 'property.sonar.auth.github.privateKey.secured.name',
-    }),
-    clientSecret: byRole('textbox', {
-      name: 'property.sonar.auth.github.clientSecret.secured.name',
-    }),
-    githubApiUrl: byRole('textbox', { name: 'property.sonar.auth.github.apiUrl.name' }),
-    githubWebUrl: byRole('textbox', { name: 'property.sonar.auth.github.webUrl.name' }),
-    allowUserToSignUp: byRole('switch', {
-      name: 'sonar.auth.github.allowUsersToSignUp',
-    }),
-    organizations: byRole('textbox', {
-      name: 'property.sonar.auth.github.organizations.name',
-    }),
-    saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
-    confirmProvisioningButton: byRole('button', {
-      name: 'settings.authentication.github.provisioning_change.confirm_changes',
-    }),
-    saveGithubProvisioning: ghContainer.byRole('button', { name: 'save' }),
-    groupAttribute: byRole('textbox', {
-      name: 'property.sonar.auth.github.group.name.name',
-    }),
-    enableConfigButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.form.enable',
-    }),
-    disableConfigButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.form.disable',
-    }),
-    editConfigButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.form.edit',
-    }),
-    editMappingButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.github.configuration.roles_mapping.button_label',
-    }),
-    mappingRow: byRole('dialog', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
-    }).byRole('row'),
-    customRoleInput: byRole('textbox', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.add_custom_role',
-    }),
-    customRoleAddBtn: byRole('dialog', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
-    }).byRole('button', { name: 'add_verb' }),
-    roleExistsError: byRole('dialog', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
-    }).byText('settings.authentication.github.configuration.roles_mapping.role_exists'),
-    emptyRoleError: byRole('dialog', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
-    }).byText('settings.authentication.github.configuration.roles_mapping.empty_custom_role'),
-    deleteCustomRoleCustom2: byRole('button', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.delete_custom_role.custom2',
-    }),
-    getMappingRowByRole: (text: string) =>
-      ui.github.mappingRow.getAll().find((row) => within(row).queryByText(text) !== null),
-    mappingCheckbox: byRole('checkbox'),
-    mappingDialogClose: byRole('dialog', {
-      name: 'settings.authentication.github.configuration.roles_mapping.dialog.title',
-    }).byRole('button', {
-      name: 'close',
-    }),
-    deleteOrg: (org: string) =>
-      byRole('button', {
-        name: `settings.definition.delete_value.property.sonar.auth.github.organizations.name.${org}`,
-      }),
-    enableFirstMessage: ghContainer.byText('settings.authentication.github.enable_first'),
-    jitProvisioningButton: ghContainer.byRole('radio', {
-      name: 'settings.authentication.form.provisioning_at_login',
-    }),
-    githubProvisioningButton: ghContainer.byRole('radio', {
-      name: 'settings.authentication.github.form.provisioning_with_github',
-    }),
-    githubProvisioningPending: ghContainer.byText(/synchronization_pending/),
-    githubProvisioningInProgress: ghContainer.byText(/synchronization_in_progress/),
-    githubProvisioningSuccess: ghContainer.byText(/synchronization_successful/),
-    githubProvisioningAlert: ghContainer.byText(/synchronization_failed/),
-    configurationValidityLoading: ghContainer.byRole('status', {
-      name: /github.configuration.validation.loading/,
-    }),
-    configurationValiditySuccess: ghContainer.byRole('status', {
-      name: /github.configuration.validation.valid/,
-    }),
-    configurationValidityError: ghContainer.byRole('status', {
-      name: /github.configuration.validation.invalid/,
-    }),
-    syncWarning: ghContainer.byText(/Warning/),
-    syncSummary: ghContainer.byText(/Test summary/),
-    configurationValidityWarning: ghContainer.byRole('status', {
-      name: /github.configuration.validation.valid.short/,
-    }),
-    checkConfigButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.github.configuration.validation.test',
-    }),
-    viewConfigValidityDetailsButton: ghContainer.byRole('button', {
-      name: 'settings.authentication.github.configuration.validation.details',
-    }),
-    configDetailsDialog: byRole('dialog', {
-      name: 'settings.authentication.github.configuration.validation.details.title',
-    }),
-    continueAutoButton: byRole('button', {
-      name: 'settings.authentication.github.confirm_auto_provisioning.continue',
-    }),
-    switchJitButton: byRole('button', {
-      name: 'settings.authentication.github.confirm_auto_provisioning.switch_jit',
-    }),
-    consentDialog: byRole('dialog', {
-      name: 'settings.authentication.github.confirm_auto_provisioning.header',
-    }),
-    getConfigDetailsTitle: () => within(ui.github.configDetailsDialog.get()).getByRole('heading'),
-    getOrgs: () => within(ui.github.configDetailsDialog.get()).getAllByRole('listitem'),
-    fillForm: async (user: UserEvent) => {
-      const { github } = ui;
-
-      await user.type(await github.clientId.find(), 'Awsome GITHUB config');
-      await user.type(github.clientSecret.get(), 'Client shut');
-      await user.type(github.appId.get(), 'App id');
-      await user.type(github.privateKey.get(), 'Private Key');
-      await user.type(github.githubApiUrl.get(), 'API Url');
-      await user.type(github.githubWebUrl.get(), 'WEb Url');
-      await user.type(github.organizations.get(), 'organization1');
-    },
-    createConfiguration: async (user: UserEvent) => {
-      const { github } = ui;
-
-      await user.click(await github.createConfigButton.find());
-      await github.fillForm(user);
-
-      await user.click(github.saveConfigButton.get());
-    },
-    enableConfiguration: async (user: UserEvent) => {
-      const { github } = ui;
-      await user.click(await github.tab.find());
-      await github.createConfiguration(user);
-      await user.click(await github.enableConfigButton.find());
-    },
-    enableProvisioning: async (user: UserEvent) => {
-      const { github } = ui;
-      await user.click(await github.tab.find());
-
-      await github.createConfiguration(user);
-
-      await user.click(await github.enableConfigButton.find());
-      await user.click(await github.githubProvisioningButton.find());
-      await user.click(github.saveGithubProvisioning.get());
-      await user.click(github.confirmProvisioningButton.get());
-    },
-  },
-  gitlab: {
-    tab: byRole('tab', { name: 'gitlab GitLab' }),
-    noGitlabConfiguration: glContainer.byText('settings.authentication.gitlab.form.not_configured'),
-    createConfigButton: glContainer.byRole('button', {
-      name: 'settings.authentication.form.create',
-    }),
-    editConfigButton: glContainer.byRole('button', {
-      name: 'settings.authentication.form.edit',
-    }),
-    deleteConfigButton: glContainer.byRole('button', {
-      name: 'settings.authentication.form.delete',
-    }),
-    enableConfigButton: glContainer.byRole('button', {
-      name: 'settings.authentication.form.enable',
-    }),
-    disableConfigButton: glContainer.byRole('button', {
-      name: 'settings.authentication.form.disable',
-    }),
-    createDialog: byRole('dialog', {
-      name: 'settings.authentication.gitlab.form.create',
-    }),
-    editDialog: byRole('dialog', {
-      name: 'settings.authentication.gitlab.form.edit',
-    }),
-    applicationId: byRole('textbox', {
-      name: 'property.applicationId.name',
-    }),
-    url: byRole('textbox', { name: 'property.url.name' }),
-    clientSecret: byRole('textbox', {
-      name: 'property.clientSecret.name',
-    }),
-    synchronizeUserGroups: byRole('switch', {
-      name: 'synchronizeUserGroups',
-    }),
-    saveConfigButton: byRole('button', { name: 'settings.almintegration.form.save' }),
-    jitProvisioningRadioButton: glContainer.byRole('radio', {
-      name: 'settings.authentication.gitlab.provisioning_at_login',
-    }),
-    autoProvisioningRadioButton: glContainer.byRole('radio', {
-      name: 'settings.authentication.gitlab.form.provisioning_with_gitlab',
-    }),
-    jitAllowUsersToSignUpToggle: byRole('switch', { name: 'sonar.auth.gitlab.allowUsersToSignUp' }),
-    autoProvisioningToken: byRole('textbox', {
-      name: 'property.provisioning.gitlab.token.secured.name',
-    }),
-    autoProvisioningUpdateTokenButton: byRole('button', {
-      name: 'settings.almintegration.form.secret.update_field',
-    }),
-    autoProvisioningGroupsInput: byRole('textbox', {
-      name: 'property.provisioning.gitlab.groups.name',
-    }),
-    removeProvisioniongGroup: byRole('button', {
-      name: /settings.definition.delete_value.property.provisioning.gitlab.groups.name./,
-    }),
-    saveProvisioning: glContainer.byRole('button', { name: 'save' }),
-    cancelProvisioningChanges: glContainer.byRole('button', { name: 'cancel' }),
-    confirmAutoProvisioningDialog: byRole('dialog', {
-      name: 'settings.authentication.gitlab.confirm.Auto',
-    }),
-    confirmJitProvisioningDialog: byRole('dialog', {
-      name: 'settings.authentication.gitlab.confirm.JIT',
-    }),
-    confirmProvisioningChange: byRole('button', {
-      name: 'settings.authentication.gitlab.provisioning_change.confirm_changes',
-    }),
-    syncSummary: glContainer.byText(/Test summary/),
-    syncWarning: glContainer.byText(/Warning/),
-    gitlabProvisioningPending: glContainer.byText(/synchronization_pending/),
-    gitlabProvisioningInProgress: glContainer.byText(/synchronization_in_progress/),
-    gitlabProvisioningSuccess: glContainer.byText(/synchronization_successful/),
-    gitlabProvisioningAlert: glContainer.byText(/synchronization_failed/),
-  },
 };
 
 it('should render tabs and allow navigation', async () => {
@@ -395,1133 +89,6 @@ it('should display the login message feature info box', () => {
   expect(ui.customMessageInformation.get()).toBeInTheDocument();
 });
 
-describe('SAML tab', () => {
-  const { saml } = ui;
-
-  it('should render an empty SAML configuration', async () => {
-    renderAuthentication();
-    expect(await saml.noSamlConfiguration.find()).toBeInTheDocument();
-  });
-
-  it('should be able to create a configuration', async () => {
-    const user = userEvent.setup();
-    renderAuthentication();
-
-    await user.click(await saml.createConfigButton.find());
-
-    expect(saml.saveConfigButton.get()).toBeDisabled();
-    await saml.fillForm(user);
-    expect(saml.saveConfigButton.get()).toBeEnabled();
-
-    await user.click(saml.saveConfigButton.get());
-
-    expect(await saml.editConfigButton.find()).toBeInTheDocument();
-  });
-
-  it('should be able to enable/disable configuration', async () => {
-    const { saml } = ui;
-    const user = userEvent.setup();
-    renderAuthentication();
-
-    await saml.createConfiguration(user);
-    await user.click(await saml.enableConfigButton.find());
-
-    expect(await saml.disableConfigButton.find()).toBeInTheDocument();
-    await user.click(saml.disableConfigButton.get());
-    await waitFor(() => expect(saml.disableConfigButton.query()).not.toBeInTheDocument());
-
-    expect(await saml.enableConfigButton.find()).toBeInTheDocument();
-  });
-
-  it('should be able to choose provisioning', async () => {
-    const { saml } = ui;
-    const user = userEvent.setup();
-
-    renderAuthentication([Feature.Scim]);
-
-    await saml.createConfiguration(user);
-
-    expect(await saml.enableFirstMessage.find()).toBeInTheDocument();
-    await user.click(await saml.enableConfigButton.find());
-
-    expect(await saml.jitProvisioningButton.find()).toBeChecked();
-    expect(saml.saveScim.get()).toBeDisabled();
-
-    await user.click(saml.scimProvisioningButton.get());
-    expect(saml.saveScim.get()).toBeEnabled();
-    await user.click(saml.saveScim.get());
-    await user.click(saml.confirmProvisioningButton.get());
-
-    expect(await saml.scimProvisioningButton.find()).toBeChecked();
-    expect(await saml.saveScim.find()).toBeDisabled();
-  });
-
-  it('should not allow editions below Enterprise to select SCIM provisioning', async () => {
-    const { saml } = ui;
-    const user = userEvent.setup();
-
-    renderAuthentication();
-
-    await saml.createConfiguration(user);
-    await user.click(await saml.enableConfigButton.find());
-
-    expect(await saml.jitProvisioningButton.find()).toBeChecked();
-    expect(saml.scimProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
-  });
-});
-
-describe('Github tab', () => {
-  const { github } = ui;
-
-  it('should render an empty Github configuration', async () => {
-    renderAuthentication();
-    const user = userEvent.setup();
-    await user.click(await github.tab.find());
-    expect(await github.noGithubConfiguration.find()).toBeInTheDocument();
-  });
-
-  it('should be able to create a configuration', async () => {
-    const user = userEvent.setup();
-    renderAuthentication();
-
-    await user.click(await github.tab.find());
-    await user.click(await github.createConfigButton.find());
-
-    expect(github.saveConfigButton.get()).toBeDisabled();
-
-    await github.fillForm(user);
-    expect(github.saveConfigButton.get()).toBeEnabled();
-
-    await user.click(github.saveConfigButton.get());
-
-    expect(await github.editConfigButton.find()).toBeInTheDocument();
-  });
-
-  it('should be able to edit configuration', async () => {
-    const { github } = ui;
-    const user = userEvent.setup();
-    renderAuthentication();
-    await user.click(await github.tab.find());
-
-    await github.createConfiguration(user);
-
-    await user.click(github.editConfigButton.get());
-    await user.click(github.deleteOrg('organization1').get());
-
-    await user.click(github.saveConfigButton.get());
-
-    await user.click(await github.editConfigButton.find());
-
-    expect(github.organizations.get()).toHaveValue('');
-  });
-
-  it('should be able to enable/disable configuration', async () => {
-    const { github } = ui;
-    const user = userEvent.setup();
-    renderAuthentication();
-    await user.click(await github.tab.find());
-
-    await github.createConfiguration(user);
-
-    await user.click(await github.enableConfigButton.find());
-
-    expect(await github.disableConfigButton.find()).toBeInTheDocument();
-    await user.click(github.disableConfigButton.get());
-    await waitFor(() => expect(github.disableConfigButton.query()).not.toBeInTheDocument());
-
-    expect(await github.enableConfigButton.find()).toBeInTheDocument();
-  });
-
-  it('should not allow edtion below Enterprise to select Github provisioning', async () => {
-    const { github } = ui;
-    const user = userEvent.setup();
-
-    renderAuthentication();
-    await user.click(await github.tab.find());
-
-    await github.createConfiguration(user);
-    await user.click(await github.enableConfigButton.find());
-
-    expect(await github.jitProvisioningButton.find()).toBeChecked();
-    expect(github.githubProvisioningButton.get()).toHaveAttribute('aria-disabled', 'true');
-  });
-
-  it('should be able to choose provisioning', async () => {
-    const { github } = ui;
-    const user = userEvent.setup();
-
-    renderAuthentication([Feature.GithubProvisioning]);
-    await user.click(await github.tab.find());
-
-    await github.createConfiguration(user);
-
-    expect(await github.enableFirstMessage.find()).toBeInTheDocument();
-    await user.click(await github.enableConfigButton.find());
-
-    expect(await github.jitProvisioningButton.find()).toBeChecked();
-
-    expect(github.saveGithubProvisioning.get()).toBeDisabled();
-    await user.click(github.allowUserToSignUp.get());
-
-    expect(github.saveGithubProvisioning.get()).toBeEnabled();
-    await user.click(github.saveGithubProvisioning.get());
-
-    await waitFor(() => expect(github.saveGithubProvisioning.query()).toBeDisabled());
-
-    await user.click(github.githubProvisioningButton.get());
-
-    expect(github.saveGithubProvisioning.get()).toBeEnabled();
-    await user.click(github.saveGithubProvisioning.get());
-    await user.click(github.confirmProvisioningButton.get());
-
-    expect(await github.githubProvisioningButton.find()).toBeChecked();
-    expect(github.disableConfigButton.get()).toBeDisabled();
-    expect(github.saveGithubProvisioning.get()).toBeDisabled();
-  });
-
-  describe('Github Provisioning', () => {
-    let user: UserEvent;
-
-    beforeEach(() => {
-      jest.useFakeTimers({
-        advanceTimers: true,
-        now: new Date('2022-02-04T12:00:59Z'),
-      });
-      user = userEvent.setup();
-    });
-
-    afterEach(() => {
-      jest.runOnlyPendingTimers();
-      jest.useRealTimers();
-    });
-
-    it('should display a success status when the synchronisation is a success', async () => {
-      handler.addProvisioningTask({
-        status: TaskStatuses.Success,
-        executedAt: '2022-02-03T11:45:35+0200',
-      });
-
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableProvisioning(user);
-      expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
-      expect(github.syncSummary.get()).toBeInTheDocument();
-    });
-
-    it('should display a success status even when another task is pending', async () => {
-      handler.addProvisioningTask({
-        status: TaskStatuses.Pending,
-        executedAt: '2022-02-03T11:55:35+0200',
-      });
-      handler.addProvisioningTask({
-        status: TaskStatuses.Success,
-        executedAt: '2022-02-03T11:45:35+0200',
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableProvisioning(user);
-      expect(github.githubProvisioningSuccess.get()).toBeInTheDocument();
-      expect(github.githubProvisioningPending.get()).toBeInTheDocument();
-    });
-
-    it('should display an error alert when the synchronisation failed', async () => {
-      handler.addProvisioningTask({
-        status: TaskStatuses.Failed,
-        executedAt: '2022-02-03T11:45:35+0200',
-        errorMessage: "T'es mauvais Jacques",
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableProvisioning(user);
-      expect(github.githubProvisioningAlert.get()).toBeInTheDocument();
-      expect(github.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
-      expect(github.githubProvisioningSuccess.query()).not.toBeInTheDocument();
-    });
-
-    it('should display an error alert even when another task is in progress', async () => {
-      handler.addProvisioningTask({
-        status: TaskStatuses.InProgress,
-        executedAt: '2022-02-03T11:55:35+0200',
-      });
-      handler.addProvisioningTask({
-        status: TaskStatuses.Failed,
-        executedAt: '2022-02-03T11:45:35+0200',
-        errorMessage: "T'es mauvais Jacques",
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableProvisioning(user);
-      expect(github.githubProvisioningAlert.get()).toBeInTheDocument();
-      expect(github.githubProvisioningButton.get()).toHaveTextContent("T'es mauvais Jacques");
-      expect(github.githubProvisioningSuccess.query()).not.toBeInTheDocument();
-      expect(github.githubProvisioningInProgress.get()).toBeInTheDocument();
-    });
-
-    it('should display that config is valid for both provisioning with 1 org', async () => {
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
-    });
-
-    it('should display that config is valid for both provisioning with multiple orgs', async () => {
-      handler.setConfigurationValidity({
-        installations: [
-          {
-            organization: 'org1',
-            autoProvisioning: { status: GitHubProvisioningStatus.Success },
-            jit: { status: GitHubProvisioningStatus.Success },
-          },
-          {
-            organization: 'org2',
-            autoProvisioning: { status: GitHubProvisioningStatus.Success },
-            jit: { status: GitHubProvisioningStatus.Success },
-          },
-        ],
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
-      expect(github.configurationValiditySuccess.get()).toHaveTextContent('2');
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getConfigDetailsTitle()).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_label',
-      );
-      expect(github.getOrgs()[0]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_labelorg1',
-      );
-      expect(github.getOrgs()[1]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_labelorg2',
-      );
-    });
-
-    it('should display that config is invalid for autoprovisioning if some apps are suspended but valid for jit', async () => {
-      const errorMessage = 'Installation suspended';
-      handler.setConfigurationValidity({
-        installations: [
-          {
-            organization: 'org1',
-            autoProvisioning: {
-              status: GitHubProvisioningStatus.Failed,
-              errorMessage,
-            },
-            jit: {
-              status: GitHubProvisioningStatus.Failed,
-              errorMessage,
-            },
-          },
-        ],
-      });
-
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValidityWarning.get()).toBeInTheDocument());
-      expect(github.configurationValidityWarning.get()).toHaveTextContent(errorMessage);
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getConfigDetailsTitle()).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_label',
-      );
-      expect(github.getOrgs()[0]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.invalid_labelorg1 - Installation suspended',
-      );
-
-      await user.click(github.configDetailsDialog.byRole('button', { name: 'close' }).get());
-
-      await user.click(github.githubProvisioningButton.get());
-      await waitFor(() => expect(github.configurationValidityError.get()).toBeInTheDocument());
-      expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
-    });
-
-    it('should display that config is valid but some organizations were not found', async () => {
-      handler.setConfigurationValidity({
-        installations: [
-          {
-            organization: 'org1',
-            autoProvisioning: { status: GitHubProvisioningStatus.Success },
-            jit: { status: GitHubProvisioningStatus.Success },
-          },
-        ],
-      });
-
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValiditySuccess.get()).toBeInTheDocument());
-      expect(github.configurationValiditySuccess.get()).toHaveTextContent('1');
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getConfigDetailsTitle()).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_label',
-      );
-      expect(github.getOrgs()[0]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_labelorg1',
-      );
-      expect(github.getOrgs()[1]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.org_not_found.organization1',
-      );
-    });
-
-    it('should display that config is invalid', async () => {
-      const errorMessage = 'Test error';
-      handler.setConfigurationValidity({
-        application: {
-          jit: {
-            status: GitHubProvisioningStatus.Failed,
-            errorMessage,
-          },
-          autoProvisioning: {
-            status: GitHubProvisioningStatus.Failed,
-            errorMessage,
-          },
-        },
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValidityError.query()).toBeInTheDocument());
-      expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getConfigDetailsTitle()).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.invalid_label',
-      );
-      expect(github.configDetailsDialog.get()).toHaveTextContent(errorMessage);
-    });
-
-    it('should display that config is valid for jit, but not for auto', async () => {
-      const errorMessage = 'Test error';
-      handler.setConfigurationValidity({
-        application: {
-          jit: {
-            status: GitHubProvisioningStatus.Success,
-          },
-          autoProvisioning: {
-            status: GitHubProvisioningStatus.Failed,
-            errorMessage,
-          },
-        },
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
-      expect(github.configurationValiditySuccess.get()).not.toHaveTextContent(errorMessage);
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getConfigDetailsTitle()).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_label',
-      );
-      await user.click(github.configDetailsDialog.byRole('button', { name: 'close' }).get());
-
-      await user.click(github.githubProvisioningButton.get());
-
-      expect(github.configurationValidityError.get()).toBeInTheDocument();
-      expect(github.configurationValidityError.get()).toHaveTextContent(errorMessage);
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getConfigDetailsTitle()).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.invalid_label',
-      );
-    });
-
-    it('should display that config is invalid because of orgs', async () => {
-      const errorMessage = 'Test error';
-      handler.setConfigurationValidity({
-        installations: [
-          {
-            organization: 'org1',
-            autoProvisioning: { status: GitHubProvisioningStatus.Success },
-            jit: { status: GitHubProvisioningStatus.Success },
-          },
-          {
-            organization: 'org2',
-            jit: { status: GitHubProvisioningStatus.Failed, errorMessage },
-            autoProvisioning: { status: GitHubProvisioningStatus.Failed, errorMessage },
-          },
-        ],
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-
-      await appLoaded();
-
-      await waitFor(() => expect(github.configurationValiditySuccess.query()).toBeInTheDocument());
-
-      await user.click(github.viewConfigValidityDetailsButton.get());
-
-      expect(github.getOrgs()[0]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.valid_labelorg1',
-      );
-      expect(github.getOrgs()[1]).toHaveTextContent(
-        'settings.authentication.github.configuration.validation.details.invalid_labelorg2 - Test error',
-      );
-
-      await user.click(github.configDetailsDialog.byRole('button', { name: 'close' }).get());
-
-      await user.click(github.githubProvisioningButton.get());
-
-      expect(github.configurationValidityError.get()).toBeInTheDocument();
-      expect(github.configurationValidityError.get()).toHaveTextContent(
-        `settings.authentication.github.configuration.validation.invalid_org.org2.${errorMessage}`,
-      );
-      await user.click(github.viewConfigValidityDetailsButton.get());
-      expect(github.getOrgs()[1]).toHaveTextContent(
-        `settings.authentication.github.configuration.validation.details.invalid_labelorg2 - ${errorMessage}`,
-      );
-    });
-
-    it('should update provisioning validity after clicking Test Configuration', async () => {
-      const errorMessage = 'Test error';
-      handler.setConfigurationValidity({
-        application: {
-          jit: {
-            status: GitHubProvisioningStatus.Failed,
-            errorMessage,
-          },
-          autoProvisioning: {
-            status: GitHubProvisioningStatus.Failed,
-            errorMessage,
-          },
-        },
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableConfiguration(user);
-      handler.setConfigurationValidity({
-        application: {
-          jit: {
-            status: GitHubProvisioningStatus.Success,
-          },
-          autoProvisioning: {
-            status: GitHubProvisioningStatus.Success,
-          },
-        },
-      });
-
-      await appLoaded();
-
-      expect(await github.configurationValidityError.find()).toBeInTheDocument();
-
-      await user.click(github.checkConfigButton.get());
-
-      expect(github.configurationValiditySuccess.get()).toBeInTheDocument();
-      expect(github.configurationValidityError.query()).not.toBeInTheDocument();
-    });
-
-    it('should show warning', async () => {
-      handler.addProvisioningTask({
-        status: TaskStatuses.Success,
-        warnings: ['Warning'],
-      });
-      renderAuthentication([Feature.GithubProvisioning]);
-      await github.enableProvisioning(user);
-
-      expect(await github.syncWarning.find()).toBeInTheDocument();
-      expect(github.syncSummary.get()).toBeInTheDocument();
-    });
-
-    it('should display a modal if user was already using auto and continue using auto provisioning', async () => {
-      const user = userEvent.setup();
-      settingsHandler.presetGithubAutoProvisioning();
-      handler.enableGithubProvisioning();
-      settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
-      renderAuthentication([Feature.GithubProvisioning]);
-
-      await user.click(await github.tab.find());
-
-      expect(await github.consentDialog.find()).toBeInTheDocument();
-      await user.click(github.continueAutoButton.get());
-
-      expect(await github.githubProvisioningButton.find()).toBeChecked();
-      expect(github.consentDialog.query()).not.toBeInTheDocument();
-    });
-
-    it('should display a modal if user was already using auto and switch to JIT', async () => {
-      const user = userEvent.setup();
-      settingsHandler.presetGithubAutoProvisioning();
-      handler.enableGithubProvisioning();
-      settingsHandler.set('sonar.auth.github.userConsentForPermissionProvisioningRequired', '');
-      renderAuthentication([Feature.GithubProvisioning]);
-
-      await user.click(await github.tab.find());
-
-      expect(await github.consentDialog.find()).toBeInTheDocument();
-      await user.click(github.switchJitButton.get());
-
-      expect(await github.jitProvisioningButton.find()).toBeChecked();
-      expect(github.consentDialog.query()).not.toBeInTheDocument();
-    });
-
-    it('should sort mapping rows', async () => {
-      const user = userEvent.setup();
-      settingsHandler.presetGithubAutoProvisioning();
-      handler.enableGithubProvisioning();
-      renderAuthentication([Feature.GithubProvisioning]);
-      await user.click(await github.tab.find());
-
-      expect(await github.editMappingButton.find()).toBeInTheDocument();
-      await user.click(github.editMappingButton.get());
-
-      const rows = (await github.mappingRow.findAll()).filter(
-        (row) => within(row).queryAllByRole('checkbox').length > 0,
-      );
-
-      expect(rows).toHaveLength(5);
-
-      expect(rows[0]).toHaveTextContent('read');
-      expect(rows[1]).toHaveTextContent('triage');
-      expect(rows[2]).toHaveTextContent('write');
-      expect(rows[3]).toHaveTextContent('maintain');
-      expect(rows[4]).toHaveTextContent('admin');
-    });
-
-    it('should apply new mapping and new provisioning type at the same time', async () => {
-      const user = userEvent.setup();
-      renderAuthentication([Feature.GithubProvisioning]);
-      await user.click(await github.tab.find());
-
-      await github.createConfiguration(user);
-      await user.click(await github.enableConfigButton.find());
-
-      expect(await github.jitProvisioningButton.find()).toBeChecked();
-      expect(github.editMappingButton.query()).not.toBeInTheDocument();
-      await user.click(github.githubProvisioningButton.get());
-      expect(await github.editMappingButton.find()).toBeInTheDocument();
-      await user.click(github.editMappingButton.get());
-
-      expect(await github.mappingRow.findAll()).toHaveLength(7);
-
-      let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'));
-      let adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin'));
-
-      expect(readCheckboxes[0]).toBeChecked();
-      expect(readCheckboxes[5]).not.toBeChecked();
-      expect(adminCheckboxes[5]).toBeChecked();
-
-      await user.click(readCheckboxes[0]);
-      await user.click(readCheckboxes[5]);
-      await user.click(adminCheckboxes[5]);
-      await user.click(github.mappingDialogClose.get());
-
-      await user.click(github.saveGithubProvisioning.get());
-      await user.click(github.confirmProvisioningButton.get());
-
-      // Clean local mapping state
-      await user.click(github.jitProvisioningButton.get());
-      await user.click(github.githubProvisioningButton.get());
-
-      await user.click(github.editMappingButton.get());
-      readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'));
-      adminCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('admin'));
-
-      expect(readCheckboxes[0]).not.toBeChecked();
-      expect(readCheckboxes[5]).toBeChecked();
-      expect(adminCheckboxes[5]).not.toBeChecked();
-      await user.click(github.mappingDialogClose.get());
-    });
-
-    it('should apply new mapping on auto-provisioning', async () => {
-      const user = userEvent.setup();
-      settingsHandler.presetGithubAutoProvisioning();
-      handler.enableGithubProvisioning();
-      renderAuthentication([Feature.GithubProvisioning]);
-      await user.click(await github.tab.find());
-
-      expect(await github.saveGithubProvisioning.find()).toBeDisabled();
-      await user.click(github.editMappingButton.get());
-
-      expect(await github.mappingRow.findAll()).toHaveLength(7);
-
-      let readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0];
-
-      expect(readCheckboxes).toBeChecked();
-
-      await user.click(readCheckboxes);
-      await user.click(github.mappingDialogClose.get());
-
-      expect(await github.saveGithubProvisioning.find()).toBeEnabled();
-
-      await user.click(github.saveGithubProvisioning.get());
-
-      // Clean local mapping state
-      await user.click(github.jitProvisioningButton.get());
-      await user.click(github.githubProvisioningButton.get());
-
-      await user.click(github.editMappingButton.get());
-      readCheckboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('read'))[0];
-
-      expect(readCheckboxes).not.toBeChecked();
-      await user.click(github.mappingDialogClose.get());
-    });
-
-    it('should add/remove/update custom roles', async () => {
-      const user = userEvent.setup();
-      settingsHandler.presetGithubAutoProvisioning();
-      handler.enableGithubProvisioning();
-      handler.addGitHubCustomRole('custom1', ['user', 'codeViewer', 'scan']);
-      handler.addGitHubCustomRole('custom2', ['user', 'codeViewer', 'issueAdmin', 'scan']);
-      renderAuthentication([Feature.GithubProvisioning]);
-      await user.click(await github.tab.find());
-
-      expect(await github.saveGithubProvisioning.find()).toBeDisabled();
-      await user.click(github.editMappingButton.get());
-
-      const rows = (await github.mappingRow.findAll()).filter(
-        (row) => within(row).queryAllByRole('checkbox').length > 0,
-      );
-
-      expect(rows).toHaveLength(7);
-
-      let custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1'));
-
-      expect(custom1Checkboxes[0]).toBeChecked();
-      expect(custom1Checkboxes[1]).toBeChecked();
-      expect(custom1Checkboxes[2]).not.toBeChecked();
-      expect(custom1Checkboxes[3]).not.toBeChecked();
-      expect(custom1Checkboxes[4]).not.toBeChecked();
-      expect(custom1Checkboxes[5]).toBeChecked();
-
-      await user.click(custom1Checkboxes[1]);
-      await user.click(custom1Checkboxes[2]);
-
-      await user.click(github.deleteCustomRoleCustom2.get());
-
-      expect(github.customRoleInput.get()).toHaveValue('');
-      await user.type(github.customRoleInput.get(), 'read');
-      await user.click(github.customRoleAddBtn.get());
-      expect(await github.roleExistsError.find()).toBeInTheDocument();
-      expect(github.customRoleAddBtn.get()).toBeDisabled();
-      await user.clear(github.customRoleInput.get());
-      expect(github.roleExistsError.query()).not.toBeInTheDocument();
-      await user.type(github.customRoleInput.get(), 'custom1');
-      await user.click(github.customRoleAddBtn.get());
-      expect(await github.roleExistsError.find()).toBeInTheDocument();
-      expect(github.customRoleAddBtn.get()).toBeDisabled();
-      await user.clear(github.customRoleInput.get());
-      await user.type(github.customRoleInput.get(), 'custom3');
-      expect(github.roleExistsError.query()).not.toBeInTheDocument();
-      expect(github.customRoleAddBtn.get()).toBeEnabled();
-      await user.click(github.customRoleAddBtn.get());
-
-      let custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3'));
-      expect(custom3Checkboxes[0]).toBeChecked();
-      expect(custom3Checkboxes[1]).not.toBeChecked();
-      expect(custom3Checkboxes[2]).not.toBeChecked();
-      expect(custom3Checkboxes[3]).not.toBeChecked();
-      expect(custom3Checkboxes[4]).not.toBeChecked();
-      expect(custom3Checkboxes[5]).not.toBeChecked();
-      await user.click(custom3Checkboxes[0]);
-      expect(await github.emptyRoleError.find()).toBeInTheDocument();
-      expect(github.mappingDialogClose.get()).toBeDisabled();
-      await user.click(custom3Checkboxes[1]);
-      expect(github.emptyRoleError.query()).not.toBeInTheDocument();
-      expect(github.mappingDialogClose.get()).toBeEnabled();
-      await user.click(github.mappingDialogClose.get());
-
-      expect(await github.saveGithubProvisioning.find()).toBeEnabled();
-      await user.click(github.saveGithubProvisioning.get());
-
-      // Clean local mapping state
-      await user.click(github.jitProvisioningButton.get());
-      await user.click(github.githubProvisioningButton.get());
-
-      await user.click(github.editMappingButton.get());
-
-      expect(
-        (await github.mappingRow.findAll()).filter(
-          (row) => within(row).queryAllByRole('checkbox').length > 0,
-        ),
-      ).toHaveLength(7);
-      custom1Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom1'));
-      custom3Checkboxes = github.mappingCheckbox.getAll(github.getMappingRowByRole('custom3'));
-      expect(github.getMappingRowByRole('custom2')).toBeUndefined();
-      expect(custom1Checkboxes[0]).toBeChecked();
-      expect(custom1Checkboxes[1]).not.toBeChecked();
-      expect(custom1Checkboxes[2]).toBeChecked();
-      expect(custom1Checkboxes[3]).not.toBeChecked();
-      expect(custom1Checkboxes[4]).not.toBeChecked();
-      expect(custom1Checkboxes[5]).toBeChecked();
-      expect(custom3Checkboxes[0]).not.toBeChecked();
-      expect(custom3Checkboxes[1]).toBeChecked();
-      expect(custom3Checkboxes[2]).not.toBeChecked();
-      expect(custom3Checkboxes[3]).not.toBeChecked();
-      expect(custom3Checkboxes[4]).not.toBeChecked();
-      expect(custom3Checkboxes[5]).not.toBeChecked();
-      await user.click(github.mappingDialogClose.get());
-    });
-  });
-});
-
-describe('GitLab', () => {
-  const { gitlab } = ui;
-
-  it('should create a Gitlab configuration and disable it', async () => {
-    handler.setGitlabConfigurations([]);
-    renderAuthentication();
-    const user = userEvent.setup();
-    await user.click(await gitlab.tab.find());
-
-    expect(await gitlab.noGitlabConfiguration.find()).toBeInTheDocument();
-    expect(gitlab.createConfigButton.get()).toBeInTheDocument();
-
-    await user.click(gitlab.createConfigButton.get());
-    expect(await gitlab.createDialog.find()).toBeInTheDocument();
-    await user.type(gitlab.applicationId.get(), '123');
-    await user.type(gitlab.url.get(), 'https://company.gitlab.com');
-    await user.type(gitlab.clientSecret.get(), '123');
-    await user.click(gitlab.synchronizeUserGroups.get());
-    await user.click(gitlab.saveConfigButton.get());
-
-    expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
-    expect(gitlab.noGitlabConfiguration.query()).not.toBeInTheDocument();
-    expect(glContainer.get()).toHaveTextContent('https://company.gitlab.com');
-
-    expect(gitlab.disableConfigButton.get()).toBeInTheDocument();
-    await user.click(gitlab.disableConfigButton.get());
-    expect(gitlab.enableConfigButton.get()).toBeInTheDocument();
-    expect(gitlab.disableConfigButton.query()).not.toBeInTheDocument();
-  });
-
-  it('should edit/delete configuration', async () => {
-    const user = userEvent.setup();
-    renderAuthentication();
-    await user.click(await gitlab.tab.find());
-
-    expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
-    expect(glContainer.get()).toHaveTextContent('URL');
-    expect(gitlab.disableConfigButton.get()).toBeInTheDocument();
-    expect(gitlab.deleteConfigButton.get()).toBeInTheDocument();
-    expect(gitlab.deleteConfigButton.get()).toBeDisabled();
-
-    await user.click(gitlab.editConfigButton.get());
-    expect(await gitlab.editDialog.find()).toBeInTheDocument();
-    expect(gitlab.url.get()).toHaveValue('URL');
-    expect(gitlab.applicationId.query()).not.toBeInTheDocument();
-    expect(gitlab.clientSecret.query()).not.toBeInTheDocument();
-    expect(gitlab.synchronizeUserGroups.get()).toBeChecked();
-    await user.clear(gitlab.url.get());
-    await user.type(gitlab.url.get(), 'https://company.gitlab.com');
-    await user.click(gitlab.saveConfigButton.get());
-
-    expect(glContainer.get()).not.toHaveTextContent('URL');
-    expect(glContainer.get()).toHaveTextContent('https://company.gitlab.com');
-
-    expect(gitlab.disableConfigButton.get()).toBeInTheDocument();
-    await user.click(gitlab.disableConfigButton.get());
-    expect(await gitlab.enableConfigButton.find()).toBeInTheDocument();
-    expect(gitlab.deleteConfigButton.get()).toBeEnabled();
-    await user.click(gitlab.deleteConfigButton.get());
-    expect(await gitlab.noGitlabConfiguration.find()).toBeInTheDocument();
-    expect(gitlab.editConfigButton.query()).not.toBeInTheDocument();
-  });
-
-  it('should change from just-in-time to Auto Provisioning with proper validation', async () => {
-    const user = userEvent.setup();
-    renderAuthentication([Feature.GitlabProvisioning]);
-    await user.click(await gitlab.tab.find());
-
-    expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
-    expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
-
-    user.click(gitlab.autoProvisioningRadioButton.get());
-    expect(await gitlab.autoProvisioningRadioButton.find()).toBeEnabled();
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-
-    await user.type(gitlab.autoProvisioningToken.get(), 'JRR Tolkien');
-    expect(await gitlab.saveProvisioning.find()).toBeDisabled();
-
-    await user.type(gitlab.autoProvisioningGroupsInput.get(), 'NWA');
-    user.click(gitlab.autoProvisioningRadioButton.get());
-    expect(await gitlab.saveProvisioning.find()).toBeEnabled();
-
-    await user.click(gitlab.removeProvisioniongGroup.get());
-    expect(await gitlab.saveProvisioning.find()).toBeDisabled();
-    await user.type(gitlab.autoProvisioningGroupsInput.get(), 'Wu-Tang Clan');
-    expect(await gitlab.saveProvisioning.find()).toBeEnabled();
-
-    await user.clear(gitlab.autoProvisioningToken.get());
-    expect(await gitlab.saveProvisioning.find()).toBeDisabled();
-    await user.type(gitlab.autoProvisioningToken.get(), 'tiktoken');
-    expect(await gitlab.saveProvisioning.find()).toBeEnabled();
-
-    await user.click(gitlab.saveProvisioning.get());
-    expect(gitlab.confirmAutoProvisioningDialog.get()).toBeInTheDocument();
-    await user.click(gitlab.confirmProvisioningChange.get());
-    expect(gitlab.confirmAutoProvisioningDialog.query()).not.toBeInTheDocument();
-
-    expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
-    expect(await gitlab.saveProvisioning.find()).toBeDisabled();
-  });
-
-  it('should change from auto provisioning to JIT with proper validation', async () => {
-    handler.setGitlabConfigurations([
-      mockGitlabConfiguration({
-        allowUsersToSignUp: false,
-        enabled: true,
-        type: ProvisioningType.auto,
-        groups: ['D12'],
-      }),
-    ]);
-    const user = userEvent.setup();
-    renderAuthentication([Feature.GitlabProvisioning]);
-    await user.click(await gitlab.tab.find());
-
-    expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
-
-    expect(gitlab.jitProvisioningRadioButton.get()).not.toBeChecked();
-    expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
-    expect(gitlab.autoProvisioningGroupsInput.get()).toHaveValue('D12');
-
-    expect(gitlab.autoProvisioningToken.query()).not.toBeInTheDocument();
-    expect(gitlab.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
-
-    await user.click(gitlab.jitProvisioningRadioButton.get());
-    expect(await gitlab.jitProvisioningRadioButton.find()).toBeChecked();
-
-    expect(await gitlab.saveProvisioning.find()).toBeEnabled();
-
-    expect(gitlab.jitAllowUsersToSignUpToggle.get()).toBeInTheDocument();
-
-    await user.click(gitlab.saveProvisioning.get());
-    expect(gitlab.confirmJitProvisioningDialog.get()).toBeInTheDocument();
-    await user.click(gitlab.confirmProvisioningChange.get());
-    expect(gitlab.confirmJitProvisioningDialog.query()).not.toBeInTheDocument();
-
-    expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
-    expect(await gitlab.saveProvisioning.find()).toBeDisabled();
-  });
-
-  it('should be able to allow user to sign up for JIT with proper validation', async () => {
-    handler.setGitlabConfigurations([
-      mockGitlabConfiguration({
-        allowUsersToSignUp: false,
-        enabled: true,
-        type: ProvisioningType.jit,
-      }),
-    ]);
-    const user = userEvent.setup();
-    renderAuthentication([Feature.GitlabProvisioning]);
-    await user.click(await gitlab.tab.find());
-
-    expect(await gitlab.editConfigButton.find()).toBeInTheDocument();
-
-    expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
-    expect(gitlab.autoProvisioningRadioButton.get()).not.toBeChecked();
-
-    expect(gitlab.jitAllowUsersToSignUpToggle.get()).not.toBeChecked();
-
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-    await user.click(gitlab.jitAllowUsersToSignUpToggle.get());
-    expect(gitlab.saveProvisioning.get()).toBeEnabled();
-    await user.click(gitlab.jitAllowUsersToSignUpToggle.get());
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-    await user.click(gitlab.jitAllowUsersToSignUpToggle.get());
-
-    await user.click(gitlab.saveProvisioning.get());
-
-    expect(gitlab.jitProvisioningRadioButton.get()).toBeChecked();
-    expect(gitlab.jitAllowUsersToSignUpToggle.get()).toBeChecked();
-    expect(await gitlab.saveProvisioning.find()).toBeDisabled();
-  });
-
-  it('should be able to edit groups and token for Auto provisioning with proper validation', async () => {
-    handler.setGitlabConfigurations([
-      mockGitlabConfiguration({
-        allowUsersToSignUp: false,
-        enabled: true,
-        type: ProvisioningType.auto,
-        groups: ['Cypress Hill', 'Public Enemy'],
-      }),
-    ]);
-    const user = userEvent.setup();
-    renderAuthentication([Feature.GitlabProvisioning]);
-    await user.click(await gitlab.tab.find());
-
-    expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
-    expect(gitlab.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
-    expect(gitlab.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
-
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-
-    // Changing the Provisioning token should enable save
-    await user.click(gitlab.autoProvisioningUpdateTokenButton.get());
-    await user.type(gitlab.autoProvisioningGroupsInput.get(), 'Tok Token!');
-    expect(gitlab.saveProvisioning.get()).toBeEnabled();
-    await user.click(gitlab.cancelProvisioningChanges.get());
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-
-    // Adding a group should enable save
-    await user.click(gitlab.autoProvisioningGroupsInput.get());
-    await user.tab();
-    await user.tab();
-    await user.tab();
-    await user.tab();
-    await user.keyboard('Run DMC');
-    expect(gitlab.saveProvisioning.get()).toBeEnabled();
-    await user.tab();
-    await user.keyboard('{Enter}');
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-
-    // Removing a group should enable save
-    await user.click(gitlab.autoProvisioningGroupsInput.get());
-    await user.tab();
-    await user.keyboard('{Enter}');
-    expect(gitlab.saveProvisioning.get()).toBeEnabled();
-
-    // Removing all groups should disable save
-    await user.click(gitlab.autoProvisioningGroupsInput.get());
-    await user.tab();
-    await user.keyboard('{Enter}');
-    expect(gitlab.saveProvisioning.get()).toBeDisabled();
-  });
-
-  it('should be able to reset Auto Provisioning changes', async () => {
-    handler.setGitlabConfigurations([
-      mockGitlabConfiguration({
-        allowUsersToSignUp: false,
-        enabled: true,
-        type: ProvisioningType.auto,
-        groups: ['Cypress Hill', 'Public Enemy'],
-      }),
-    ]);
-    const user = userEvent.setup();
-    renderAuthentication([Feature.GitlabProvisioning]);
-    await user.click(await gitlab.tab.find());
-
-    expect(gitlab.autoProvisioningRadioButton.get()).toBeChecked();
-
-    // Cancel doesn't fully work yet as the AuthenticationFormField needs to be worked on
-    await user.click(gitlab.autoProvisioningGroupsInput.get());
-    await user.tab();
-    await user.tab();
-    await user.tab();
-    await user.tab();
-    await user.keyboard('A Tribe Called Quest');
-    await user.click(gitlab.autoProvisioningGroupsInput.get());
-    await user.tab();
-    await user.keyboard('{Enter}');
-    await user.click(gitlab.autoProvisioningUpdateTokenButton.get());
-    await user.type(gitlab.autoProvisioningGroupsInput.get(), 'ToToken!');
-    expect(gitlab.saveProvisioning.get()).toBeEnabled();
-    await user.click(gitlab.cancelProvisioningChanges.get());
-    // expect(gitlab.autoProvisioningUpdateTokenButton.get()).toBeInTheDocument();
-    expect(gitlab.autoProvisioningGroupsInput.get()).toHaveValue('Cypress Hill');
-  });
-
-  describe('Gitlab Provisioning', () => {
-    beforeEach(() => {
-      jest.useFakeTimers({
-        advanceTimers: true,
-        now: new Date('2022-02-04T12:00:59Z'),
-      });
-      handler.setGitlabConfigurations([
-        mockGitlabConfiguration({
-          id: '1',
-          enabled: true,
-          type: ProvisioningType.auto,
-          groups: ['Test'],
-        }),
-      ]);
-    });
-
-    afterEach(() => {
-      jest.runOnlyPendingTimers();
-      jest.useRealTimers();
-    });
-
-    it('should display a success status when the synchronisation is a success', async () => {
-      computeEngineHandler.addTask({
-        status: TaskStatuses.Success,
-        executedAt: '2022-02-03T11:45:35+0200',
-        infoMessages: ['Test summary'],
-        type: TaskTypes.GitlabProvisioning,
-      });
-
-      renderAuthentication([Feature.GitlabProvisioning]);
-      expect(await gitlab.gitlabProvisioningSuccess.find()).toBeInTheDocument();
-      expect(gitlab.syncSummary.get()).toBeInTheDocument();
-    });
-
-    it('should display a success status even when another task is pending', async () => {
-      computeEngineHandler.addTask({
-        status: TaskStatuses.Pending,
-        executedAt: '2022-02-03T11:55:35+0200',
-        type: TaskTypes.GitlabProvisioning,
-      });
-      computeEngineHandler.addTask({
-        status: TaskStatuses.Success,
-        executedAt: '2022-02-03T11:45:35+0200',
-        type: TaskTypes.GitlabProvisioning,
-      });
-      renderAuthentication([Feature.GitlabProvisioning]);
-      expect(await gitlab.gitlabProvisioningSuccess.find()).toBeInTheDocument();
-      expect(gitlab.gitlabProvisioningPending.get()).toBeInTheDocument();
-    });
-
-    it('should display an error alert when the synchronisation failed', async () => {
-      computeEngineHandler.addTask({
-        status: TaskStatuses.Failed,
-        executedAt: '2022-02-03T11:45:35+0200',
-        errorMessage: "T'es mauvais Jacques",
-        type: TaskTypes.GitlabProvisioning,
-      });
-      renderAuthentication([Feature.GitlabProvisioning]);
-      expect(await gitlab.gitlabProvisioningAlert.find()).toBeInTheDocument();
-      expect(gitlab.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
-      expect(gitlab.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
-    });
-
-    it('should display an error alert even when another task is in progress', async () => {
-      computeEngineHandler.addTask({
-        status: TaskStatuses.InProgress,
-        executedAt: '2022-02-03T11:55:35+0200',
-        type: TaskTypes.GitlabProvisioning,
-      });
-      computeEngineHandler.addTask({
-        status: TaskStatuses.Failed,
-        executedAt: '2022-02-03T11:45:35+0200',
-        errorMessage: "T'es mauvais Jacques",
-        type: TaskTypes.GitlabProvisioning,
-      });
-      renderAuthentication([Feature.GitlabProvisioning]);
-      expect(await gitlab.gitlabProvisioningAlert.find()).toBeInTheDocument();
-      expect(gitlab.autoProvisioningRadioButton.get()).toHaveTextContent("T'es mauvais Jacques");
-      expect(gitlab.gitlabProvisioningSuccess.query()).not.toBeInTheDocument();
-      expect(gitlab.gitlabProvisioningInProgress.get()).toBeInTheDocument();
-    });
-
-    it('should show warning', async () => {
-      computeEngineHandler.addTask({
-        status: TaskStatuses.Success,
-        warnings: ['Warning'],
-        infoMessages: ['Test summary'],
-        type: TaskTypes.GitlabProvisioning,
-      });
-      renderAuthentication([Feature.GitlabProvisioning]);
-
-      expect(await gitlab.syncWarning.find()).toBeInTheDocument();
-      expect(gitlab.syncSummary.get()).toBeInTheDocument();
-    });
-  });
-});
-
-const appLoaded = async () => {
-  await waitFor(async () => {
-    expect(await screen.findByText('loading')).not.toBeInTheDocument();
-  });
-};
-
 function renderAuthentication(features: Feature[] = []) {
   renderComponent(
     <AvailableFeaturesContext.Provider value={features}>
index 4d961f61ff99b5a33f8d28b570abe8189a2df39f..6bef14fdc65ef872943223d864fca0167d50cbe4 100644 (file)
@@ -24,7 +24,7 @@ import {
   useGithubProvisioningEnabledQuery,
   useGithubRolesMappingMutation,
   useToggleGithubProvisioningMutation,
-} from '../../../../../queries/identity-provider';
+} from '../../../../../queries/identity-provider/github';
 import { useSaveValueMutation, useSaveValuesMutation } from '../../../../../queries/settings';
 import { Feature } from '../../../../../types/features';
 import { GitHubMapping } from '../../../../../types/provisioning';
index cf720362be133ed44f96b5b340d75ef72f054f36..da7e0c02366eeace1655c44c9b02094c73fae125 100644 (file)
@@ -19,7 +19,7 @@
  */
 import React from 'react';
 import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
-import { useScimStatusQuery } from '../../../../../queries/identity-provider';
+import { useScimStatusQuery } from '../../../../../queries/identity-provider/scim';
 import { Feature } from '../../../../../types/features';
 import { ExtendedSettingDefinition } from '../../../../../types/settings';
 import useConfiguration from './useConfiguration';
index 70b7cfc9ca682efb67e39025d0db7f6ab73194c3..fdb63224c4c9b744351f37b5f70ba3a28ace4fb8 100644 (file)
@@ -32,7 +32,7 @@ import Suggestions from '../../components/embed-docs-modal/Suggestions';
 import Spinner from '../../components/ui/Spinner';
 import { now, toISO8601WithOffsetString } from '../../helpers/dates';
 import { translate } from '../../helpers/l10n';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
 import { useUsersQueries } from '../../queries/users';
 import { IdentityProvider, Provider } from '../../types/types';
 import { RestUserDetailed } from '../../types/users';
index 6fb3a709866318077c7de1f1f230aa5305c5ce88..fd36eb24e3b29de8ce33bf6cc5fffc9053b0d394 100644 (file)
@@ -22,8 +22,8 @@ import { screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
 import selectEvent from 'react-select-event';
-import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
 import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
+import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
 import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
 import UserTokensMock from '../../../api/mocks/UserTokensMock';
@@ -42,7 +42,7 @@ const tokenHandler = new UserTokensMock();
 const systemHandler = new SystemServiceMock();
 const componentsHandler = new ComponentsServiceMock();
 const settingsHandler = new SettingsServiceMock();
-const authenticationHandler = new AuthenticationServiceMock();
+const githubHandler = new GithubProvisioningServiceMock();
 
 const ui = {
   createUserButton: byRole('button', { name: 'users.create_user' }),
@@ -142,7 +142,7 @@ beforeEach(() => {
   componentsHandler.reset();
   settingsHandler.reset();
   systemHandler.reset();
-  authenticationHandler.reset();
+  githubHandler.reset();
 });
 
 describe('different filters combinations', () => {
@@ -592,11 +592,11 @@ describe('in manage mode', () => {
 
   describe('Github Provisioning', () => {
     beforeEach(() => {
-      authenticationHandler.handleActivateGithubProvisioning();
+      githubHandler.handleActivateGithubProvisioning();
     });
 
     it('should display a success status when the synchronisation is a success', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Success,
         executedAt: '2022-02-03T11:45:35+0200',
       });
@@ -605,11 +605,11 @@ describe('in manage mode', () => {
     });
 
     it('should display a success status even when another task is pending', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Pending,
         executedAt: '2022-02-03T11:55:35+0200',
       });
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Success,
         executedAt: '2022-02-03T11:45:35+0200',
       });
@@ -619,7 +619,7 @@ describe('in manage mode', () => {
     });
 
     it('should display an error alert when the synchronisation failed', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Failed,
         executedAt: '2022-02-03T11:45:35+0200',
         errorMessage: 'Error Message',
@@ -631,11 +631,11 @@ describe('in manage mode', () => {
     });
 
     it('should display an error alert even when another task is in progress', async () => {
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.InProgress,
         executedAt: '2022-02-03T11:55:35+0200',
       });
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Failed,
         executedAt: '2022-02-03T11:45:35+0200',
         errorMessage: 'Error Message',
@@ -649,7 +649,7 @@ describe('in manage mode', () => {
 
     it('should display an warning alert', async () => {
       const warningMessage = 'Very long warning about something that user is not interested in';
-      authenticationHandler.addProvisioningTask({
+      githubHandler.addProvisioningTask({
         status: TaskStatuses.Success,
         warnings: [warningMessage],
       });
index 434258ca1aadee759899c2e101e4700db681730a..2f8daaff94ae7d04ea34c88dd45542ba16576a71 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
 import { getBaseUrl } from '../../helpers/system';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
 import { Permissions } from '../../types/permissions';
 import { PermissionDefinitions, PermissionGroup, Provider } from '../../types/types';
 import GroupIcon from '../icons/GroupIcon';
index da403626660cdf5b41eb72d32b57440e05592893..3b6e626e2ee018e05d70aa736b18fe3224ae19ab 100644 (file)
@@ -24,7 +24,7 @@ import UseQuery from '../../helpers/UseQuery';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
 import { useIsGitHubProjectQuery } from '../../queries/devops-integration';
-import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
+import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider/github';
 import { Dict, PermissionDefinitions, PermissionGroup, PermissionUser } from '../../types/types';
 import GroupHolder from './GroupHolder';
 import PermissionHeader from './PermissionHeader';
index acbb275b506fb579f90444461d46605ef8cc4f10..084b0e365f705218b60b93a5fd5b24aab2831755 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { isPermissionDefinitionGroup } from '../../helpers/permissions';
 import { getBaseUrl } from '../../helpers/system';
-import { useIdentityProviderQuery } from '../../queries/identity-provider';
+import { useIdentityProviderQuery } from '../../queries/identity-provider/common';
 import { PermissionDefinitions, PermissionUser, Provider } from '../../types/types';
 import PermissionCell from './PermissionCell';
 import usePermissionChange from './usePermissionChange';
index fb3e7206f2d0fbe4db263f40b056e3bd4160abbb..e8312fb0b0034c09e3b9464ce41a5087eb82de2d 100644 (file)
@@ -107,10 +107,11 @@ export function mockGitlabConfiguration(
     id: Math.random().toString(),
     enabled: false,
     url: 'URL',
+    applicationId: '123',
     allowUsersToSignUp: false,
-    synchronizeUserGroups: true,
-    type: ProvisioningType.jit,
-    groups: [],
+    synchronizeGroups: true,
+    synchronizationType: ProvisioningType.jit,
+    provisioningGroups: [],
     ...overrides,
   };
 }
diff --git a/server/sonar-web/src/main/js/queries/identity-provider.ts b/server/sonar-web/src/main/js/queries/identity-provider.ts
deleted file mode 100644 (file)
index 68bddee..0000000
+++ /dev/null
@@ -1,315 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
-import { useContext } from 'react';
-import { getActivity } from '../api/ce';
-import {
-  activateGithubProvisioning,
-  activateScim,
-  addGithubRolesMapping,
-  checkConfigurationValidity,
-  createGitLabConfiguration,
-  deactivateGithubProvisioning,
-  deactivateScim,
-  deleteGitLabConfiguration,
-  deleteGithubRolesMapping,
-  fetchGitLabConfigurations,
-  fetchGithubProvisioningStatus,
-  fetchGithubRolesMapping,
-  fetchIsScimEnabled,
-  syncNowGithubProvisioning,
-  updateGitLabConfiguration,
-  updateGithubRolesMapping,
-} from '../api/provisioning';
-import { getSystemInfo } from '../api/system';
-import { AvailableFeaturesContext } from '../app/components/available-features/AvailableFeaturesContext';
-import { addGlobalSuccessMessage } from '../helpers/globalMessages';
-import { translate } from '../helpers/l10n';
-import { mapReactQueryResult } from '../helpers/react-query';
-import { Feature } from '../types/features';
-import { AlmSyncStatus, GitHubMapping } from '../types/provisioning';
-import { TaskStatuses, TaskTypes } from '../types/tasks';
-import { SysInfoCluster } from '../types/types';
-
-const MAPPING_STALE_TIME = 60_000;
-
-export function useIdentityProviderQuery() {
-  return useQuery(['identity_provider'], async () => {
-    const info = (await getSystemInfo()) as SysInfoCluster;
-    return { provider: info.System['External Users and Groups Provisioning'] };
-  });
-}
-
-export function useScimStatusQuery() {
-  const hasScim = useContext(AvailableFeaturesContext).includes(Feature.Scim);
-
-  return useQuery(['identity_provider', 'scim_status'], () => {
-    if (!hasScim) {
-      return false;
-    }
-    return fetchIsScimEnabled();
-  });
-}
-
-export function useToggleScimMutation() {
-  const client = useQueryClient();
-  return useMutation({
-    mutationFn: (activate: boolean) => (activate ? activateScim() : deactivateScim()),
-    onSuccess: () => {
-      client.invalidateQueries({ queryKey: ['identity_provider'] });
-    },
-  });
-}
-
-export function useToggleGithubProvisioningMutation() {
-  const client = useQueryClient();
-  return useMutation({
-    mutationFn: (activate: boolean) =>
-      activate ? activateGithubProvisioning() : deactivateGithubProvisioning(),
-    onSuccess: () => {
-      client.invalidateQueries({ queryKey: ['identity_provider'] });
-    },
-  });
-}
-
-export const useCheckGitHubConfigQuery = (githubEnabled: boolean) => {
-  return useQuery(['identity_provider', 'github_check'], checkConfigurationValidity, {
-    enabled: githubEnabled,
-  });
-};
-
-interface GithubSyncStatusOptions {
-  noRefetch?: boolean;
-}
-
-export function useGitHubSyncStatusQuery(options: GithubSyncStatusOptions = {}) {
-  const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
-    Feature.GithubProvisioning,
-  );
-  return useQuery(['identity_provider', 'github_sync', 'status'], fetchGithubProvisioningStatus, {
-    enabled: hasGithubProvisioning,
-    refetchInterval: options.noRefetch ? undefined : 10_000,
-  });
-}
-
-export function useGithubProvisioningEnabledQuery() {
-  const res = useGitHubSyncStatusQuery({ noRefetch: true });
-
-  return mapReactQueryResult(res, (data) => data.enabled);
-}
-
-export function useSyncWithGitHubNow() {
-  const queryClient = useQueryClient();
-  const { data } = useGitHubSyncStatusQuery();
-  const mutation = useMutation(syncNowGithubProvisioning, {
-    onSuccess: () => {
-      queryClient.invalidateQueries(['identity_provider', 'github_sync']);
-    },
-  });
-
-  return {
-    synchronizeNow: mutation.mutate,
-    canSyncNow: data?.enabled && !data.nextSync && !mutation.isLoading,
-  };
-}
-
-// Order is reversed to put custom roles at the end (their index is -1)
-const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read'];
-
-export function useGithubRolesMappingQuery() {
-  return useQuery(['identity_provider', 'github_mapping'], fetchGithubRolesMapping, {
-    staleTime: MAPPING_STALE_TIME,
-    select: (data) =>
-      [...data].sort((a, b) => {
-        if (defaultRoleOrder.includes(a.id) || defaultRoleOrder.includes(b.id)) {
-          return defaultRoleOrder.indexOf(b.id) - defaultRoleOrder.indexOf(a.id);
-        }
-        return a.githubRole.localeCompare(b.githubRole);
-      }),
-  });
-}
-
-export function useGithubRolesMappingMutation() {
-  const client = useQueryClient();
-  const queryKey = ['identity_provider', 'github_mapping'];
-  return useMutation({
-    mutationFn: async (mapping: GitHubMapping[]) => {
-      const state = keyBy(client.getQueryData<GitHubMapping[]>(queryKey), (m) => m.id);
-
-      const [maybeChangedRoles, newRoles] = partition(mapping, (m) => state[m.id]);
-      const changedRoles = maybeChangedRoles.filter((item) => !isEqual(item, state[item.id]));
-      const deletedRoles = Object.values(state).filter(
-        (m) => !m.isBaseRole && !mapping.some((cm) => m.id === cm.id),
-      );
-
-      return {
-        addedOrChanged: await Promise.all([
-          ...changedRoles.map((data) =>
-            updateGithubRolesMapping(data.id, pick(data, 'permissions')),
-          ),
-          ...newRoles.map((m) => addGithubRolesMapping(m)),
-        ]),
-        deleted: await Promise.all([
-          deletedRoles.map((dm) => deleteGithubRolesMapping(dm.id)),
-        ]).then(() => deletedRoles.map((dm) => dm.id)),
-      };
-    },
-    onSuccess: ({ addedOrChanged, deleted }) => {
-      const state = client.getQueryData<GitHubMapping[]>(queryKey);
-      if (state) {
-        const newData = unionBy(
-          addedOrChanged,
-          state.filter((s) => !deleted.find((id) => id === s.id)),
-          (el) => el.id,
-        );
-        client.setQueryData(queryKey, newData);
-      }
-      addGlobalSuccessMessage(
-        translate('settings.authentication.github.configuration.roles_mapping.save_success'),
-      );
-    },
-  });
-}
-
-export function useGitLabConfigurationsQuery() {
-  return useQuery(['identity_provider', 'gitlab_config', 'list'], fetchGitLabConfigurations);
-}
-
-export function useCreateGitLabConfigurationMutation() {
-  const client = useQueryClient();
-  return useMutation({
-    mutationFn: (data: Parameters<typeof createGitLabConfiguration>[0]) =>
-      createGitLabConfiguration(data),
-    onSuccess(data) {
-      client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
-        configurations: [data],
-        page: {
-          pageIndex: 1,
-          pageSize: 1,
-          total: 1,
-        },
-      });
-    },
-  });
-}
-
-export function useUpdateGitLabConfigurationMutation() {
-  const client = useQueryClient();
-  return useMutation({
-    mutationFn: ({
-      id,
-      data,
-    }: {
-      id: Parameters<typeof updateGitLabConfiguration>[0];
-      data: Parameters<typeof updateGitLabConfiguration>[1];
-    }) => updateGitLabConfiguration(id, data),
-    onSuccess(data) {
-      client.invalidateQueries({ queryKey: ['identity_provider'] });
-      client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
-        configurations: [data],
-        page: {
-          pageIndex: 1,
-          pageSize: 1,
-          total: 1,
-        },
-      });
-    },
-  });
-}
-
-export function useDeleteGitLabConfigurationMutation() {
-  const client = useQueryClient();
-  return useMutation({
-    mutationFn: (id: Parameters<typeof deleteGitLabConfiguration>[0]) =>
-      deleteGitLabConfiguration(id),
-    onSuccess() {
-      client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
-        configurations: [],
-        page: {
-          pageIndex: 1,
-          pageSize: 1,
-          total: 0,
-        },
-      });
-    },
-  });
-}
-
-export function useGitLabSyncStatusQuery() {
-  const getLastSync = async () => {
-    const lastSyncTasks = await getActivity({
-      type: TaskTypes.GitlabProvisioning,
-      p: 1,
-      ps: 1,
-      status: [TaskStatuses.Success, TaskStatuses.Failed, TaskStatuses.Canceled].join(','),
-    });
-    const lastSync = lastSyncTasks?.tasks[0];
-    if (!lastSync) {
-      return undefined;
-    }
-    const summary = lastSync.infoMessages ? lastSync.infoMessages?.join(', ') : '';
-    const errorMessage = lastSync.errorMessage ?? '';
-    return {
-      executionTimeMs: lastSync?.executionTimeMs ?? 0,
-      startedAt: +new Date(lastSync?.startedAt ?? 0),
-      finishedAt: +new Date(lastSync?.startedAt ?? 0) + (lastSync?.executionTimeMs ?? 0),
-      warningMessage:
-        lastSync.warnings && lastSync.warnings.length > 0
-          ? lastSync.warnings?.join(', ')
-          : undefined,
-      status: lastSync?.status as
-        | TaskStatuses.Success
-        | TaskStatuses.Failed
-        | TaskStatuses.Canceled,
-      ...(lastSync.status === TaskStatuses.Success ? { summary } : {}),
-      ...(lastSync.status !== TaskStatuses.Success ? { errorMessage } : {}),
-    };
-  };
-
-  const getNextSync = async () => {
-    const nextSyncTasks = await getActivity({
-      type: TaskTypes.GitlabProvisioning,
-      p: 1,
-      ps: 1,
-      status: [TaskStatuses.Pending, TaskStatuses.InProgress].join(','),
-    });
-    const nextSync = nextSyncTasks?.tasks[0];
-    if (!nextSync) {
-      return undefined;
-    }
-    return { status: nextSync.status as TaskStatuses.Pending | TaskStatuses.InProgress };
-  };
-
-  return useQuery(
-    ['identity_provider', 'gitlab_sync', 'status'],
-    async () => {
-      const [lastSync, nextSync] = await Promise.all([getLastSync(), getNextSync()]);
-      return {
-        lastSync,
-        nextSync,
-      } as AlmSyncStatus;
-    },
-    {
-      refetchInterval: 10_000,
-    },
-  );
-}
diff --git a/server/sonar-web/src/main/js/queries/identity-provider/common.ts b/server/sonar-web/src/main/js/queries/identity-provider/common.ts
new file mode 100644 (file)
index 0000000..6942b17
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { useQuery } from '@tanstack/react-query';
+import { getSystemInfo } from '../../api/system';
+import { SysInfoCluster } from '../../types/types';
+
+export function useIdentityProviderQuery() {
+  return useQuery(['identity_provider', 'users_and_groups_provider'], async () => {
+    const info = (await getSystemInfo()) as SysInfoCluster;
+    return { provider: info.System['External Users and Groups Provisioning'] };
+  });
+}
diff --git a/server/sonar-web/src/main/js/queries/identity-provider/github.ts b/server/sonar-web/src/main/js/queries/identity-provider/github.ts
new file mode 100644 (file)
index 0000000..9a999b0
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { isEqual, keyBy, partition, pick, unionBy } from 'lodash';
+import { useContext } from 'react';
+import {
+  activateGithubProvisioning,
+  addGithubRolesMapping,
+  checkConfigurationValidity,
+  deactivateGithubProvisioning,
+  deleteGithubRolesMapping,
+  fetchGithubProvisioningStatus,
+  fetchGithubRolesMapping,
+  syncNowGithubProvisioning,
+  updateGithubRolesMapping,
+} from '../../api/github-provisioning';
+import { AvailableFeaturesContext } from '../../app/components/available-features/AvailableFeaturesContext';
+import { addGlobalSuccessMessage } from '../../helpers/globalMessages';
+import { translate } from '../../helpers/l10n';
+import { mapReactQueryResult } from '../../helpers/react-query';
+import { Feature } from '../../types/features';
+import { GitHubMapping } from '../../types/provisioning';
+
+const MAPPING_STALE_TIME = 60_000;
+
+export function useToggleGithubProvisioningMutation() {
+  const client = useQueryClient();
+  return useMutation({
+    mutationFn: (activate: boolean) =>
+      activate ? activateGithubProvisioning() : deactivateGithubProvisioning(),
+    onSuccess: () => {
+      client.invalidateQueries({ queryKey: ['identity_provider'] });
+    },
+  });
+}
+
+export const useCheckGitHubConfigQuery = (githubEnabled: boolean) => {
+  return useQuery(['identity_provider', 'github_check'], checkConfigurationValidity, {
+    enabled: githubEnabled,
+  });
+};
+
+interface GithubSyncStatusOptions {
+  noRefetch?: boolean;
+}
+
+export function useGitHubSyncStatusQuery(options: GithubSyncStatusOptions = {}) {
+  const hasGithubProvisioning = useContext(AvailableFeaturesContext).includes(
+    Feature.GithubProvisioning,
+  );
+  return useQuery(['identity_provider', 'github_sync', 'status'], fetchGithubProvisioningStatus, {
+    enabled: hasGithubProvisioning,
+    refetchInterval: options.noRefetch ? undefined : 10_000,
+  });
+}
+
+export function useGithubProvisioningEnabledQuery() {
+  const res = useGitHubSyncStatusQuery({ noRefetch: true });
+
+  return mapReactQueryResult(res, (data) => data.enabled);
+}
+
+export function useSyncWithGitHubNow() {
+  const queryClient = useQueryClient();
+  const { data } = useGitHubSyncStatusQuery();
+  const mutation = useMutation(syncNowGithubProvisioning, {
+    onSuccess: () => {
+      queryClient.invalidateQueries(['identity_provider', 'github_sync']);
+    },
+  });
+
+  return {
+    synchronizeNow: mutation.mutate,
+    canSyncNow: data?.enabled && !data.nextSync && !mutation.isLoading,
+  };
+}
+
+// Order is reversed to put custom roles at the end (their index is -1)
+const defaultRoleOrder = ['admin', 'maintain', 'write', 'triage', 'read'];
+
+export function useGithubRolesMappingQuery() {
+  return useQuery(['identity_provider', 'github_mapping'], fetchGithubRolesMapping, {
+    staleTime: MAPPING_STALE_TIME,
+    select: (data) =>
+      [...data].sort((a, b) => {
+        if (defaultRoleOrder.includes(a.id) || defaultRoleOrder.includes(b.id)) {
+          return defaultRoleOrder.indexOf(b.id) - defaultRoleOrder.indexOf(a.id);
+        }
+        return a.githubRole.localeCompare(b.githubRole);
+      }),
+  });
+}
+
+export function useGithubRolesMappingMutation() {
+  const client = useQueryClient();
+  const queryKey = ['identity_provider', 'github_mapping'];
+  return useMutation({
+    mutationFn: async (mapping: GitHubMapping[]) => {
+      const state = keyBy(client.getQueryData<GitHubMapping[]>(queryKey), (m) => m.id);
+
+      const [maybeChangedRoles, newRoles] = partition(mapping, (m) => state[m.id]);
+      const changedRoles = maybeChangedRoles.filter((item) => !isEqual(item, state[item.id]));
+      const deletedRoles = Object.values(state).filter(
+        (m) => !m.isBaseRole && !mapping.some((cm) => m.id === cm.id),
+      );
+
+      return {
+        addedOrChanged: await Promise.all([
+          ...changedRoles.map((data) =>
+            updateGithubRolesMapping(data.id, pick(data, 'permissions')),
+          ),
+          ...newRoles.map((m) => addGithubRolesMapping(m)),
+        ]),
+        deleted: await Promise.all([
+          deletedRoles.map((dm) => deleteGithubRolesMapping(dm.id)),
+        ]).then(() => deletedRoles.map((dm) => dm.id)),
+      };
+    },
+    onSuccess: ({ addedOrChanged, deleted }) => {
+      const state = client.getQueryData<GitHubMapping[]>(queryKey);
+      if (state) {
+        const newData = unionBy(
+          addedOrChanged,
+          state.filter((s) => !deleted.find((id) => id === s.id)),
+          (el) => el.id,
+        );
+        client.setQueryData(queryKey, newData);
+      }
+      addGlobalSuccessMessage(
+        translate('settings.authentication.github.configuration.roles_mapping.save_success'),
+      );
+    },
+  });
+}
diff --git a/server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts b/server/sonar-web/src/main/js/queries/identity-provider/gitlab.ts
new file mode 100644 (file)
index 0000000..8368d98
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { getActivity } from '../../api/ce';
+import {
+  createGitLabConfiguration,
+  deleteGitLabConfiguration,
+  fetchGitLabConfigurations,
+  updateGitLabConfiguration,
+} from '../../api/gitlab-provisioning';
+import { AlmSyncStatus } from '../../types/provisioning';
+import { TaskStatuses, TaskTypes } from '../../types/tasks';
+
+export function useGitLabConfigurationsQuery() {
+  return useQuery(['identity_provider', 'gitlab_config', 'list'], fetchGitLabConfigurations);
+}
+
+export function useCreateGitLabConfigurationMutation() {
+  const client = useQueryClient();
+  return useMutation({
+    mutationFn: (data: Parameters<typeof createGitLabConfiguration>[0]) =>
+      createGitLabConfiguration(data),
+    onSuccess(data) {
+      client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+        gitlabConfigurations: [data],
+        page: {
+          pageIndex: 1,
+          pageSize: 1,
+          total: 1,
+        },
+      });
+    },
+  });
+}
+
+export function useUpdateGitLabConfigurationMutation() {
+  const client = useQueryClient();
+  return useMutation({
+    mutationFn: ({
+      id,
+      data,
+    }: {
+      id: Parameters<typeof updateGitLabConfiguration>[0];
+      data: Parameters<typeof updateGitLabConfiguration>[1];
+    }) => updateGitLabConfiguration(id, data),
+    onSuccess(data) {
+      client.invalidateQueries({ queryKey: ['identity_provider', 'users_and_groups_provider'] });
+      client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+        gitlabConfigurations: [data],
+        page: {
+          pageIndex: 1,
+          pageSize: 1,
+          total: 1,
+        },
+      });
+    },
+  });
+}
+
+export function useDeleteGitLabConfigurationMutation() {
+  const client = useQueryClient();
+  return useMutation({
+    mutationFn: (id: Parameters<typeof deleteGitLabConfiguration>[0]) =>
+      deleteGitLabConfiguration(id),
+    onSuccess() {
+      client.setQueryData(['identity_provider', 'gitlab_config', 'list'], {
+        gitlabConfigurations: [],
+        page: {
+          pageIndex: 1,
+          pageSize: 1,
+          total: 0,
+        },
+      });
+    },
+  });
+}
+
+export function useGitLabSyncStatusQuery() {
+  const getLastSync = async () => {
+    const lastSyncTasks = await getActivity({
+      type: TaskTypes.GitlabProvisioning,
+      p: 1,
+      ps: 1,
+      status: [TaskStatuses.Success, TaskStatuses.Failed, TaskStatuses.Canceled].join(','),
+    });
+    const lastSync = lastSyncTasks?.tasks[0];
+    if (!lastSync) {
+      return undefined;
+    }
+    const summary = lastSync.infoMessages ? lastSync.infoMessages?.join(', ') : '';
+    const errorMessage = lastSync.errorMessage ?? '';
+    return {
+      executionTimeMs: lastSync?.executionTimeMs ?? 0,
+      startedAt: +new Date(lastSync?.startedAt ?? 0),
+      finishedAt: +new Date(lastSync?.startedAt ?? 0) + (lastSync?.executionTimeMs ?? 0),
+      warningMessage:
+        lastSync.warnings && lastSync.warnings.length > 0
+          ? lastSync.warnings?.join(', ')
+          : undefined,
+      status: lastSync?.status as
+        | TaskStatuses.Success
+        | TaskStatuses.Failed
+        | TaskStatuses.Canceled,
+      ...(lastSync.status === TaskStatuses.Success ? { summary } : {}),
+      ...(lastSync.status !== TaskStatuses.Success ? { errorMessage } : {}),
+    };
+  };
+
+  const getNextSync = async () => {
+    const nextSyncTasks = await getActivity({
+      type: TaskTypes.GitlabProvisioning,
+      p: 1,
+      ps: 1,
+      status: [TaskStatuses.Pending, TaskStatuses.InProgress].join(','),
+    });
+    const nextSync = nextSyncTasks?.tasks[0];
+    if (!nextSync) {
+      return undefined;
+    }
+    return { status: nextSync.status as TaskStatuses.Pending | TaskStatuses.InProgress };
+  };
+
+  return useQuery(
+    ['identity_provider', 'gitlab_sync', 'status'],
+    async () => {
+      const [lastSync, nextSync] = await Promise.all([getLastSync(), getNextSync()]);
+      return {
+        lastSync,
+        nextSync,
+      } as AlmSyncStatus;
+    },
+    {
+      refetchInterval: 10_000,
+    },
+  );
+}
diff --git a/server/sonar-web/src/main/js/queries/identity-provider/scim.ts b/server/sonar-web/src/main/js/queries/identity-provider/scim.ts
new file mode 100644 (file)
index 0000000..091ae2a
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useContext } from 'react';
+import { activateScim, deactivateScim, fetchIsScimEnabled } from '../../api/scim-provisioning';
+import { AvailableFeaturesContext } from '../../app/components/available-features/AvailableFeaturesContext';
+import { Feature } from '../../types/features';
+
+export function useScimStatusQuery() {
+  const hasScim = useContext(AvailableFeaturesContext).includes(Feature.Scim);
+
+  return useQuery(['identity_provider', 'scim_status'], () => {
+    if (!hasScim) {
+      return false;
+    }
+    return fetchIsScimEnabled();
+  });
+}
+
+export function useToggleScimMutation() {
+  const client = useQueryClient();
+  return useMutation({
+    mutationFn: (activate: boolean) => (activate ? activateScim() : deactivateScim()),
+    onSuccess: () => {
+      client.invalidateQueries({ queryKey: ['identity_provider'] });
+    },
+  });
+}
index 9a7700dd84043143422a49a171752bc65951b50c..86212ed52d0bf81a8cb47b7f53049a4fbad9f0b8 100644 (file)
@@ -96,33 +96,34 @@ export interface GitHubMapping {
 export interface GitLabConfigurationCreateBody {
   applicationId: string;
   url: string;
-  clientSecret: string;
-  synchronizeUserGroups: boolean;
+  secret: string;
+  synchronizeGroups: boolean;
 }
 
 export type GitLabConfigurationUpdateBody = {
   applicationId?: string;
   url?: string;
-  clientSecret?: string;
-  synchronizeUserGroups?: boolean;
+  secret?: string;
+  synchronizeGroups?: boolean;
   enabled?: boolean;
-  type?: ProvisioningType;
+  synchronizationType?: ProvisioningType;
   provisioningToken?: string;
-  groups?: string[];
+  provisioningGroups?: string[];
   allowUsersToSignUp?: boolean;
 };
 
 export type GitlabConfiguration = {
   id: string;
   enabled: boolean;
-  synchronizeUserGroups: boolean;
+  applicationId: string;
+  synchronizeGroups: boolean;
   url: string;
-  type: ProvisioningType;
-  groups: string[];
+  synchronizationType: ProvisioningType;
+  provisioningGroups: string[];
   allowUsersToSignUp: boolean;
 };
 
 export enum ProvisioningType {
   jit = 'JIT',
-  auto = 'Auto',
+  auto = 'AUTO_PROVISIONING',
 }
index 406b80257bd2749570b8e50316c4d7aa79ce79a2..a99a9a94c1df303d88ca3b99475ad26517645e06 100644 (file)
@@ -1586,20 +1586,26 @@ settings.authentication.gitlab.form.applicationId.name=Application ID
 settings.authentication.gitlab.form.applicationId.description=Application ID provided by GitLab when registering the application.
 settings.authentication.gitlab.form.url.name=GitLab URL
 settings.authentication.gitlab.form.url.description=URL to access GitLab.
-settings.authentication.gitlab.form.clientSecret.name=Secret
-settings.authentication.gitlab.form.clientSecret.description=Secret provided by GitLab when registering the application.
-settings.authentication.gitlab.form.synchronizeUserGroups.name=Synchronize user groups
-settings.authentication.gitlab.form.synchronizeUserGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab Oauth2 application will need to provide the api scope.
+settings.authentication.gitlab.form.secret.name=Secret
+settings.authentication.gitlab.form.secret.description=Secret provided by GitLab when registering the application.
+settings.authentication.gitlab.form.synchronizeGroups.name=Synchronize user groups
+settings.authentication.gitlab.form.synchronizeGroups.description=For each GitLab group they belong to, the user will be associated to a group with the same name (if it exists) in SonarQube. If enabled, the GitLab Oauth2 application will need to provide the api scope.
+settings.authentication.gitlab.form.provisioningGroups.name=Groups
+settings.authentication.gitlab.form.provisioningGroups.description=Only members of these groups (and sub-groups) will be provisioned. Please enter the group slug as it appears in GitLab URL, for instance `my-gitlab-group`.
+settings.authentication.gitlab.form.allowUsersToSignUp.name=Allow users to sign up
+settings.authentication.gitlab.form.allowUsersToSignUp.description=Allow new users to authenticate. When set to 'false', only existing users will be able to authenticate to the server.
+settings.authentication.gitlab.form.provisioningToken.name=Provisioning token
+settings.authentication.gitlab.form.provisioningToken.description=Token used for provisioning users. Both a group or a personal access token can be used as soon as it has visibility on desired groups.
 settings.authentication.gitlab.provisioning_at_login=Just-in-Time user provisioning (default)
 settings.authentication.gitlab.provisioning_at_login.description=Users are synchronized only when users log in to SonarQube.
 settings.authentication.gitlab.description.doc=For more details, see {documentation}.
-settings.authentication.gitlab.confirm.Auto=Switch to automatic provisioning
+settings.authentication.gitlab.confirm.AUTO_PROVISIONING=Switch to automatic provisioning
 settings.authentication.gitlab.confirm.JIT=Switch to Just-in-Time provisioning
-settings.authentication.gitlab.confirm.Auto.description=Once you transition to automatic provisioning users on GitLab projects will be inherited from GitLab. You will no longer have the ability to edit them within SonarQube. Do you want to proceed with this change?
-settings.authentication.gitlab.confirm.JIT.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users. Users are provisioned and updated only at user login. Are you sure?
+settings.authentication.gitlab.confirm.AUTO_PROVISIONING.description=Once you transition to automatic provisioning users and groups on GitLab projects will be inherited from GitLab. You will no longer have the ability to edit them within SonarQube. Do you want to proceed with this change?
+settings.authentication.gitlab.confirm.JIT.description=Switching to Just-in-Time provisioning removes the automatic synchronization of users and groups. Users are provisioned and updated only at user login. Are you sure?
 settings.authentication.gitlab.provisioning_change.confirm_changes=Confirm Changes
-settings.authentication.gitlab.form.provisioning_with_gitlab=Automatic user provisioning
-settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users are automatically provisioned from your GitLab organizations. Once activated, users can only be created and modified from your GitLab groups. Existing local users will be kept and can only be deactivated.
+settings.authentication.gitlab.form.provisioning_with_gitlab=Automatic user and group provisioning
+settings.authentication.gitlab.form.provisioning_with_gitlab.description=Users and groups are automatically provisioned from GitLab. Once activated, users and groups can only be created and modified from GitLab. Existing local users will be kept and can only be deactivated.
 settings.authentication.gitlab.form.provisioning.disabled=Your current edition does not support provisioning with GitLab. See the {documentation} for more information.
 settings.authentication.gitlab.configuration.unsaved_changes=You have unsaved changes.