aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-alm-client/src
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/sonar-alm-client/src
parent67ff6070437876c08adafb5ac244252d81c315e7 (diff)
downloadsonarqube-044247d48921aa5486c4cea1e2719207ff250fc9.tar.gz
sonarqube-044247d48921aa5486c4cea1e2719207ff250fc9.zip
SONAR-14371 Move gitlab http client to CE
Diffstat (limited to 'server/sonar-alm-client/src')
-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
10 files changed, 925 insertions, 0 deletions
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");
+ }
+}