]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16291 Improve Bibucket cloud logs
authorAntoine Vinot <antoine.vinot@sonarsource.com>
Mon, 13 Jun 2022 13:14:03 +0000 (15:14 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 14 Jun 2022 20:02:51 +0000 (20:02 +0000)
server/sonar-alm-client/src/main/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClient.java
server/sonar-alm-client/src/test/java/org/sonar/alm/client/bitbucket/bitbucketcloud/BitbucketCloudRestClientTest.java

index f8e8ce3b145884008243efb456791bfa8f95906b..f461b57f77c1f181bad60232c8b07b980ed47b19 100644 (file)
@@ -52,8 +52,14 @@ public class BitbucketCloudRestClient {
   private static final String ENDPOINT = "https://api.bitbucket.org";
   private static final String ACCESS_TOKEN_ENDPOINT = "https://bitbucket.org/site/oauth2/access_token";
   private static final String VERSION = "2.0";
-  private static final String UNABLE_TO_CONTACT_BBC_SERVERS = "Unable to contact Bitbucket Cloud servers";
-  private static final String ERROR_BBC_SERVERS = "Error returned by Bitbucket Cloud";
+  protected static final String ERROR_BBC_SERVERS = "Error returned by Bitbucket Cloud";
+  protected static final String UNABLE_TO_CONTACT_BBC_SERVERS = "Unable to contact Bitbucket Cloud servers";
+  protected static final String MISSING_PULL_REQUEST_READ_PERMISSION = "The OAuth consumer in the Bitbucket workspace is not configured with the permission to read pull requests.";
+  protected static final String SCOPE = "Scope is: %s";
+  protected static final String UNAUTHORIZED_CLIENT = "Check your credentials";
+  protected static final String OAUTH_CONSUMER_NOT_PRIVATE = "Configure the OAuth consumer in the Bitbucket workspace to be a private consumer";
+  protected static final String BBC_FAIL_WITH_RESPONSE = "Bitbucket Cloud API call to [%s] failed with %s http code. Bitbucket Cloud response content : [%s]";
+  protected static final String BBC_FAIL_WITH_ERROR = "Bitbucket Cloud API call to [%s] failed with error: %s";
 
   protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
 
@@ -80,9 +86,8 @@ public class BitbucketCloudRestClient {
     Token token = validateAccessToken(clientId, clientSecret);
 
     if (token.getScopes() == null || !token.getScopes().contains("pullrequest")) {
-      String msg = "The OAuth consumer in the Bitbucket workspace is not configured with the permission to read pull requests";
-      LOG.info("Validation failed. {}}: {}", msg, token.getScopes());
-      throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + msg);
+      LOG.info(MISSING_PULL_REQUEST_READ_PERMISSION + String.format(SCOPE, token.getScopes()));
+      throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + MISSING_PULL_REQUEST_READ_PERMISSION);
     }
 
     try {
@@ -112,27 +117,26 @@ public class BitbucketCloudRestClient {
 
       ErrorDetails errorMsg = getTokenError(response.body());
       if (errorMsg.body != null) {
+        LOG.info(String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), response.code(), errorMsg.body));
         switch (errorMsg.body) {
           case "invalid_grant":
-            throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS +
-              ": Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
+            throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + OAUTH_CONSUMER_NOT_PRIVATE);
           case "unauthorized_client":
-            throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": Check your credentials");
+            throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + UNAUTHORIZED_CLIENT);
           default:
             if (errorMsg.parsedErrorMsg != null) {
-              LOG.info("Validation failed: " + errorMsg.parsedErrorMsg);
               throw new IllegalArgumentException(ERROR_BBC_SERVERS + ": " + errorMsg.parsedErrorMsg);
             } else {
-              LOG.info("Validation failed: " + errorMsg.body);
               throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
             }
         }
       } else {
-        LOG.info("Validation failed");
+        LOG.info(String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), response.code(), response.message()));
       }
       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
 
     } catch (IOException e) {
+      LOG.info(String.format(BBC_FAIL_WITH_ERROR, request.url(), e.getMessage()));
       throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
     }
   }
@@ -183,15 +187,14 @@ public class BitbucketCloudRestClient {
       }
       return handler.apply(response);
     } catch (IOException e) {
+      LOG.info(ERROR_BBC_SERVERS + ": {}", e.getMessage());
       throw new IllegalStateException(ERROR_BBC_SERVERS, e);
     }
   }
 
   private static void handleError(Response response) throws IOException {
-    int responseCode = response.code();
     ErrorDetails error = getError(response.body());
-    LOG.info(ERROR_BBC_SERVERS + ": {} {}", responseCode, error.parsedErrorMsg != null ? error.parsedErrorMsg : error.body);
-
+    LOG.info(String.format(BBC_FAIL_WITH_RESPONSE, response.request().url(), response.code(), error.body));
     if (error.parsedErrorMsg != null) {
       throw new IllegalStateException(ERROR_BBC_SERVERS + ": " + error.parsedErrorMsg);
     } else {
index b1491ebb3ccd8b23ed67e5e374085ac0b479bbeb..cda046118e1fe44c9e9fc0e14c34587d4fc045d7 100644 (file)
@@ -21,6 +21,8 @@ package org.sonar.alm.client.bitbucket.bitbucketcloud;
 
 import com.google.gson.Gson;
 import java.io.IOException;
+import java.util.List;
+import javax.net.ssl.SSLHandshakeException;
 import okhttp3.Call;
 import okhttp3.OkHttpClient;
 import okhttp3.Protocol;
@@ -32,10 +34,12 @@ import okhttp3.mockwebserver.RecordedRequest;
 import okhttp3.mockwebserver.SocketPolicy;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
 import org.sonarqube.ws.client.OkHttpClientBuilder;
 
-import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -43,17 +47,30 @@ import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.BBC_FAIL_WITH_ERROR;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.BBC_FAIL_WITH_RESPONSE;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.ERROR_BBC_SERVERS;
 import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.JSON_MEDIA_TYPE;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.MISSING_PULL_REQUEST_READ_PERMISSION;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.OAUTH_CONSUMER_NOT_PRIVATE;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.SCOPE;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.UNABLE_TO_CONTACT_BBC_SERVERS;
+import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.UNAUTHORIZED_CLIENT;
 
 public class BitbucketCloudRestClientTest {
+
+  @Rule
+  public LogTester logTester = new LogTester();
+
   private final MockWebServer server = new MockWebServer();
   private BitbucketCloudRestClient underTest;
+  private String serverURL;
 
   @Before
   public void prepare() throws IOException {
     server.start();
-
-    underTest = new BitbucketCloudRestClient(new OkHttpClientBuilder().build(), server.url("/").toString(), server.url("/").toString());
+    serverURL = server.url("/").toString();
+    underTest = new BitbucketCloudRestClient(new OkHttpClientBuilder().build(), serverURL, serverURL);
   }
 
   @After
@@ -104,33 +121,33 @@ public class BitbucketCloudRestClientTest {
   @Test
   public void get_repo() {
     server.enqueue(new MockResponse()
-            .setHeader("Content-Type", "application/json;charset=UTF-8")
-            .setBody(
-                    "    {\n" +
-                            "      \"slug\": \"banana\",\n" +
-                            "      \"uuid\": \"BANANA-UUID\",\n" +
-                            "      \"name\": \"banana\",\n" +
-                            "      \"mainbranch\": {\n" +
-                            "        \"type\": \"branch\",\n" +
-                            "        \"name\": \"develop\"\n" +
-                            "       },"+
-                            "      \"project\": {\n" +
-                            "        \"key\": \"HOY\",\n" +
-                            "        \"uuid\": \"BANANA-PROJECT-UUID\",\n" +
-                            "        \"name\": \"hoy\"\n" +
-                            "      }\n" +
-                            "    }"));
+      .setHeader("Content-Type", "application/json;charset=UTF-8")
+      .setBody(
+        "    {\n" +
+          "      \"slug\": \"banana\",\n" +
+          "      \"uuid\": \"BANANA-UUID\",\n" +
+          "      \"name\": \"banana\",\n" +
+          "      \"mainbranch\": {\n" +
+          "        \"type\": \"branch\",\n" +
+          "        \"name\": \"develop\"\n" +
+          "       }," +
+          "      \"project\": {\n" +
+          "        \"key\": \"HOY\",\n" +
+          "        \"uuid\": \"BANANA-PROJECT-UUID\",\n" +
+          "        \"name\": \"hoy\"\n" +
+          "      }\n" +
+          "    }"));
 
     Repository repository = underTest.getRepo("user:apppwd", "workspace", "rep");
     assertThat(repository.getUuid()).isEqualTo("BANANA-UUID");
     assertThat(repository.getName()).isEqualTo("banana");
     assertThat(repository.getSlug()).isEqualTo("banana");
     assertThat(repository.getProject())
-            .extracting(Project::getUuid, Project::getKey, Project::getName)
-            .contains("BANANA-PROJECT-UUID", "HOY", "hoy");
+      .extracting(Project::getUuid, Project::getKey, Project::getName)
+      .contains("BANANA-PROJECT-UUID", "HOY", "hoy");
     assertThat(repository.getMainBranch())
-            .extracting(MainBranch::getType, MainBranch::getName)
-            .contains("branch", "develop");
+      .extracting(MainBranch::getType, MainBranch::getName)
+      .contains("branch", "develop");
   }
 
   @Test
@@ -138,7 +155,7 @@ public class BitbucketCloudRestClientTest {
     Project project = new Project("PROJECT-UUID-ONE", "projectKey", "projectName");
     MainBranch mainBranch = new MainBranch("branch", "develop");
     Repository repository = new Repository("REPO-UUID-ONE", "repo-slug", "repoName", project, mainBranch);
-    RepositoryList repos = new RepositoryList(null, asList(repository), 1, 100);
+    RepositoryList repos = new RepositoryList(null, List.of(repository), 1, 100);
     server.enqueue(new MockResponse()
       .setHeader("Content-Type", "application/json;charset=UTF-8")
       .setBody(new Gson().toJson(repos)));
@@ -151,20 +168,21 @@ public class BitbucketCloudRestClientTest {
       .hasSize(1)
       .extracting(Repository::getUuid, Repository::getName, Repository::getSlug,
         g -> g.getProject().getUuid(), g -> g.getProject().getKey(), g -> g.getProject().getName(),
-              g -> g.getMainBranch().getType(), g -> g.getMainBranch().getName())
+        g -> g.getMainBranch().getType(), g -> g.getMainBranch().getName())
       .containsExactlyInAnyOrder(
-              tuple("REPO-UUID-ONE", "repoName", "repo-slug",
-                      "PROJECT-UUID-ONE", "projectKey", "projectName",
-                      "branch", "develop"));
+        tuple("REPO-UUID-ONE", "repoName", "repo-slug",
+          "PROJECT-UUID-ONE", "projectKey", "projectName",
+          "branch", "develop"));
   }
 
   @Test
-  public void failIfUnauthorized() {
+  public void validate_fails_if_unauthorized() {
     server.enqueue(new MockResponse().setResponseCode(401).setBody("Unauthorized"));
 
     assertThatIllegalArgumentException()
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers");
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL, "401", "Unauthorized"));
   }
 
   @Test
@@ -191,18 +209,31 @@ public class BitbucketCloudRestClientTest {
     assertThat(request.getBody().readUtf8()).isEqualTo("grant_type=client_credentials");
   }
 
+  @Test
+  public void validate_fails_if_unsufficient_pull_request_privileges() throws Exception {
+    String tokenResponse = "{\"scopes\": \"\", \"access_token\": \"token\", \"expires_in\": 7200, "
+      + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
+    server.enqueue(new MockResponse().setBody(tokenResponse));
+
+    assertThatExceptionOfType(IllegalArgumentException.class)
+      .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+      .withMessage(ERROR_BBC_SERVERS + ": " + MISSING_PULL_REQUEST_READ_PERMISSION);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(MISSING_PULL_REQUEST_READ_PERMISSION + String.format(SCOPE, ""));
+  }
+
   @Test
   public void validate_with_invalid_workspace() {
     String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
       + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
     server.enqueue(new MockResponse().setBody(tokenResponse).setResponseCode(200).setHeader("Content-Type", JSON_MEDIA_TYPE));
-    String response = "{\"type\": \"error\", \"error\": {\"message\": \"No workspace with identifier 'workspace'.\"}}";
 
+    String response = "{\"type\": \"error\", \"error\": {\"message\": \"No workspace with identifier 'workspace'.\"}}";
     server.enqueue(new MockResponse().setBody(response).setResponseCode(404).setHeader("Content-Type", JSON_MEDIA_TYPE));
 
     assertThatExceptionOfType(IllegalArgumentException.class)
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
       .withMessage("Error returned by Bitbucket Cloud: No workspace with identifier 'workspace'.");
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL + "2.0/repositories/workspace", "404", response));
   }
 
   @Test
@@ -214,7 +245,8 @@ public class BitbucketCloudRestClientTest {
 
     assertThatExceptionOfType(IllegalArgumentException.class)
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers: Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + OAUTH_CONSUMER_NOT_PRIVATE);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL, "400", "invalid_grant"));
   }
 
   @Test
@@ -225,7 +257,8 @@ public class BitbucketCloudRestClientTest {
 
     assertThatExceptionOfType(IllegalArgumentException.class)
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers: Check your credentials");
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + UNAUTHORIZED_CLIENT);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL, "400", "unauthorized_client"));
   }
 
   @Test
@@ -241,6 +274,7 @@ public class BitbucketCloudRestClientTest {
     assertThatExceptionOfType(IllegalArgumentException.class)
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
       .withMessage("Error returned by Bitbucket Cloud: Your credentials lack one or more required privilege scopes.");
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL + "2.0/repositories/workspace", "400", error));
   }
 
   @Test
@@ -263,11 +297,13 @@ public class BitbucketCloudRestClientTest {
 
   @Test
   public void validate_app_password_with_invalid_credentials() {
-    server.enqueue(new MockResponse().setResponseCode(401).setHeader("Content-Type", JSON_MEDIA_TYPE));
+    String response = "{\"type\": \"error\", \"error\": {\"message\": \"Invalid credentials.\"}}";
+    server.enqueue(new MockResponse().setBody(response).setResponseCode(401).setHeader("Content-Type", JSON_MEDIA_TYPE));
 
     assertThatExceptionOfType(IllegalArgumentException.class)
       .isThrownBy(() -> underTest.validateAppPassword("wrong:wrong", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers");
+      .withMessage("Error returned by Bitbucket Cloud: Invalid credentials.");
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL + "2.0/repositories/workspace", "401", response));
   }
 
   @Test
@@ -275,11 +311,13 @@ public class BitbucketCloudRestClientTest {
     OkHttpClient clientMock = mock(OkHttpClient.class);
     Call callMock = mock(Call.class);
 
+    String url = "http://any.test/";
+    String message = "Unknown issue";
     when(callMock.execute()).thenReturn(new Response.Builder()
-      .request(new Request.Builder().url("http://any.test").build())
+      .request(new Request.Builder().url(url).build())
       .protocol(Protocol.HTTP_1_1)
       .code(500)
-      .message("")
+      .message(message)
       .build());
     when(clientMock.newCall(any())).thenReturn(callMock);
 
@@ -287,28 +325,49 @@ public class BitbucketCloudRestClientTest {
 
     assertThatIllegalArgumentException()
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers");
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, url, "500", message));
   }
 
   @Test
   public void invalidJsonResponseBodyIsSupported() {
+    String body = "not a JSON string";
     server.enqueue(new MockResponse().setResponseCode(500)
       .setHeader("content-type", "application/json; charset=utf-8")
-      .setBody("not a JSON string"));
+      .setBody(body));
 
     assertThatIllegalArgumentException()
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers");
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL, "500", body));
   }
 
   @Test
   public void responseBodyWithoutErrorFieldIsSupported() {
+    String body = "{\"foo\": \"bar\"}";
     server.enqueue(new MockResponse().setResponseCode(500)
       .setHeader("content-type", "application/json; charset=utf-8")
-      .setBody("{\"foo\": \"bar\"}"));
+      .setBody(body));
 
     assertThatIllegalArgumentException()
       .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
-      .withMessage("Unable to contact Bitbucket Cloud servers");
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_RESPONSE, serverURL, "500", body));
+  }
+
+  @Test
+  public void validate_fails_when_ssl_verification_failed() throws IOException {
+    //GIVEN
+    OkHttpClient okHttpClient = mock(OkHttpClient.class);
+    Call call = mock(Call.class);
+    underTest = new BitbucketCloudRestClient(okHttpClient, serverURL, serverURL);
+    when(okHttpClient.newCall(any())).thenReturn(call);
+    when(call.execute()).thenThrow(new SSLHandshakeException("SSL verification failed"));
+    //WHEN
+    //THEN
+    assertThatIllegalArgumentException()
+      .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+      .withMessage(UNABLE_TO_CONTACT_BBC_SERVERS);
+    assertThat(logTester.logs(LoggerLevel.INFO)).containsExactly(String.format(BBC_FAIL_WITH_ERROR, serverURL, "SSL verification failed"));
   }
 }