aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-alm-client/src
diff options
context:
space:
mode:
authorZipeng WU <zipeng.wu@sonarsource.com>2021-01-26 18:22:53 +0100
committersonartech <sonartech@sonarsource.com>2021-02-04 20:07:07 +0000
commitc364b859dd4b1610dd7537b77f1152e620c5525a (patch)
treea3fbe7c270df158c7161d9875551a59be560de35 /server/sonar-alm-client/src
parent1f7132f59445a3a5a7c51724b770396dd8192c5e (diff)
downloadsonarqube-c364b859dd4b1610dd7537b77f1152e620c5525a.tar.gz
sonarqube-c364b859dd4b1610dd7537b77f1152e620c5525a.zip
SONAR-14371 move Azure Devops client and WS actions to CE
Diffstat (limited to 'server/sonar-alm-client/src')
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java158
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureError.java41
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProject.java48
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProjectList.java56
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepo.java63
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepoList.java49
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java273
7 files changed, 688 insertions, 0 deletions
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java
new file mode 100644
index 00000000000..c4f2f4f0c57
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/AzureDevOpsHttpClient.java
@@ -0,0 +1,158 @@
+/*
+ * 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.azure;
+
+import com.google.common.base.Strings;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.apache.commons.codec.binary.Base64;
+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 org.sonar.api.internal.apachecommons.lang.StringUtils.isBlank;
+import static org.sonar.api.internal.apachecommons.lang.StringUtils.substringBeforeLast;
+
+@ServerSide
+public class AzureDevOpsHttpClient {
+ private static final Logger LOG = Loggers.get(AzureDevOpsHttpClient.class);
+
+ protected static final String GET = "GET";
+ protected static final String UNABLE_TO_CONTACT_AZURE_SERVER = "Unable to contact Azure DevOps server";
+
+ protected final OkHttpClient client;
+
+ public AzureDevOpsHttpClient(TimeoutConfiguration timeoutConfiguration) {
+ client = new OkHttpClientBuilder()
+ .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
+ .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
+ .build();
+ }
+
+ public GsonAzureProjectList getProjects(String serverUrl, String token) {
+ String url = String.format("%s/_apis/projects", getTrimmedUrl(serverUrl));
+ LOG.debug(String.format("get projects : [%s]", url));
+ return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureProjectList.class));
+ }
+
+
+ public GsonAzureRepoList getRepos(String serverUrl, String token, @Nullable String projectName) {
+ String url;
+ if (projectName != null && !projectName.isEmpty()) {
+ url = String.format("%s/%s/_apis/git/repositories", getTrimmedUrl(serverUrl), projectName);
+ } else {
+ url = String.format("%s/_apis/git/repositories", getTrimmedUrl(serverUrl));
+ }
+ LOG.debug(String.format("get repos : [%s]", url));
+ return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepoList.class));
+ }
+
+ public GsonAzureRepo getRepo(String serverUrl, String token, String projectName, String repositoryName) {
+ String url = String.format("%s/%s/_apis/git/repositories/%s", getTrimmedUrl(serverUrl), projectName, repositoryName);
+ LOG.debug(String.format("get repo : [%s]", url));
+ return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepo.class));
+ }
+
+ protected <G> G doGet(String token, String url, Function<Response, G> handler) {
+ Request request = prepareRequestWithToken(token, GET, url, null);
+ return doCall(request, handler);
+ }
+
+ protected <G> G doCall(Request request, Function<Response, G> handler) {
+ try (Response response = client.newCall(request).execute()) {
+ checkResponseIsSuccessful(response);
+ return handler.apply(response);
+ } catch (JsonSyntaxException e) {
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER + ", got an unexpected response", e);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER, e);
+ }
+ }
+
+ protected static Request prepareRequestWithToken(String token, String method, String url, @Nullable RequestBody body) {
+ return new Request.Builder()
+ .method(method, body)
+ .url(url)
+ .addHeader("Authorization", encodeToken("accessToken:" + token))
+ .build();
+ }
+
+ protected static void checkResponseIsSuccessful(Response response) throws IOException {
+ if (!response.isSuccessful()) {
+ LOG.debug(UNABLE_TO_CONTACT_AZURE_SERVER + ": {} {}", response.request().url().toString(), response.code());
+ if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ throw new IllegalArgumentException("Invalid personal access token");
+ }
+ ResponseBody responseBody = response.body();
+ String body = responseBody == null ? "" : responseBody.string();
+ String errorMessage = generateErrorMessage(body, UNABLE_TO_CONTACT_AZURE_SERVER);
+ LOG.info(String.format("Azure API call to [%s] failed with %s http code. Azure response content : [%s]", response.request().url().toString(), response.code(), body));
+ throw new IllegalArgumentException(errorMessage);
+ }
+ }
+
+ protected static String generateErrorMessage(String body, String defaultMessage) {
+ GsonAzureError gsonAzureError = null;
+ try {
+ gsonAzureError = buildGson().fromJson(body, GsonAzureError.class);
+ } catch (JsonSyntaxException e) {
+ // not a json payload, ignore the error
+ }
+ if (gsonAzureError != null && !Strings.isNullOrEmpty(gsonAzureError.message())) {
+ return defaultMessage + " : " + gsonAzureError.message();
+ } else {
+ return defaultMessage;
+ }
+ }
+
+ protected static String getTrimmedUrl(String rawUrl) {
+ if (isBlank(rawUrl)) {
+ return rawUrl;
+ }
+ if (rawUrl.endsWith("/")) {
+ return substringBeforeLast(rawUrl, "/");
+ }
+ return rawUrl;
+ }
+
+ protected static String encodeToken(String token) {
+ return String.format("BASIC %s", Base64.encodeBase64String(token.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ protected static Gson buildGson() {
+ return new GsonBuilder()
+ .create();
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureError.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureError.java
new file mode 100644
index 00000000000..e4e91799e24
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureError.java
@@ -0,0 +1,41 @@
+/*
+ * 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.azure;
+
+import com.google.gson.annotations.SerializedName;
+import javax.annotation.Nullable;
+
+public class GsonAzureError {
+ @SerializedName("message")
+ private final String message;
+
+ public GsonAzureError(@Nullable String message) {
+ this.message = message;
+ }
+
+ public GsonAzureError() {
+ // http://stackoverflow.com/a/18645370/229031
+ this(null);
+ }
+
+ public String message() {
+ return message;
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProject.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProject.java
new file mode 100644
index 00000000000..c206f02d61d
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProject.java
@@ -0,0 +1,48 @@
+/*
+ * 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.azure;
+
+import com.google.gson.annotations.SerializedName;
+
+public class GsonAzureProject {
+
+ @SerializedName("name")
+ private String name;
+
+ @SerializedName("description")
+ private String description;
+
+ public GsonAzureProject() {
+ // http://stackoverflow.com/a/18645370/229031
+ }
+
+ public GsonAzureProject(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProjectList.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProjectList.java
new file mode 100644
index 00000000000..e594bbbf90a
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureProjectList.java
@@ -0,0 +1,56 @@
+/*
+ * 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.azure;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GsonAzureProjectList {
+
+ @SerializedName("value")
+ private List<GsonAzureProject> values;
+
+ public GsonAzureProjectList() {
+ // http://stackoverflow.com/a/18645370/229031
+ this(new ArrayList<>());
+ }
+
+ public GsonAzureProjectList(List<GsonAzureProject> values) {
+ this.values = values;
+ }
+
+ public List<GsonAzureProject> getValues() {
+ return values;
+ }
+
+ public GsonAzureProjectList setValues(List<GsonAzureProject> values) {
+ this.values = values;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "{" +
+ "values=" + values +
+ '}';
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepo.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepo.java
new file mode 100644
index 00000000000..60327fc5c63
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepo.java
@@ -0,0 +1,63 @@
+/*
+ * 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.azure;
+
+import com.google.gson.annotations.SerializedName;
+
+public class GsonAzureRepo {
+ @SerializedName("id")
+ private String id;
+
+ @SerializedName("name")
+ private String name;
+
+ @SerializedName("url")
+ private String url;
+
+ @SerializedName("project")
+ private GsonAzureProject project;
+
+ public GsonAzureRepo() {
+ // http://stackoverflow.com/a/18645370/229031
+ }
+
+ public GsonAzureRepo(String id, String name, String url, GsonAzureProject project) {
+ this.id = id;
+ this.name = name;
+ this.url = url;
+ this.project = project;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public GsonAzureProject getProject() {
+ return project;
+ }
+}
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepoList.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepoList.java
new file mode 100644
index 00000000000..eaeffc8a5c6
--- /dev/null
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/azure/GsonAzureRepoList.java
@@ -0,0 +1,49 @@
+/*
+ * 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.azure;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GsonAzureRepoList {
+
+ @SerializedName("value")
+ private List<GsonAzureRepo> values;
+
+ public GsonAzureRepoList() {
+ // http://stackoverflow.com/a/18645370/229031
+ this(new ArrayList<>());
+ }
+
+ public GsonAzureRepoList(List<GsonAzureRepo> values) {
+ this.values = values;
+ }
+
+ static GsonAzureRepoList parse(String json) {
+ return new Gson().fromJson(json, GsonAzureRepoList.class);
+ }
+
+ public List<GsonAzureRepo> getValues() {
+ return values;
+ }
+
+}
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java
new file mode 100644
index 00000000000..c296f79b5af
--- /dev/null
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/azure/AzureDevOpsHttpClientTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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.azure;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.alm.client.ConstantTimeoutConfiguration;
+import org.sonar.alm.client.TimeoutConfiguration;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+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 javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+public class AzureDevOpsHttpClientTest {
+ public static final String UNABLE_TO_CONTACT_AZURE = "Unable to contact Azure DevOps server, got an unexpected response";
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private static final String NON_JSON_PAYLOAD = "non json payload";
+ private final MockWebServer server = new MockWebServer();
+ private AzureDevOpsHttpClient underTest;
+
+ @Before
+ public void prepare() throws IOException {
+ server.start();
+
+ TimeoutConfiguration timeoutConfiguration = new ConstantTimeoutConfiguration(10_000);
+ underTest = new AzureDevOpsHttpClient(timeoutConfiguration);
+ }
+
+ @After
+ public void stopServer() throws IOException {
+ server.shutdown();
+ }
+
+ @Test
+ public void get_projects() throws InterruptedException {
+ enqueueResponse(200, " { \"count\": 2,\n" +
+ " \"value\": [\n" +
+ " {\n" +
+ " \"id\": \"3311cd05-3f00-4a5e-b47f-df94a9982b6e\",\n" +
+ " \"name\": \"Project 1\",\n" +
+ " \"description\": \"Project Description\",\n" +
+ " \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/3311cd05-3f00-4a5e-b47f-df94a9982b6e\",\n" +
+ " \"state\": \"wellFormed\",\n" +
+ " \"revision\": 63,\n" +
+ " \"visibility\": \"private\"\n" +
+ " }," +
+ "{\n" +
+ " \"id\": \"3be0f34d-c931-4ff8-8d37-18a83663bd3c\",\n" +
+ " \"name\": \"Project 2\",\n" +
+ " \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/3be0f34d-c931-4ff8-8d37-18a83663bd3c\",\n" +
+ " \"state\": \"wellFormed\",\n" +
+ " \"revision\": 52,\n" +
+ " \"visibility\": \"private\"\n" +
+ " }]}");
+
+ GsonAzureProjectList projects = underTest.getProjects(server.url("").toString(), "token");
+
+ RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS);
+ String azureDevOpsUrlCall = request.getRequestUrl().toString();
+ assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "_apis/projects");
+ assertThat(request.getMethod()).isEqualTo("GET");
+
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.DEBUG))
+ .contains("get projects : [" + server.url("").toString() + "_apis/projects]");
+ assertThat(projects.getValues()).hasSize(2);
+ assertThat(projects.getValues())
+ .extracting(GsonAzureProject::getName, GsonAzureProject::getDescription)
+ .containsExactly(tuple("Project 1", "Project Description"), tuple("Project 2", null));
+ }
+
+ @Test
+ public void get_projects_non_json_payload() {
+ enqueueResponse(200, NON_JSON_PAYLOAD);
+
+ assertThatThrownBy(() -> underTest.getProjects(server.url("").toString(), "token"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(UNABLE_TO_CONTACT_AZURE);
+ }
+
+ @Test
+ public void get_projects_with_invalid_pat() {
+ enqueueResponse(401);
+
+ assertThatThrownBy(() -> underTest.getProjects(server.url("").toString(), "invalid-token"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid personal access token");
+ }
+
+ @Test
+ public void get_projects_with_server_error() {
+ enqueueResponse(500);
+
+ assertThatThrownBy(() -> underTest.getProjects(server.url("").toString(), "token"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Unable to contact Azure DevOps server");
+ }
+
+ @Test
+ public void get_repos_with_project_name() throws InterruptedException {
+ enqueueResponse(200, "{\n" +
+ " \"value\": [\n" +
+ " {\n" +
+ " \"id\": \"741248a4-285e-4a6d-af52-1a49d8070638\",\n" +
+ " \"name\": \"Repository 1\",\n" +
+ " \"url\": \"https://ado.sonarqube.com/repositories/\",\n" +
+ " \"project\": {\n" +
+ " \"id\": \"c88ddb32-ced8-420d-ab34-764133038b34\",\n" +
+ " \"name\": \"projectName\",\n" +
+ " \"url\": \"https://ado.sonarqube.com/DefaultCollection/_apis/projects/c88ddb32-ced8-420d-ab34-764133038b34\",\n" +
+ " \"state\": \"wellFormed\",\n" +
+ " \"revision\": 29,\n" +
+ " \"visibility\": \"private\",\n" +
+ " \"lastUpdateTime\": \"2020-11-11T09:38:03.3Z\"\n" +
+ " },\n" +
+ " \"size\": 0\n" +
+ " }\n" +
+ " ],\n" +
+ " \"count\": 1\n" +
+ "}");
+
+ GsonAzureRepoList repos = underTest.getRepos(server.url("").toString(), "token", "projectName");
+
+ RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS);
+ String azureDevOpsUrlCall = request.getRequestUrl().toString();
+ assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "projectName/_apis/git/repositories");
+ assertThat(request.getMethod()).isEqualTo("GET");
+
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.DEBUG))
+ .contains("get repos : [" + server.url("").toString() + "projectName/_apis/git/repositories]");
+ assertThat(repos.getValues()).hasSize(1);
+ assertThat(repos.getValues())
+ .extracting(GsonAzureRepo::getName, GsonAzureRepo::getUrl, r -> r.getProject().getName())
+ .containsExactly(tuple("Repository 1", "https://ado.sonarqube.com/repositories/", "projectName"));
+ }
+
+ @Test
+ public void get_repos_without_project_name() throws InterruptedException {
+ enqueueResponse(200, "{ \"value\": [], \"count\": 0 }");
+
+ GsonAzureRepoList repos = underTest.getRepos(server.url("").toString(), "token", null);
+
+ RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS);
+ String azureDevOpsUrlCall = request.getRequestUrl().toString();
+ assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "_apis/git/repositories");
+ assertThat(request.getMethod()).isEqualTo("GET");
+ assertThat(repos.getValues()).isEmpty();
+ }
+
+ @Test
+ public void get_repos_non_json_payload() {
+ enqueueResponse(200, NON_JSON_PAYLOAD);
+
+ assertThatThrownBy(() -> underTest.getRepos(server.url("").toString(), "token", null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(UNABLE_TO_CONTACT_AZURE);
+ }
+
+ @Test
+ public void get_repo() throws InterruptedException {
+ enqueueResponse(200, "{ " +
+ " \"id\": \"Repo-Id-1\",\n" +
+ " \"name\": \"Repo-Name-1\",\n" +
+ " \"url\": \"https://ado.sonarqube.com/DefaultCollection/Repo-Id-1\",\n" +
+ " \"project\": {\n" +
+ " \"id\": \"84ea9d51-0c8a-44ad-be92-b2af7fe2c299\",\n" +
+ " \"name\": \"Project-Name\",\n" +
+ " \"description\": \"Project's description\" \n" +
+ " },\n" +
+ " \"size\": 0" +
+ "}");
+
+ GsonAzureRepo repo = underTest.getRepo(server.url("").toString(), "token", "Project-Name", "Repo-Name-1");
+
+ RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS);
+ String azureDevOpsUrlCall = request.getRequestUrl().toString();
+ assertThat(azureDevOpsUrlCall).isEqualTo(server.url("") + "Project-Name/_apis/git/repositories/Repo-Name-1");
+ assertThat(request.getMethod()).isEqualTo("GET");
+
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.DEBUG))
+ .contains("get repo : [" + server.url("").toString() + "Project-Name/_apis/git/repositories/Repo-Name-1]");
+ assertThat(repo.getId()).isEqualTo("Repo-Id-1");
+ assertThat(repo.getName()).isEqualTo("Repo-Name-1");
+ assertThat(repo.getUrl()).isEqualTo("https://ado.sonarqube.com/DefaultCollection/Repo-Id-1");
+ assertThat(repo.getProject().getName()).isEqualTo("Project-Name");
+ }
+
+ @Test
+ public void get_repo_non_json_payload() {
+ enqueueResponse(200, NON_JSON_PAYLOAD);
+
+ assertThatThrownBy(() -> underTest.getRepo(server.url("").toString(), "token", "projectName", "repoName"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(UNABLE_TO_CONTACT_AZURE);
+ }
+
+ @Test
+ public void get_repo_json_error_payload() {
+ enqueueResponse(400,
+ "{'message':'TF200016: The following project does not exist: projectName. Verify that the name of the project is correct and that the project exists on the specified Azure DevOps Server.'}");
+
+ assertThatThrownBy(() -> underTest.getRepo(server.url("").toString(), "token", "projectName", "repoName"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(
+ "Unable to contact Azure DevOps server : TF200016: The following project does not exist: projectName. Verify that the name of the project is correct and that the project exists on the specified Azure DevOps Server.");
+ }
+
+ private void enqueueResponse(int responseCode) {
+ enqueueResponse(responseCode, "");
+ }
+
+ private void enqueueResponse(int responseCode, @Nullable String body) {
+ server.enqueue(new MockResponse()
+ .setHeader("Content-Type", "application/json;charset=UTF-8")
+ .setResponseCode(responseCode)
+ .setBody(body));
+ }
+
+ @Test
+ public void trim_url() {
+ assertThat(AzureDevOpsHttpClient.getTrimmedUrl("http://localhost:4564/"))
+ .isEqualTo("http://localhost:4564");
+ }
+
+ @Test
+ public void trim_url_without_ending_slash() {
+ assertThat(AzureDevOpsHttpClient.getTrimmedUrl("http://localhost:4564"))
+ .isEqualTo("http://localhost:4564");
+ }
+
+ @Test
+ public void trim_null_url() {
+ assertThat(AzureDevOpsHttpClient.getTrimmedUrl(null))
+ .isNull();
+ }
+
+ @Test
+ public void trim_empty_url() {
+ assertThat(AzureDevOpsHttpClient.getTrimmedUrl(""))
+ .isEmpty();
+ }
+}