]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13902 Improve bitbucket server error handling mechanism
authorMalek-Ben-Anes <malek.benanes@sonarsource.com>
Tue, 25 Oct 2022 09:19:27 +0000 (11:19 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 25 Oct 2022 20:04:26 +0000 (20:04 +0000)
server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClient.java
server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucketserver/BitbucketServerRestClientTest.java

index 8eda23af7244fbae8c8565737ce1ded7f971c505..8a09f3b28eb44e7adb1cbee565af3f10f84e9997 100644 (file)
@@ -36,7 +36,6 @@ import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.RequestBody;
 import okhttp3.Response;
-import okhttp3.ResponseBody;
 import org.sonar.alm.client.TimeoutConfiguration;
 import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.log.Logger;
@@ -57,6 +56,8 @@ public class BitbucketServerRestClient {
   private static final String GET = "GET";
   protected static final String UNABLE_TO_CONTACT_BITBUCKET_SERVER = "Unable to contact Bitbucket server";
 
+  protected static final String UNEXPECTED_RESPONSE_FROM_BITBUCKET_SERVER = "Unexpected response from Bitbucket server";
+
   protected final OkHttpClient client;
 
   public BitbucketServerRestClient(TimeoutConfiguration timeoutConfiguration) {
@@ -70,44 +71,44 @@ public class BitbucketServerRestClient {
 
   public void validateUrl(String serverUrl) {
     HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/repos");
-    doGet("", url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
+    doGet("", url, body -> buildGson().fromJson(body, RepositoryList.class));
   }
 
   public void validateToken(String serverUrl, String token) {
     HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/users");
-    doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), UserList.class));
+    doGet(token, url, body -> buildGson().fromJson(body, UserList.class));
   }
 
   public void validateReadPermission(String serverUrl, String personalAccessToken) {
     HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/repos");
-    doGet(personalAccessToken, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
+    doGet(personalAccessToken, url, body -> buildGson().fromJson(body, RepositoryList.class));
   }
 
   public RepositoryList getRepos(String serverUrl, String token, @Nullable String project, @Nullable String repo) {
     String projectOrEmpty = Optional.ofNullable(project).orElse("");
     String repoOrEmpty = Optional.ofNullable(repo).orElse("");
     HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/repos?projectname=%s&name=%s", projectOrEmpty, repoOrEmpty));
-    return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
+    return doGet(token, url, body -> buildGson().fromJson(body, RepositoryList.class));
   }
 
   public Repository getRepo(String serverUrl, String token, String project, String repoSlug) {
     HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects/%s/repos/%s", project, repoSlug));
-    return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class));
+    return doGet(token, url, body -> buildGson().fromJson(body, Repository.class));
   }
 
   public RepositoryList getRecentRepo(String serverUrl, String token) {
     HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/profile/recent/repos");
-    return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
+    return doGet(token, url, body -> buildGson().fromJson(body, RepositoryList.class));
   }
 
   public ProjectList getProjects(String serverUrl, String token) {
     HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/projects");
-    return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), ProjectList.class));
+    return doGet(token, url, body -> buildGson().fromJson(body, ProjectList.class));
   }
 
   public BranchesList getBranches(String serverUrl, String token, String projectSlug, String repositorySlug) {
     HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects/%s/repos/%s/branches", projectSlug, repositorySlug));
-    return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), BranchesList.class));
+    return doGet(token, url, body -> buildGson().fromJson(body, BranchesList.class));
   }
 
   protected static HttpUrl buildUrl(@Nullable String serverUrl, String relativeUrl) {
@@ -117,7 +118,7 @@ public class BitbucketServerRestClient {
     return HttpUrl.parse(removeEnd(serverUrl, "/") + relativeUrl);
   }
 
-  protected <G> G doGet(String token, HttpUrl url, Function<Response, G> handler) {
+  protected <G> G doGet(String token, HttpUrl url, Function<String, G> handler) {
     Request request = prepareRequestWithBearerToken(token, GET, url, null);
     return doCall(request, handler);
   }
@@ -126,7 +127,8 @@ public class BitbucketServerRestClient {
     Request.Builder builder = new Request.Builder()
       .method(method, body)
       .url(url)
-      .addHeader("x-atlassian-token", "no-check");
+      .addHeader("x-atlassian-token", "no-check")
+      .addHeader("Accept", "application/json");
 
     if (!isNullOrEmpty(token)) {
       builder.addHeader("Authorization", "Bearer " + token);
@@ -135,27 +137,52 @@ public class BitbucketServerRestClient {
     return builder.build();
   }
 
-  protected <G> G doCall(Request request, Function<Response, G> handler) {
+  protected <G> G doCall(Request request, Function<String, G> handler) {
+    String bodyString = getBodyString(request);
+    return applyHandler(handler, bodyString);
+  }
+
+  private String getBodyString(Request request) {
     try (Response response = client.newCall(request).execute()) {
-      handleError(response);
-      return handler.apply(response);
-    } catch (JsonSyntaxException e) {
-      LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": " + e.getMessage(), e);
-      throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ", got an unexpected response", e);
+      String bodyString = response.body() == null ? "" : response.body().string();
+      validateResponseBody(response.isSuccessful(), bodyString);
+      handleHttpErrorIfAny(response.isSuccessful(), response.code(), bodyString);
+      return bodyString;
     } catch (IOException e) {
       LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": " + e.getMessage(), e);
       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER, e);
     }
   }
 
-  protected static void handleError(Response response) throws IOException {
-    if (!response.isSuccessful()) {
-      String errorMessage = getErrorMessage(response.body());
-      LOG.debug(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": {} {}", response.code(), errorMessage);
-      if (response.code() == HTTP_UNAUTHORIZED) {
+  protected static <G> G applyHandler(Function<String, G> handler, String bodyString) {
+    try {
+      return handler.apply(bodyString);
+    } catch (JsonSyntaxException e) {
+      LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ". Unexpected body response was : [{}]", bodyString);
+      LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": {}", e.getMessage(), e);
+      throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ", got an unexpected response", e);
+    }
+  }
+
+  protected static void validateResponseBody(boolean isSuccessful, String bodyString) {
+    if (isSuccessful) {
+      try {
+        buildGson().fromJson(bodyString, Object.class);
+      } catch (JsonParseException e) {
+        LOG.info(UNEXPECTED_RESPONSE_FROM_BITBUCKET_SERVER + " : [{}]", bodyString);
+        throw new IllegalArgumentException(UNEXPECTED_RESPONSE_FROM_BITBUCKET_SERVER, e);
+      }
+    }
+  }
+
+  protected static void handleHttpErrorIfAny(boolean isSuccessful, int httpCode, String bodyString) {
+    if (!isSuccessful) {
+      String errorMessage = getErrorMessage(bodyString);
+      LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": {} {}", httpCode, errorMessage);
+      if (httpCode == HTTP_UNAUTHORIZED) {
         throw new BitbucketServerException(HTTP_UNAUTHORIZED, "Invalid personal access token");
-      } else if (response.code() == HTTP_NOT_FOUND) {
-        throw new BitbucketServerException(HTTP_NOT_FOUND, errorMessage);
+      } else if (httpCode == HTTP_NOT_FOUND) {
+        throw new BitbucketServerException(HTTP_NOT_FOUND, "Error 404. The requested Bitbucket server is unreachable.");
       }
       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER);
     }
@@ -167,9 +194,8 @@ public class BitbucketServerRestClient {
     return s1 != null && s2 != null && s1.equals(s2);
   }
 
-  protected static String getErrorMessage(ResponseBody body) throws IOException {
-    String bodyString = body.string();
-    if (equals(MediaType.parse("application/json;charset=utf-8"), body.contentType()) && !isNullOrEmpty(bodyString)) {
+  protected static String getErrorMessage(String bodyString) {
+    if (!isNullOrEmpty(bodyString)) {
       try {
         return Stream.of(buildGson().fromJson(bodyString, Errors.class).errorData)
           .map(e -> e.exceptionName + " " + e.message)
index 8b4cbeba5dfd60743e73dd95d97d78ac7d0ee9c6..54184b4a0089a70fe6055bb7b5fbdc8328d33964 100644 (file)
 package org.sonar.alm.client.bitbucketserver;
 
 import java.io.IOException;
+import java.util.function.Function;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import okhttp3.MediaType;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.SocketPolicy;
+import okio.Buffer;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.sonar.alm.client.ConstantTimeoutConfiguration;
 import org.sonar.api.utils.log.LogTester;
 
@@ -33,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
 
+@RunWith(DataProviderRunner.class)
 public class BitbucketServerRestClientTest {
   private final MockWebServer server = new MockWebServer();
   private static final String REPOS_BODY = "{\n" +
@@ -112,7 +122,7 @@ public class BitbucketServerRestClientTest {
     assertThat(gsonBBSRepoList.isLastPage()).isTrue();
     assertThat(gsonBBSRepoList.getValues()).hasSize(2);
     assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug,
-      g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName())
+        g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName())
       .containsExactlyInAnyOrder(
         tuple(2L, "banana", "banana", 2L, "HOY", "hoy"),
         tuple(1L, "potato", "potato", 1L, "HEY", "hey"));
@@ -152,7 +162,7 @@ public class BitbucketServerRestClientTest {
     assertThat(gsonBBSRepoList.isLastPage()).isTrue();
     assertThat(gsonBBSRepoList.getValues()).hasSize(2);
     assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug,
-      g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName())
+        g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName())
       .containsExactlyInAnyOrder(
         tuple(2L, "banana", "banana", 2L, "HOY", "hoy"),
         tuple(1L, "potato", "potato", 1L, "HEY", "hey"));
@@ -211,6 +221,20 @@ public class BitbucketServerRestClientTest {
         tuple(2L, "HOY", "hoy"));
   }
 
+  @Test
+  public void get_projects_failed() {
+    server.enqueue(new MockResponse()
+          .setBody(new Buffer().write(new byte[4096]))
+          .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
+
+    String serverUrl = server.url("/").toString();
+    assertThatThrownBy(() -> underTest.getProjects(serverUrl, "token"))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("Unable to contact Bitbucket server");
+
+    assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server");
+  }
+
   @Test
   public void getBranches_given0Branches_returnEmptyList() {
     String bodyWith0Branches = "{\n" +
@@ -290,6 +314,13 @@ public class BitbucketServerRestClientTest {
     assertThat(branches.getBranches()).hasSize(2);
   }
 
+  @Test
+  public void invalid_empty_url() {
+    assertThatThrownBy(() -> BitbucketServerRestClient.buildUrl(null, ""))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage("url must start with http:// or https://");
+  }
+
   @Test
   public void invalid_url() {
     assertThatThrownBy(() -> BitbucketServerRestClient.buildUrl("file://wrong-url", ""))
@@ -301,13 +332,31 @@ public class BitbucketServerRestClientTest {
   public void malformed_json() {
     server.enqueue(new MockResponse()
       .setHeader("Content-Type", "application/json;charset=UTF-8")
-      .setBody(
-        "I'm malformed JSON"));
+      .setBody("I'm malformed JSON"));
 
     String serverUrl = server.url("/").toString();
     assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Unexpected response from Bitbucket server");
+    assertThat(String.join(", ", logTester.logs()))
+      .contains("Unexpected response from Bitbucket server : [I'm malformed JSON]");
+  }
+
+  @Test
+  public void fail_json_error_handling() {
+    assertThatThrownBy(() -> underTest.applyHandler(body -> underTest.buildGson().fromJson(body, Object.class), "not json"))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
+    assertThat(String.join(", ", logTester.logs()))
+      .contains("Unable to contact Bitbucket server. Unexpected body response was : [not json]");
+  }
+
+  @Test
+  public void validate_handler_call_on_empty_body() {
+    server.enqueue(new MockResponse().setResponseCode(200)
+            .setBody(""));
+    assertThat(underTest.doGet("token", server.url("/"),  Function.identity()))
+            .isEmpty();
   }
 
   @Test
@@ -352,6 +401,35 @@ public class BitbucketServerRestClientTest {
       .hasMessage("Invalid personal access token");
   }
 
+  @DataProvider
+  public static Object[][] expectedErrorMessageFromHttpNoJsonBody() {
+    return new Object[][] {
+      {200, "content ready", "application/json;charset=UTF-8", "Unexpected response from Bitbucket server"},
+      {201, "content ready!", "application/xhtml+xml", "Unexpected response from Bitbucket server"},
+      {401, "<p>unauthorized</p>", "application/json;charset=UTF-8", "Invalid personal access token"},
+      {401, "<p>unauthorized</p>", "application/json", "Invalid personal access token"},
+      {401, "<not-authorized>401</not-authorized>", "application/xhtml+xml", "Invalid personal access token"},
+      {403, "<p>forbidden</p>", "application/json;charset=UTF-8", "Unable to contact Bitbucket server"},
+      {404, "<p>not found</p>","application/json;charset=UTF-8", "Error 404. The requested Bitbucket server is unreachable."},
+      {406, "<p>not accepted</p>", "application/json;charset=UTF-8", "Unable to contact Bitbucket server"},
+      {409, "<p>conflict</p>", "application/json;charset=UTF-8", "Unable to contact Bitbucket server"}
+    };
+  }
+
+  @Test
+  @UseDataProvider("expectedErrorMessageFromHttpNoJsonBody")
+  public void fail_response_when_http_no_json_body(int responseCode, String body, String headerContent, String expectedErrorMessage) {
+    server.enqueue(new MockResponse()
+            .setHeader("Content-Type", headerContent)
+            .setResponseCode(responseCode)
+            .setBody(body));
+
+    String serverUrl = server.url("/").toString();
+    assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessage(expectedErrorMessage);
+  }
+
   @Test
   public void fail_validate_on_io_exception() throws IOException {
     server.shutdown();
@@ -482,7 +560,7 @@ public class BitbucketServerRestClientTest {
     String serverUrl = server.url("/").toString();
     assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
       .isInstanceOf(BitbucketServerException.class)
-      .hasMessage("something unexpected")
+      .hasMessage("Error 404. The requested Bitbucket server is unreachable.")
       .extracting(e -> ((BitbucketServerException) e).getHttpStatus()).isEqualTo(404);
   }
 
@@ -493,7 +571,7 @@ public class BitbucketServerRestClientTest {
     String serverUrl = server.url("/").toString();
     assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
       .isInstanceOf(BitbucketServerException.class)
-      .hasMessage("")
+      .hasMessage("Error 404. The requested Bitbucket server is unreachable.")
       .extracting(e -> ((BitbucketServerException) e).getHttpStatus()).isEqualTo(404);
   }
 
@@ -516,7 +594,9 @@ public class BitbucketServerRestClientTest {
     String serverUrl = server.url("/").toString();
     assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
       .isInstanceOf(IllegalArgumentException.class)
-      .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
+      .hasMessage("Unexpected response from Bitbucket server");
+    assertThat(String.join(", ", logTester.logs()))
+      .contains("Unexpected response from Bitbucket server : [this is not a json payload]");
   }
 
   @Test
@@ -538,7 +618,9 @@ public class BitbucketServerRestClientTest {
     String serverUrl = server.url("/").toString();
     assertThatThrownBy(() -> underTest.validateToken(serverUrl, "token"))
       .isInstanceOf(IllegalArgumentException.class)
-      .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
+      .hasMessage("Unexpected response from Bitbucket server");
+    assertThat(String.join(", ", logTester.logs()))
+            .contains("Unexpected response from Bitbucket server : [this is not a json payload]");
   }
 
   @Test
@@ -571,7 +653,9 @@ public class BitbucketServerRestClientTest {
     String serverUrl = server.url("/").toString();
     assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
       .isInstanceOf(IllegalArgumentException.class)
-      .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
+      .hasMessage("Unexpected response from Bitbucket server");
+    assertThat(String.join(", ", logTester.logs()))
+            .contains("Unexpected response from Bitbucket server : [this is not a json payload]");
   }
 
   @Test
@@ -585,4 +669,13 @@ public class BitbucketServerRestClientTest {
       .hasMessage("Invalid personal access token");
   }
 
+  @Test
+  public void check_mediaTypes_equality() {
+    assertThat(underTest.equals(null, null)).isFalse();
+    assertThat(underTest.equals(MediaType.parse("application/json"), null)).isFalse();
+    assertThat(underTest.equals(null, MediaType.parse("application/json"))).isFalse();
+    assertThat(underTest.equals(MediaType.parse("application/ json"), MediaType.parse("text/html; charset=UTF-8"))).isFalse();
+    assertThat(underTest.equals(MediaType.parse("application/Json"), MediaType.parse("application/JSON"))).isTrue();
+  }
+
 }