aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJacek <jacek.poreda@sonarsource.com>2021-01-26 11:23:07 +0100
committersonartech <sonartech@sonarsource.com>2021-02-04 20:07:07 +0000
commit044247d48921aa5486c4cea1e2719207ff250fc9 (patch)
tree1e78e0602d4a1a65b5e0f10757a25aea586317b2 /server
parent67ff6070437876c08adafb5ac244252d81c315e7 (diff)
downloadsonarqube-044247d48921aa5486c4cea1e2719207ff250fc9.tar.gz
sonarqube-044247d48921aa5486c4cea1e2719207ff250fc9.zip
SONAR-14371 Move gitlab http client to CE
Diffstat (limited to 'server')
-rw-r--r--server/sonar-alm-client/build.gradle15
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfiguration.java40
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfigurationImpl.java64
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java183
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GsonError.java62
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/Project.java105
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/ProjectList.java53
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/package-info.java23
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/ConstantTimeoutConfiguration.java38
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/TimeoutConfigurationImplTest.java87
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java270
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java5
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ImportHelper.java76
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java149
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java197
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/package-info.java23
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/package-info.java23
-rw-r--r--server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/gitlab/search_gitlab_repos.json33
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModuleTest.java36
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWsTest.java61
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java148
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionTest.java282
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java6
23 files changed, 1977 insertions, 2 deletions
diff --git a/server/sonar-alm-client/build.gradle b/server/sonar-alm-client/build.gradle
index 7c9be696227..cdcae780e98 100644
--- a/server/sonar-alm-client/build.gradle
+++ b/server/sonar-alm-client/build.gradle
@@ -1,5 +1,18 @@
description = 'SonarQube :: ALM integrations :: Clients'
dependencies {
- testCompile group: 'junit', name: 'junit'
+ compile project(path: ':sonar-plugin-api', configuration: 'shadow')
+ compile project(':sonar-ws')
+ compile 'com.google.code.gson:gson'
+ compile 'com.google.guava:guava'
+ compile 'com.squareup.okhttp3:okhttp'
+
+ testCompile project(':sonar-plugin-api-impl')
+ testCompile 'junit:junit'
+ testCompile 'com.tngtech.java:junit-dataprovider'
+ testCompile 'org.assertj:assertj-core'
+ testCompile 'org.assertj:assertj-guava'
+ testCompile 'org.mockito:mockito-core'
+ testCompile 'com.squareup.okhttp3:mockwebserver'
+
}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfiguration.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfiguration.java
new file mode 100644
index 00000000000..f6ea6e1ca32
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client;
+
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+
+/**
+ * Holds the configuration of timeouts when connecting to ALMs.
+ */
+@ServerSide
+@ComputeEngineSide
+public interface TimeoutConfiguration {
+ /**
+ * @return connect timeout in milliseconds
+ */
+ long getConnectTimeout();
+
+ /**
+ * @return read timeout in milliseconds
+ */
+ long getReadTimeout();
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfigurationImpl.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfigurationImpl.java
new file mode 100644
index 00000000000..3e73abd5a8b
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/TimeoutConfigurationImpl.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client;
+
+import java.util.OptionalLong;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.log.Loggers;
+
+/**
+ * Implementation of {@link TimeoutConfiguration} reading values from configuration properties.
+ */
+public class TimeoutConfigurationImpl implements TimeoutConfiguration {
+ private static final String CONNECT_TIMEOUT_PROPERTY = "sonar.alm.timeout.connect";
+ private static final String READ_TIMEOUT_PROPERTY = "sonar.alm.timeout.read";
+
+ private static final long DEFAULT_TIMEOUT = 30_000;
+ private final Configuration configuration;
+
+ public TimeoutConfigurationImpl(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public long getConnectTimeout() {
+ return safelyParseLongValue(CONNECT_TIMEOUT_PROPERTY).orElse(DEFAULT_TIMEOUT);
+ }
+
+ private OptionalLong safelyParseLongValue(String property) {
+ return configuration.get(property)
+ .map(value -> {
+ try {
+ return OptionalLong.of(Long.parseLong(value));
+ } catch (NumberFormatException e) {
+ Loggers.get(TimeoutConfigurationImpl.class)
+ .warn("Value of property {} can not be parsed to a long: {}", property, value);
+ return OptionalLong.empty();
+ }
+ })
+ .orElse(OptionalLong.empty());
+
+ }
+
+ @Override
+ public long getReadTimeout() {
+ return safelyParseLongValue(READ_TIMEOUT_PROPERTY).orElse(DEFAULT_TIMEOUT);
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java
new file mode 100644
index 00000000000..e0d453c56e2
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java
@@ -0,0 +1,183 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client.gitlab;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.sonar.alm.client.TimeoutConfiguration;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonarqube.ws.client.OkHttpClientBuilder;
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@ServerSide
+public class GitlabHttpClient {
+
+ private static final Logger LOG = Loggers.get(GitlabHttpClient.class);
+ protected static final String PRIVATE_TOKEN = "Private-Token";
+ protected final OkHttpClient client;
+
+ public GitlabHttpClient(TimeoutConfiguration timeoutConfiguration) {
+ client = new OkHttpClientBuilder()
+ .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
+ .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
+ .build();
+ }
+
+ private static String urlEncode(String value) {
+ try {
+ return URLEncoder.encode(value, UTF_8.toString());
+ } catch (UnsupportedEncodingException ex) {
+ throw new IllegalStateException(ex.getCause());
+ }
+ }
+
+ protected static void checkResponseIsSuccessful(Response response) throws IOException {
+ checkResponseIsSuccessful(response, "GitLab Merge Request did not happen, please check your configuration");
+ }
+
+ protected static void checkResponseIsSuccessful(Response response, String errorMessage) throws IOException {
+ if (!response.isSuccessful()) {
+ String body = response.body().string();
+ LOG.error(String.format("Gitlab API call to [%s] failed with %s http code. gitlab response content : [%s]", response.request().url().toString(), response.code(), body));
+ if (isTokenRevoked(response, body)) {
+ throw new IllegalArgumentException("Your GitLab token was revoked");
+ } else if (isTokenExpired(response, body)) {
+ throw new IllegalArgumentException("Your GitLab token is expired");
+ } else if (isInsufficientScope(response, body)) {
+ throw new IllegalArgumentException("Your GitLab token has insufficient scope");
+ } else if (response.code() == HTTP_UNAUTHORIZED) {
+ throw new IllegalArgumentException("Invalid personal access token");
+ } else {
+ throw new IllegalArgumentException(errorMessage);
+ }
+ }
+ }
+
+ private static boolean isTokenRevoked(Response response, String body) {
+ if (response.code() == HTTP_UNAUTHORIZED) {
+ try {
+ Optional<GsonError> gitlabError = GsonError.parseOne(body);
+ return gitlabError.map(GsonError::getErrorDescription).map(description -> description.contains("Token was revoked")).orElse(false);
+ } catch (JsonParseException e) {
+ // nothing to do
+ }
+ }
+ return false;
+ }
+
+ private static boolean isTokenExpired(Response response, String body) {
+ if (response.code() == HTTP_UNAUTHORIZED) {
+ try {
+ Optional<GsonError> gitlabError = GsonError.parseOne(body);
+ return gitlabError.map(GsonError::getErrorDescription).map(description -> description.contains("Token is expired")).orElse(false);
+ } catch (JsonParseException e) {
+ // nothing to do
+ }
+ }
+ return false;
+ }
+
+ private static boolean isInsufficientScope(Response response, String body) {
+ if (response.code() == HTTP_FORBIDDEN) {
+ try {
+ Optional<GsonError> gitlabError = GsonError.parseOne(body);
+ return gitlabError.map(GsonError::getError).map("insufficient_scope"::equals).orElse(false);
+ } catch (JsonParseException e) {
+ // nothing to do
+ }
+ }
+ return false;
+ }
+
+ public Project getProject(String gitlabUrl, String pat, Long gitlabProjectId) {
+ String url = String.format("%s/projects/%s", gitlabUrl, gitlabProjectId);
+ LOG.debug(String.format("get project : [%s]", url));
+ Request request = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, pat)
+ .get()
+ .url(url)
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response);
+ String body = response.body().string();
+ LOG.trace(String.format("loading project payload result : [%s]", body));
+ return new GsonBuilder().create().fromJson(body, Project.class);
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
+ } catch (IOException e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ public ProjectList searchProjects(String gitlabUrl, String personalAccessToken, @Nullable String projectName,
+ int pageNumber, int pageSize) {
+ String url = String.format("%s/projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%s&page=%d&per_page=%d",
+ gitlabUrl, projectName == null ? "" : urlEncode(projectName), pageNumber, pageSize);
+
+ LOG.debug(String.format("get projects : [%s]", url));
+ Request request = new Request.Builder()
+ .addHeader(PRIVATE_TOKEN, personalAccessToken)
+ .url(url)
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response, "Could not get projects from GitLab instance");
+ List<Project> projectList = Project.parseJsonArray(response.body().string());
+ int returnedPageNumber = parseAndGetIntegerHeader(response.header("X-Page"));
+ int returnedPageSize = parseAndGetIntegerHeader(response.header("X-Per-Page"));
+ int totalProjects = parseAndGetIntegerHeader(response.header("X-Total"));
+ return new ProjectList(projectList, returnedPageNumber, returnedPageSize, totalProjects);
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException("Could not parse GitLab answer to search projects. Got a non-json payload as result.");
+ } catch (IOException e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ }
+
+ private static int parseAndGetIntegerHeader(@Nullable String header) {
+ if (header == null) {
+ throw new IllegalArgumentException("Pagination data from GitLab response is missing");
+ } else {
+ try {
+ return Integer.parseInt(header);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Could not parse pagination number", e);
+ }
+ }
+ }
+
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GsonError.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GsonError.java
new file mode 100644
index 00000000000..8f3af6690bb
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GsonError.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client.gitlab;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import java.util.Optional;
+
+public class GsonError {
+ @SerializedName("error")
+ private final String error;
+
+ @SerializedName("error_description")
+ private final String errorDescription;
+
+ @SerializedName("message")
+ private final String message;
+
+ public GsonError() {
+ this("", "", "");
+ }
+
+ public GsonError(String error, String errorDescription, String message) {
+ this.error = error;
+ this.errorDescription = errorDescription;
+ this.message = message;
+ }
+
+ public static Optional<GsonError> parseOne(String json) {
+ Gson gson = new Gson();
+ return Optional.ofNullable(gson.fromJson(json, GsonError.class));
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public String getErrorDescription() {
+ return errorDescription;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/Project.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/Project.java
new file mode 100644
index 00000000000..f20c955c88d
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/Project.java
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client.gitlab;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Project {
+ // https://docs.gitlab.com/ee/api/projects.html#get-single-project
+ // https://docs.gitlab.com/ee/api/projects.html#list-all-projects
+ @SerializedName("id")
+ private long id;
+
+ @SerializedName("name")
+ private final String name;
+
+ @SerializedName("name_with_namespace")
+ private String nameWithNamespace;
+
+ @SerializedName("path")
+ private String path;
+
+ @SerializedName("path_with_namespace")
+ private final String pathWithNamespace;
+
+ @SerializedName("web_url")
+ private String webUrl;
+
+ public Project(String name, String pathWithNamespace) {
+ this.name = name;
+ this.pathWithNamespace = pathWithNamespace;
+ }
+
+ public Project() {
+ // http://stackoverflow.com/a/18645370/229031
+ this(0, "", "", "", "", "");
+ }
+
+ public Project(long id, String name, String nameWithNamespace, String path, String pathWithNamespace,
+ String webUrl) {
+ this.id = id;
+ this.name = name;
+ this.nameWithNamespace = nameWithNamespace;
+ this.path = path;
+ this.pathWithNamespace = pathWithNamespace;
+ this.webUrl = webUrl;
+ }
+
+
+ public static Project parseJson(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, Project.class);
+ }
+
+ public static List<Project> parseJsonArray(String json) {
+ Gson gson = new Gson();
+ return gson.fromJson(json, new TypeToken<LinkedList<Project>>() {
+ }.getType());
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getNameWithNamespace() {
+ return nameWithNamespace;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getPathWithNamespace() {
+ return pathWithNamespace;
+ }
+
+ public String getWebUrl() {
+ return webUrl;
+ }
+
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/ProjectList.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/ProjectList.java
new file mode 100644
index 00000000000..6548e669922
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/ProjectList.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client.gitlab;
+
+import java.util.List;
+
+public class ProjectList {
+
+ private final List<Project> projects;
+ private final int pageNumber;
+ private final int pageSize;
+ private final int total;
+
+ public ProjectList(List<Project> projects, int pageNumber, int pageSize, int total) {
+ this.projects = projects;
+ this.pageNumber = pageNumber;
+ this.pageSize = pageSize;
+ this.total = total;
+ }
+
+ public List<Project> getProjects() {
+ return projects;
+ }
+
+ public int getPageNumber() {
+ return pageNumber;
+ }
+
+ public int getPageSize() {
+ return pageSize;
+ }
+
+ public int getTotal() {
+ return total;
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/package-info.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/package-info.java
new file mode 100644
index 00000000000..54fa6284913
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.alm.client.gitlab;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/ConstantTimeoutConfiguration.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/ConstantTimeoutConfiguration.java
new file mode 100644
index 00000000000..ac261ee5120
--- /dev/null
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/ConstantTimeoutConfiguration.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client;
+
+public class ConstantTimeoutConfiguration implements TimeoutConfiguration {
+ private final long timeout;
+
+ public ConstantTimeoutConfiguration(long timeout) {
+ this.timeout = timeout;
+ }
+
+ @Override
+ public long getConnectTimeout() {
+ return timeout;
+ }
+
+ @Override
+ public long getReadTimeout() {
+ return timeout;
+ }
+}
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/TimeoutConfigurationImplTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/TimeoutConfigurationImplTest.java
new file mode 100644
index 00000000000..85f3f39d018
--- /dev/null
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/TimeoutConfigurationImplTest.java
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(DataProviderRunner.class)
+public class TimeoutConfigurationImplTest {
+ private final MapSettings settings = new MapSettings();
+ private final TimeoutConfigurationImpl underTest = new TimeoutConfigurationImpl(settings.asConfig());
+
+ @Test
+ public void getConnectTimeout_returns_10000_when_property_is_not_defined() {
+ assertThat(underTest.getConnectTimeout()).isEqualTo(30_000L);
+ }
+
+ @Test
+ @UseDataProvider("notALongPropertyValues")
+ public void getConnectTimeout_returns_10000_when_property_is_not_a_long(String notALong) {
+ settings.setProperty("sonar.alm.timeout.connect", notALong);
+
+ assertThat(underTest.getConnectTimeout()).isEqualTo(30_000L);
+ }
+
+ @Test
+ public void getConnectTimeout_returns_value_of_property() {
+ long expected = new Random().nextInt(9_456_789);
+ settings.setProperty("sonar.alm.timeout.connect", expected);
+
+ assertThat(underTest.getConnectTimeout()).isEqualTo(expected);
+ }
+
+ @Test
+ public void getReadTimeout_returns_10000_when_property_is_not_defined() {
+ assertThat(underTest.getReadTimeout()).isEqualTo(30_000L);
+ }
+
+ @Test
+ @UseDataProvider("notALongPropertyValues")
+ public void getReadTimeout_returns_10000_when_property_is_not_a_long(String notALong) {
+ settings.setProperty("sonar.alm.timeout.read", notALong);
+
+ assertThat(underTest.getReadTimeout()).isEqualTo(30_000L);
+ }
+
+ @Test
+ public void getReadTimeout_returns_value_of_property() {
+ long expected = new Random().nextInt(9_456_789);
+ settings.setProperty("sonar.alm.timeout.read", expected);
+
+ assertThat(underTest.getReadTimeout()).isEqualTo(expected);
+ }
+
+ @DataProvider
+ public static Object[][] notALongPropertyValues() {
+ return new Object[][] {
+ {"foo"},
+ {""},
+ {"12.5"}
+ };
+ }
+}
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java
new file mode 100644
index 00000000000..a19ee334320
--- /dev/null
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java
@@ -0,0 +1,270 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.alm.client.gitlab;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.alm.client.ConstantTimeoutConfiguration;
+import org.sonar.alm.client.TimeoutConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class GitlabHttpClientTest {
+
+ private final MockWebServer server = new MockWebServer();
+ private GitlabHttpClient underTest;
+ private String gitlabUrl;
+
+ @Before
+ public void prepare() throws IOException {
+ server.start();
+ String urlWithEndingSlash = server.url("").toString();
+ gitlabUrl = urlWithEndingSlash.substring(0, urlWithEndingSlash.length() - 1);
+
+ TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000);
+ underTest = new GitlabHttpClient(timeoutConfiguration);
+ }
+
+ @After
+ public void stopServer() throws IOException {
+ server.shutdown();
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_token_is_revoked() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(401)
+ .setBody("{\"error\":\"invalid_token\",\"error_description\":\"Token was revoked. You have to re-authorize from the user.\"}");
+ server.enqueue(response);
+
+ String gitlabUrl = this.gitlabUrl;
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Your GitLab token was revoked");
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_token_insufficient_scope() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(403)
+ .setBody("{\"error\":\"insufficient_scope\"," +
+ "\"error_description\":\"The request requires higher privileges than provided by the access token.\"," +
+ "\"scope\":\"api read_api\"}");
+ server.enqueue(response);
+
+ String gitlabUrl = this.gitlabUrl;
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Your GitLab token has insufficient scope");
+ }
+
+ @Test
+ public void should_throw_IllegalArgumentException_when_invalide_json_in_401_response() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(401)
+ .setBody("error in pat");
+ server.enqueue(response);
+
+ String gitlabUrl = this.gitlabUrl;
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid personal access token");
+ }
+
+ @Test
+ public void get_project() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setBody("{\n"
+ + " \"id\": 12345,\n"
+ + " \"name\": \"SonarQube example 1\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
+ + " \"path\": \"sonarqube-example-1\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
+ + " }");
+ server.enqueue(response);
+
+ assertThat(underTest.getProject(gitlabUrl, "pat", 12345L))
+ .extracting(Project::getId, Project::getName)
+ .containsExactly(12345L, "SonarQube example 1");
+ }
+
+ @Test
+ public void get_project_fail_if_non_json_payload() {
+ MockResponse response = new MockResponse()
+ .setResponseCode(200)
+ .setBody("non json payload");
+ server.enqueue(response);
+
+ String instanceUrl = gitlabUrl;
+ assertThatThrownBy(() -> underTest.getProject(instanceUrl, "pat", 12345L))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not parse GitLab answer to retrieve a project. Got a non-json payload as result.");
+ }
+
+ @Test
+ public void search_projects() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[\n"
+ + " {\n"
+ + " \"id\": 1,\n"
+ + " \"name\": \"SonarQube example 1\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n"
+ + " \"path\": \"sonarqube-example-1\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"id\": 2,\n"
+ + " \"name\": \"SonarQube example 2\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 2\",\n"
+ + " \"path\": \"sonarqube-example-2\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-2\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2\"\n"
+ + " },\n"
+ + " {\n"
+ + " \"id\": 3,\n"
+ + " \"name\": \"SonarQube example 3\",\n"
+ + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 3\",\n"
+ + " \"path\": \"sonarqube-example-3\",\n"
+ + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-3\",\n"
+ + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3\"\n"
+ + " }\n"
+ + "]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ projects.addHeader("X-Total", 3);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10);
+
+ assertThat(projectList.getPageNumber()).isEqualTo(1);
+ assertThat(projectList.getPageSize()).isEqualTo(10);
+ assertThat(projectList.getTotal()).isEqualTo(3);
+
+ assertThat(projectList.getProjects()).hasSize(3);
+ assertThat(projectList.getProjects()).extracting(
+ Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly(
+ tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1"),
+ tuple(2L, "SonarQube example 2", "SonarSource / SonarQube / SonarQube example 2", "sonarqube-example-2", "sonarsource/sonarqube/sonarqube-example-2",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-2"),
+ tuple(3L, "SonarQube example 3", "SonarSource / SonarQube / SonarQube example 3", "sonarqube-example-3", "sonarsource/sonarqube/sonarqube-example-3",
+ "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-3"));
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_projectName_param_should_be_encoded() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ projects.addHeader("X-Total", 0);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "&page=<script>alert('nasty')</script>", 1, 10);
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(projectList.getProjects()).isEmpty();
+ assertThat(gitlabUrlCall).isEqualTo(
+ server.url("")
+ + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=%26page%3D%3Cscript%3Ealert%28%27nasty%27%29%3C%2Fscript%3E&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_projectName_param_null_should_pass_empty_string() throws InterruptedException {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[]");
+ projects.addHeader("X-Page", 1);
+ projects.addHeader("X-Per-Page", 10);
+ projects.addHeader("X-Total", 0);
+ server.enqueue(projects);
+
+ ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", null, 1, 10);
+
+ RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS);
+ String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString();
+ assertThat(projectList.getProjects()).isEmpty();
+ assertThat(gitlabUrlCall).isEqualTo(
+ server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=&page=1&per_page=10");
+ assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET");
+ }
+
+ @Test
+ public void search_projects_fail_if_could_not_parse_pagination_number() {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[ ]");
+ projects.addHeader("X-Page", "bad-page-number");
+ projects.addHeader("X-Per-Page", "bad-per-page-number");
+ projects.addHeader("X-Total", "bad-total-number");
+ server.enqueue(projects);
+
+ String gitlabInstanceUrl = gitlabUrl;
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabInstanceUrl, "pat", "example", 1, 10))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not parse pagination number");
+ }
+
+ @Test
+ public void search_projects_fail_if_pagination_data_not_returned() {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(200)
+ .setBody("[ ]");
+ server.enqueue(projects);
+
+ String gitlabInstanceUrl = gitlabUrl;
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabInstanceUrl, "pat", "example", 1, 10))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Pagination data from GitLab response is missing");
+ }
+
+ @Test
+ public void throws_ISE_when_get_projects_not_http_200() {
+ MockResponse projects = new MockResponse()
+ .setResponseCode(500)
+ .setBody("test");
+ server.enqueue(projects);
+
+ String gitlabUrl = this.gitlabUrl;
+ assertThatThrownBy(() -> underTest.searchProjects(gitlabUrl, "pat", "example", 1, 2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Could not get projects from GitLab instance");
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
index 8f15ca75fcf..5bcfb2009c4 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModule.java
@@ -20,12 +20,15 @@
package org.sonar.server.almintegration.ws;
import org.sonar.core.platform.Module;
+import org.sonar.server.almintegration.ws.gitlab.ImportGitLabProjectAction;
+import org.sonar.server.almintegration.ws.gitlab.SearchGitlabReposAction;
public class AlmIntegrationsWSModule extends Module {
@Override
protected void configureModule() {
- // TODO:: move alm_integrations actions here
add(
+ ImportGitLabProjectAction.class,
+ SearchGitlabReposAction.class,
AlmIntegrationsWs.class);
}
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ImportHelper.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ImportHelper.java
new file mode 100644
index 00000000000..7f6fb0c1bbb
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ImportHelper.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.server.ws.Request;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.project.Visibility;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Projects.CreateWsResponse.Project;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonarqube.ws.Projects.CreateWsResponse;
+import static org.sonarqube.ws.Projects.CreateWsResponse.newBuilder;
+
+@ServerSide
+public class ImportHelper {
+ public static final String PARAM_ALM_SETTING = "almSetting";
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+
+ public ImportHelper(DbClient dbClient, UserSession userSession) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ }
+
+ public void checkProvisionProjectPermission() {
+ userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS);
+ }
+
+ public AlmSettingDto getAlmSetting(Request request) {
+ String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING);
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return dbClient.almSettingDao().selectByKey(dbSession, almSettingKey)
+ .orElseThrow(() -> new NotFoundException(String.format("ALM Setting '%s' not found", almSettingKey)));
+ }
+ }
+
+ public String getUserUuid() {
+ return requireNonNull(userSession.getUuid(), "User UUID cannot be null");
+ }
+
+ public static CreateWsResponse toCreateResponse(ComponentDto componentDto) {
+ return newBuilder()
+ .setProject(Project.newBuilder()
+ .setKey(componentDto.getDbKey())
+ .setName(componentDto.name())
+ .setQualifier(componentDto.qualifier())
+ .setVisibility(Visibility.getLabel(componentDto.isPrivate())))
+ .build();
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java
new file mode 100644
index 00000000000..b116ab2e222
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws.gitlab;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Optional;
+import org.sonar.alm.client.gitlab.GitlabHttpClient;
+import org.sonar.alm.client.gitlab.Project;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
+import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.component.ComponentUpdater;
+import org.sonar.server.project.ProjectDefaultVisibility;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.Projects.CreateWsResponse;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.server.component.NewComponent.newComponentBuilder;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+
+public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
+
+ public static final String PARAM_GITLAB_PROJECT_ID = "gitlabProjectId";
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final ProjectDefaultVisibility projectDefaultVisibility;
+ private final GitlabHttpClient gitlabHttpClient;
+ private final ComponentUpdater componentUpdater;
+ private final UuidFactory uuidFactory;
+ private final ImportHelper importHelper;
+
+ public ImportGitLabProjectAction(DbClient dbClient, UserSession userSession,
+ ProjectDefaultVisibility projectDefaultVisibility, GitlabHttpClient gitlabHttpClient,
+ ComponentUpdater componentUpdater, UuidFactory uuidFactory, ImportHelper importHelper) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.projectDefaultVisibility = projectDefaultVisibility;
+ this.gitlabHttpClient = gitlabHttpClient;
+ this.componentUpdater = componentUpdater;
+ this.uuidFactory = uuidFactory;
+ this.importHelper = importHelper;
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction("import_gitlab_project")
+ .setDescription("Import a GitLab project to SonarQube, creating a new project and configuring MR decoration<br/>" +
+ "Requires the 'Create Projects' permission")
+ .setPost(true)
+ .setSince("8.5")
+ .setHandler(this);
+
+ action.createParam(ImportHelper.PARAM_ALM_SETTING)
+ .setRequired(true)
+ .setDescription("ALM setting key");
+ action.createParam(PARAM_GITLAB_PROJECT_ID)
+ .setRequired(true)
+ .setDescription("GitLab project ID");
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ CreateWsResponse createResponse = doHandle(request);
+ writeProtobuf(createResponse, request, response);
+ }
+
+ private CreateWsResponse doHandle(Request request) {
+ importHelper.checkProvisionProjectPermission();
+ AlmSettingDto almSettingDto = importHelper.getAlmSetting(request);
+ String userUuid = importHelper.getUserUuid();
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto);
+ String pat = almPatDto.map(AlmPatDto::getPersonalAccessToken)
+ .orElseThrow(() -> new IllegalArgumentException(String.format("personal access token for '%s' is missing", almSettingDto.getKey())));
+
+ long gitlabProjectId = request.mandatoryParamAsLong(PARAM_GITLAB_PROJECT_ID);
+
+ String url = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null");
+ Project gitlabProject = gitlabHttpClient.getProject(url, pat, gitlabProjectId);
+
+ ComponentDto componentDto = createProject(dbSession, gitlabProject);
+ populateMRSetting(dbSession, gitlabProjectId, componentDto, almSettingDto);
+
+ return ImportHelper.toCreateResponse(componentDto);
+ }
+ }
+
+ private void populateMRSetting(DbSession dbSession, Long gitlabProjectId, ComponentDto componentDto, AlmSettingDto almSettingDto) {
+ dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, new ProjectAlmSettingDto()
+ .setProjectUuid(componentDto.projectUuid())
+ .setAlmSettingUuid(almSettingDto.getUuid())
+ .setAlmRepo(gitlabProjectId.toString())
+ .setAlmSlug(null)
+ .setMonorepo(false));
+ dbSession.commit();
+ }
+
+ private ComponentDto createProject(DbSession dbSession, Project gitlabProject) {
+ boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
+ String sqProjectKey = generateProjectKey(gitlabProject.getPathWithNamespace(), uuidFactory.create());
+
+ return componentUpdater.create(dbSession, newComponentBuilder()
+ .setKey(sqProjectKey)
+ .setName(gitlabProject.getName())
+ .setPrivate(visibility)
+ .setQualifier(PROJECT)
+ .build(),
+ userSession.getUuid());
+ }
+
+ @VisibleForTesting
+ String generateProjectKey(String pathWithNamespace, String uuid) {
+ String sqProjectKey = pathWithNamespace + "_" + uuid;
+
+ if (sqProjectKey.length() > 250) {
+ sqProjectKey = sqProjectKey.substring(sqProjectKey.length() - 250);
+ }
+
+ return sqProjectKey.replace("/", "_");
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java
new file mode 100644
index 00000000000..b3214bbd458
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposAction.java
@@ -0,0 +1,197 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws.gitlab;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.sonar.alm.client.gitlab.GitlabHttpClient;
+import org.sonar.alm.client.gitlab.Project;
+import org.sonar.alm.client.gitlab.ProjectList;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.AlmIntegrations;
+import org.sonarqube.ws.AlmIntegrations.GitlabRepository;
+import org.sonarqube.ws.Common.Paging;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+
+public class SearchGitlabReposAction implements AlmIntegrationsWsAction {
+
+ private static final String PARAM_ALM_SETTING = "almSetting";
+ private static final String PARAM_PROJECT_NAME = "projectName";
+ private static final int DEFAULT_PAGE_SIZE = 20;
+ private static final int MAX_PAGE_SIZE = 500;
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final GitlabHttpClient gitlabHttpClient;
+
+ public SearchGitlabReposAction(DbClient dbClient, UserSession userSession, GitlabHttpClient gitlabHttpClient) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.gitlabHttpClient = gitlabHttpClient;
+ }
+
+ @Override
+ public void define(WebService.NewController context) {
+ WebService.NewAction action = context.createAction("search_gitlab_repos")
+ .setDescription("Search the GitLab projects.<br/>" +
+ "Requires the 'Create Projects' permission")
+ .setPost(false)
+ .setSince("8.5")
+ .setHandler(this);
+
+ action.createParam(PARAM_ALM_SETTING)
+ .setRequired(true)
+ .setMaximumLength(200)
+ .setDescription("ALM setting key");
+ action.createParam(PARAM_PROJECT_NAME)
+ .setRequired(false)
+ .setMaximumLength(200)
+ .setDescription("Project name filter");
+
+ action.addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
+
+ action.setResponseExample(getClass().getResource("search_gitlab_repos.json"));
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ AlmIntegrations.SearchGitlabReposWsResponse wsResponse = doHandle(request);
+ writeProtobuf(wsResponse, request, response);
+ }
+
+ private AlmIntegrations.SearchGitlabReposWsResponse doHandle(Request request) {
+ String almSettingKey = request.mandatoryParam(PARAM_ALM_SETTING);
+ String projectName = request.param(PARAM_PROJECT_NAME);
+
+ int pageNumber = request.mandatoryParamAsInt("p");
+ int pageSize = request.mandatoryParamAsInt("ps");
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ userSession.checkLoggedIn().checkPermission(PROVISION_PROJECTS);
+
+ String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null");
+ AlmSettingDto almSettingDto = dbClient.almSettingDao().selectByKey(dbSession, almSettingKey)
+ .orElseThrow(() -> new NotFoundException(String.format("ALM Setting '%s' not found", almSettingKey)));
+ Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto);
+
+ String personalAccessToken = almPatDto.map(AlmPatDto::getPersonalAccessToken).orElseThrow(() -> new IllegalArgumentException("No personal access token found"));
+ String gitlabUrl = requireNonNull(almSettingDto.getUrl(), "ALM url cannot be null");
+
+ ProjectList gitlabProjectList = gitlabHttpClient
+ .searchProjects(gitlabUrl, personalAccessToken, projectName, pageNumber, pageSize);
+
+ Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId = getSqProjectsKeyByGitlabProjectId(dbSession, almSettingDto, gitlabProjectList);
+
+ List<GitlabRepository> gitlabRepositories = gitlabProjectList.getProjects().stream()
+ .map(project -> toGitlabRepository(project, sqProjectsKeyByGitlabProjectId))
+ .collect(toList());
+
+ return AlmIntegrations.SearchGitlabReposWsResponse.newBuilder()
+ .addAllRepositories(gitlabRepositories)
+ .setPaging(Paging.newBuilder()
+ .setPageIndex(gitlabProjectList.getPageNumber())
+ .setPageSize(gitlabProjectList.getPageSize())
+ .setTotal(gitlabProjectList.getTotal())
+ .build())
+ .build();
+ }
+ }
+
+ private Map<String, ProjectKeyName> getSqProjectsKeyByGitlabProjectId(DbSession dbSession, AlmSettingDto almSettingDto,
+ ProjectList gitlabProjectList) {
+ Set<String> gitlabProjectIds = gitlabProjectList.getProjects().stream().map(Project::getId).map(String::valueOf)
+ .collect(toSet());
+ Map<String, ProjectAlmSettingDto> projectAlmSettingDtos = dbClient.projectAlmSettingDao()
+ .selectByAlmSettingAndRepos(dbSession, almSettingDto, gitlabProjectIds)
+ .stream().collect(Collectors.toMap(ProjectAlmSettingDto::getProjectUuid, Function.identity()));
+
+ return dbClient.projectDao().selectByUuids(dbSession, projectAlmSettingDtos.keySet())
+ .stream()
+ .collect(Collectors.toMap(projectDto -> projectAlmSettingDtos.get(projectDto.getUuid()).getAlmRepo(),
+ p -> new ProjectKeyName(p.getKey(), p.getName()), resolveNameCollisionOperatorByNaturalOrder()));
+ }
+
+ private static BinaryOperator<ProjectKeyName> resolveNameCollisionOperatorByNaturalOrder() {
+ return (a, b) -> b.key.compareTo(a.key) > 0 ? a : b;
+ }
+
+ private static GitlabRepository toGitlabRepository(Project project, Map<String, ProjectKeyName> sqProjectsKeyByGitlabProjectId) {
+ String name = project.getName();
+ String pathName = removeLastOccurrenceOfString(project.getNameWithNamespace(), " / " + name);
+
+ String slug = project.getPath();
+ String pathSlug = removeLastOccurrenceOfString(project.getPathWithNamespace(), "/" + slug);
+
+ GitlabRepository.Builder builder = GitlabRepository.newBuilder()
+ .setId(project.getId())
+ .setName(name)
+ .setPathName(pathName)
+ .setSlug(slug)
+ .setPathSlug(pathSlug)
+ .setUrl(project.getWebUrl());
+
+ String projectIdAsString = String.valueOf(project.getId());
+ Optional.ofNullable(sqProjectsKeyByGitlabProjectId.get(projectIdAsString))
+ .ifPresent(p -> builder
+ .setSqProjectKey(p.key)
+ .setSqProjectName(p.name));
+
+ return builder.build();
+ }
+
+ private static String removeLastOccurrenceOfString(String string, String stringToRemove) {
+ StringBuilder resultString = new StringBuilder(string);
+ int index = resultString.lastIndexOf(stringToRemove);
+ if (index > -1) {
+ resultString.delete(index, string.length() + index);
+ }
+ return resultString.toString();
+ }
+
+ static class ProjectKeyName {
+ String key;
+ String name;
+
+ ProjectKeyName(String key, String name) {
+ this.key = key;
+ this.name = name;
+ }
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/package-info.java
new file mode 100644
index 00000000000..82dc3b7e8a4
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.almintegration.ws.gitlab;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/package-info.java
new file mode 100644
index 00000000000..d2901036b39
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.almintegration.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/gitlab/search_gitlab_repos.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/gitlab/search_gitlab_repos.json
new file mode 100644
index 00000000000..a1247e0ac99
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/almintegration/ws/gitlab/search_gitlab_repos.json
@@ -0,0 +1,33 @@
+{
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 3,
+ "total": 10
+ },
+ "repositories": [
+ {
+ "id": 1,
+ "name": "Gitlab repo name 1",
+ "pathName": "Group",
+ "slug": "gitlab-repo-name-1",
+ "pathSlug": "group",
+ "url": "https://example.gitlab.com/group/gitlab-repo-name-1"
+ },
+ {
+ "id": 2,
+ "name": "Gitlab repo name 2",
+ "pathName": "Group",
+ "slug": "gitlab-repo-name-2",
+ "pathSlug": "group",
+ "url": "https://example.gitlab.com/group/gitlab-repo-name-2"
+ },
+ {
+ "id": 3,
+ "name": "Gitlab repo name 3",
+ "pathName": "Group",
+ "slug": "gitlab-repo-name-3",
+ "pathSlug": "group",
+ "url": "https://example.gitlab.com/group/gitlab-repo-name-3"
+ }
+ ]
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModuleTest.java
new file mode 100644
index 00000000000..555d0b1345f
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWSModuleTest.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AlmIntegrationsWSModuleTest {
+
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new AlmIntegrationsWSModule().configure(container);
+ assertThat(container.size()).isPositive();
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWsTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWsTest.java
new file mode 100644
index 00000000000..382717c52c6
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/AlmIntegrationsWsTest.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws;
+
+import java.util.Collections;
+import org.junit.Test;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AlmIntegrationsWsTest {
+ private final AlmIntegrationsWs underTest = new AlmIntegrationsWs(Collections.singletonList(new AlmIntegrationsWsAction() {
+ @Override
+ public void handle(Request request, Response response) {
+ // nothing to do
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+ controller.createAction("foo")
+ .setHandler((request, response) -> {
+ throw new UnsupportedOperationException("not implemented");
+ });
+ }
+ }));
+
+ @Test
+ public void define_ws() {
+ WebService.Context context = new WebService.Context();
+
+ underTest.define(context);
+
+ WebService.Controller controller = context.controller("api/alm_integrations");
+ assertThat(controller).isNotNull();
+ assertThat(controller.description()).isNotEmpty();
+ assertThat(controller.actions()).hasSize(1);
+
+ WebService.Action fooAction = controller.action("foo");
+ assertThat(fooAction).isNotNull();
+ assertThat(fooAction.handler()).isNotNull();
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java
new file mode 100644
index 00000000000..55c0d0e6ea6
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws.gitlab;
+
+import java.util.Optional;
+import java.util.stream.IntStream;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.alm.client.gitlab.GitlabHttpClient;
+import org.sonar.alm.client.gitlab.Project;
+import org.sonar.api.utils.System2;
+import org.sonar.core.i18n.I18n;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbTester;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.component.ComponentUpdater;
+import org.sonar.server.es.TestProjectIndexers;
+import org.sonar.server.favorite.FavoriteUpdater;
+import org.sonar.server.permission.PermissionTemplateService;
+import org.sonar.server.project.ProjectDefaultVisibility;
+import org.sonar.server.project.Visibility;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Projects;
+
+import static java.util.stream.Collectors.joining;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+import static org.sonar.server.tester.UserSessionRule.standalone;
+
+public class ImportGitLabProjectActionTest {
+
+ private final System2 system2 = mock(System2.class);
+
+ @Rule
+ public UserSessionRule userSession = standalone();
+
+ @Rule
+ public DbTester db = DbTester.create(system2);
+
+ private final ComponentUpdater componentUpdater = new ComponentUpdater(db.getDbClient(), mock(I18n.class), System2.INSTANCE,
+ mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory());
+
+ private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
+ private final UuidFactory uuidFactory = mock(UuidFactory.class);
+ private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
+ private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
+ private final ImportGitLabProjectAction importGitLabProjectAction = new ImportGitLabProjectAction(
+ db.getDbClient(), userSession, projectDefaultVisibility, gitlabHttpClient, componentUpdater, uuidFactory, importHelper);
+ private final WsActionTester ws = new WsActionTester(importGitLabProjectAction);
+
+ @Before
+ public void before() {
+ when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
+ }
+
+ @Test
+ public void import_project() {
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting();
+ db.almPats().insert(dto -> {
+ dto.setAlmSettingUuid(almSetting.getUuid());
+ dto.setUserUuid(user.getUuid());
+ dto.setPersonalAccessToken("PAT");
+ });
+ Project project = getGitlabProject();
+ when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project);
+ when(uuidFactory.create()).thenReturn("uuid");
+
+ Projects.CreateWsResponse response = ws.newRequest()
+ .setParam("almSetting", almSetting.getKey())
+ .setParam("gitlabProjectId", "12345")
+ .executeProtobuf(Projects.CreateWsResponse.class);
+
+ verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L);
+
+ Projects.CreateWsResponse.Project result = response.getProject();
+ assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid");
+ assertThat(result.getName()).isEqualTo(project.getName());
+
+ Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
+ assertThat(projectDto).isPresent();
+ assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent();
+ }
+
+ @Test
+ public void generate_project_key_less_than_250() {
+ String name = "abcdeert";
+ assertThat(importGitLabProjectAction.generateProjectKey(name, "uuid")).isEqualTo("abcdeert_uuid");
+ }
+
+ @Test
+ public void generate_project_key_equal_250() {
+ String name = IntStream.range(0, 245).mapToObj(i -> "a").collect(joining());
+ String projectKey = importGitLabProjectAction.generateProjectKey(name, "uuid");
+ assertThat(projectKey)
+ .hasSize(250)
+ .isEqualTo(name + "_uuid");
+
+ }
+
+ @Test
+ public void generate_project_key_more_than_250() {
+ String name = IntStream.range(0, 250).mapToObj(i -> "a").collect(joining());
+ String projectKey = importGitLabProjectAction.generateProjectKey(name, "uuid");
+ assertThat(projectKey)
+ .hasSize(250)
+ .isEqualTo(name.substring(5) + "_uuid");
+ }
+
+ @Test
+ public void generate_project_key_containing_slash() {
+ String name = "a/b/c";
+ assertThat(importGitLabProjectAction.generateProjectKey(name, "uuid")).isEqualTo("a_b_c_uuid");
+ }
+
+ private Project getGitlabProject() {
+ return new Project(randomAlphanumeric(5), randomAlphanumeric(5));
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionTest.java
new file mode 100644
index 00000000000..d1d728efa65
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/SearchGitlabReposActionTest.java
@@ -0,0 +1,282 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.
+ */
+package org.sonar.server.almintegration.ws.gitlab;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.alm.client.gitlab.GitlabHttpClient;
+import org.sonar.alm.client.gitlab.Project;
+import org.sonar.alm.client.gitlab.ProjectList;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbTester;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.AlmIntegrations;
+import org.sonarqube.ws.AlmIntegrations.GitlabRepository;
+import org.sonarqube.ws.AlmIntegrations.SearchGitlabReposWsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto;
+import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
+
+public class SearchGitlabReposActionTest {
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+ @Rule
+ public DbTester db = DbTester.create();
+
+ private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
+ private final WsActionTester ws = new WsActionTester(new SearchGitlabReposAction(db.getDbClient(), userSession,
+ gitlabHttpClient));
+
+ @Test
+ public void list_gitlab_repos() {
+ Project gitlabProject1 = new Project(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1");
+ Project gitlabProject2 = new Project(2L, "repoName2", "path1 / repoName2", "repo-slug-2", "path-1/repo-slug-2", "url-2");
+ Project gitlabProject3 = new Project(3L, "repoName3", "repoName3 / repoName3", "repo-slug-3", "repo-slug-3/repo-slug-3", "url-3");
+ Project gitlabProject4 = new Project(4L, "repoName4", "repoName4 / repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4/repo-slug-4", "url-4");
+ when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
+ .thenReturn(
+ new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3, gitlabProject4), 1, 10, 4));
+
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting();
+ db.almPats().insert(dto -> {
+ dto.setAlmSettingUuid(almSetting.getUuid());
+ dto.setUserUuid(user.getUuid());
+ dto.setPersonalAccessToken("some-pat");
+ });
+ ProjectDto projectDto = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto);
+
+ AlmIntegrations.SearchGitlabReposWsResponse response = ws.newRequest()
+ .setParam("almSetting", almSetting.getKey())
+ .executeProtobuf(SearchGitlabReposWsResponse.class);
+
+ assertThat(response.getRepositoriesCount()).isEqualTo(4);
+
+ assertThat(response.getRepositoriesList())
+ .extracting(GitlabRepository::getId,
+ GitlabRepository::getName,
+ GitlabRepository::getPathName,
+ GitlabRepository::getSlug,
+ GitlabRepository::getPathSlug,
+ GitlabRepository::getUrl,
+ GitlabRepository::hasSqProjectKey,
+ GitlabRepository::hasSqProjectName)
+ .containsExactlyInAnyOrder(
+ tuple(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1", false, false),
+ tuple(2L, "repoName2", "path1", "repo-slug-2", "path-1", "url-2", false, false),
+ tuple(3L, "repoName3", "repoName3", "repo-slug-3", "repo-slug-3", "url-3", false, false),
+ tuple(4L, "repoName4", "repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4", "url-4", false, false));
+ }
+
+ @Test
+ public void list_gitlab_repos_some_projects_already_set_up() {
+ Project gitlabProject1 = new Project(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1");
+ Project gitlabProject2 = new Project(2L, "repoName2", "path1 / repoName2", "repo-slug-2", "path-1/repo-slug-2", "url-2");
+ Project gitlabProject3 = new Project(3L, "repoName3", "repoName3 / repoName3", "repo-slug-3", "repo-slug-3/repo-slug-3", "url-3");
+ Project gitlabProject4 = new Project(4L, "repoName4", "repoName4 / repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4/repo-slug-4", "url-4");
+ when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
+ .thenReturn(
+ new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3, gitlabProject4), 1, 10, 4));
+
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting();
+ db.almPats().insert(dto -> {
+ dto.setAlmSettingUuid(almSetting.getUuid());
+ dto.setUserUuid(user.getUuid());
+ dto.setPersonalAccessToken("some-pat");
+ });
+ ProjectDto projectDto1 = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto1);
+
+ ProjectDto projectDto2 = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto2, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo("2"));
+
+ ProjectDto projectDto3 = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto3, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo("3"));
+
+ ProjectDto projectDto4 = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto4, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo("3"));
+
+ AlmIntegrations.SearchGitlabReposWsResponse response = ws.newRequest()
+ .setParam("almSetting", almSetting.getKey())
+ .executeProtobuf(SearchGitlabReposWsResponse.class);
+
+ assertThat(response.getRepositoriesCount()).isEqualTo(4);
+
+ assertThat(response.getRepositoriesList())
+ .extracting(GitlabRepository::getId,
+ GitlabRepository::getName,
+ GitlabRepository::getPathName,
+ GitlabRepository::getSlug,
+ GitlabRepository::getPathSlug,
+ GitlabRepository::getUrl,
+ GitlabRepository::getSqProjectKey,
+ GitlabRepository::getSqProjectName)
+ .containsExactlyInAnyOrder(
+ tuple(1L, "repoName1", "repoNamePath1", "repo-slug-1", "repo-path-slug-1", "url-1", "", ""),
+ tuple(2L, "repoName2", "path1", "repo-slug-2", "path-1", "url-2", projectDto2.getKey(), projectDto2.getName()),
+ tuple(3L, "repoName3", "repoName3", "repo-slug-3", "repo-slug-3", "url-3", projectDto3.getKey(), projectDto3.getName()),
+ tuple(4L, "repoName4", "repoName4 / repoName4", "repo-slug-4", "repo-slug-4/repo-slug-4", "url-4", "", ""));
+ }
+
+ @Test
+ public void return_empty_list_when_no_gitlab_projects() {
+ when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt())).thenReturn(new ProjectList(new LinkedList<>(), 1, 10, 0));
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmSettingDto almSetting = db.almSettings().insertBitbucketAlmSetting();
+ db.almPats().insert(dto -> {
+ dto.setAlmSettingUuid(almSetting.getUuid());
+ dto.setUserUuid(user.getUuid());
+ });
+ ProjectDto projectDto = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto);
+
+ AlmIntegrations.SearchGitlabReposWsResponse response = ws.newRequest()
+ .setParam("almSetting", almSetting.getKey())
+ .executeProtobuf(SearchGitlabReposWsResponse.class);
+
+ assertThat(response.getRepositoriesList()).isEmpty();
+ }
+
+ @Test
+ public void check_pat_is_missing() {
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting();
+
+ TestRequest request = ws.newRequest()
+ .setParam("almSetting", almSetting.getKey());
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("No personal access token found");
+ }
+
+ @Test
+ public void fail_when_alm_setting_not_found() {
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmPatDto almPatDto = newAlmPatDto();
+ db.getDbClient().almPatDao().insert(db.getSession(), almPatDto);
+
+ TestRequest request = ws.newRequest()
+ .setParam("almSetting", "testKey");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(NotFoundException.class)
+ .hasMessage("ALM Setting 'testKey' not found");
+ }
+
+ @Test
+ public void fail_when_not_logged_in() {
+ TestRequest request = ws.newRequest()
+ .setParam("almSetting", "anyvalue");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(UnauthorizedException.class)
+ .hasMessage("Authentication is required");
+ }
+
+ @Test
+ public void fail_when_no_creation_project_permission() {
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user);
+
+ TestRequest request = ws.newRequest()
+ .setParam("almSetting", "anyvalue");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(ForbiddenException.class)
+ .hasMessage("Insufficient privileges");
+ }
+
+ @Test
+ public void verify_response_example() {
+ Project gitlabProject1 = new Project(1L, "Gitlab repo name 1", "Group / Gitlab repo name 1", "gitlab-repo-name-1", "group/gitlab-repo-name-1",
+ "https://example.gitlab.com/group/gitlab-repo-name-1");
+ Project gitlabProject2 = new Project(2L, "Gitlab repo name 2", "Group / Gitlab repo name 2", "gitlab-repo-name-2", "group/gitlab-repo-name-2",
+ "https://example.gitlab.com/group/gitlab-repo-name-2");
+ Project gitlabProject3 = new Project(3L, "Gitlab repo name 3", "Group / Gitlab repo name 3", "gitlab-repo-name-3", "group/gitlab-repo-name-3",
+ "https://example.gitlab.com/group/gitlab-repo-name-3");
+ when(gitlabHttpClient.searchProjects(any(), any(), any(), anyInt(), anyInt()))
+ .thenReturn(
+ new ProjectList(Arrays.asList(gitlabProject1, gitlabProject2, gitlabProject3), 1, 3, 10));
+
+ UserDto user = db.users().insertUser();
+ userSession.logIn(user).addPermission(PROVISION_PROJECTS);
+ AlmSettingDto almSetting = db.almSettings().insertGitlabAlmSetting();
+ db.almPats().insert(dto -> {
+ dto.setAlmSettingUuid(almSetting.getUuid());
+ dto.setUserUuid(user.getUuid());
+ dto.setPersonalAccessToken("some-pat");
+ });
+ ProjectDto projectDto = db.components().insertPrivateProjectDto();
+ db.almSettings().insertGitlabProjectAlmSetting(almSetting, projectDto);
+
+ WebService.Action def = ws.getDef();
+ String responseExample = def.responseExampleAsString();
+
+ assertThat(responseExample).isNotBlank();
+
+ ws.newRequest()
+ .setParam("almSetting", almSetting.getKey())
+ .execute().assertJson(
+ responseExample);
+ }
+
+ @Test
+ public void definition() {
+ WebService.Action def = ws.getDef();
+
+ assertThat(def.since()).isEqualTo("8.5");
+ assertThat(def.isPost()).isFalse();
+ assertThat(def.params())
+ .extracting(WebService.Param::key, WebService.Param::isRequired)
+ .containsExactlyInAnyOrder(
+ tuple("almSetting", true),
+ tuple("p", false),
+ tuple("ps", false),
+ tuple("projectName", false));
+ }
+
+}
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 962bafff35c..bed805ac5af 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -20,6 +20,8 @@
package org.sonar.server.platform.platformlevel;
import java.util.List;
+import org.sonar.alm.client.TimeoutConfigurationImpl;
+import org.sonar.alm.client.gitlab.GitlabHttpClient;
import org.sonar.api.profiles.AnnotationProfileParser;
import org.sonar.api.profiles.XMLProfileParser;
import org.sonar.api.profiles.XMLProfileSerializer;
@@ -40,6 +42,7 @@ import org.sonar.core.extension.CoreExtensionsInstaller;
import org.sonar.core.platform.ComponentContainer;
import org.sonar.core.platform.PlatformEditionProvider;
import org.sonar.server.almintegration.ws.AlmIntegrationsWSModule;
+import org.sonar.server.almintegration.ws.ImportHelper;
import org.sonar.server.almsettings.MultipleAlmFeatureProvider;
import org.sonar.server.authentication.AuthenticationModule;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationHandler;
@@ -486,6 +489,9 @@ public class PlatformLevel4 extends PlatformLevel {
PluginsWs.class,
// ALM integrations
+ TimeoutConfigurationImpl.class,
+ ImportHelper.class,
+ GitlabHttpClient.class,
AlmIntegrationsWSModule.class,
// Branch