]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19418 Add debug logging in case error Scanner WS client
authorJacek Poreda <jacek.poreda@sonarsource.com>
Fri, 16 Jun 2023 08:31:26 +0000 (10:31 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 20 Jun 2023 13:10:18 +0000 (13:10 +0000)
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/DefaultScannerWsClient.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/DefaultScannerWsClientTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java
sonar-ws/src/main/java/org/sonarqube/ws/client/MockWsResponse.java
sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpResponse.java
sonar-ws/src/main/java/org/sonarqube/ws/client/WsResponse.java
sonar-ws/src/test/java/org/sonarqube/ws/client/BaseServiceTest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/LocalWsConnectorTest.java

index e3ba9e36e69561050b3c30ae803ab7e560ff207a..c63a8c296761c59d65bb8c92db1f60aa4dbef110 100644 (file)
@@ -29,6 +29,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import javax.annotation.CheckForNull;
 import org.apache.commons.lang.StringUtils;
@@ -101,7 +102,9 @@ public class DefaultScannerWsClient implements ScannerWsClient {
 
   private void failIfUnauthorized(WsResponse response) {
     int code = response.code();
+
     if (code == HTTP_UNAUTHORIZED) {
+      logResponseDetailsIfDebug(response);
       response.close();
       if (hasCredentials) {
         // credentials are not valid
@@ -110,12 +113,13 @@ public class DefaultScannerWsClient implements ScannerWsClient {
       }
       // not authenticated - see https://jira.sonarsource.com/browse/SONAR-4048
       throw MessageException.of(format("Not authorized. Analyzing this project requires authentication. " +
-                                       "Please check the user token in the property '%s' or the credentials in the properties '%s' and '%s'.",
+        "Please check the user token in the property '%s' or the credentials in the properties '%s' and '%s'.",
         ScannerWsClientProvider.TOKEN_PROPERTY, CoreProperties.LOGIN, CoreProperties.PASSWORD));
     }
     if (code == HTTP_FORBIDDEN) {
+      logResponseDetailsIfDebug(response);
       throw MessageException.of("You're not authorized to analyze this project or the project doesn't exist on SonarQube" +
-                                " and you're not authorized to create it. Please contact an administrator.");
+        " and you're not authorized to create it. Please contact an administrator.");
     }
     if (code == HTTP_BAD_REQUEST) {
       String jsonMsg = tryParseAsJsonError(response.content());
@@ -123,11 +127,19 @@ public class DefaultScannerWsClient implements ScannerWsClient {
         throw MessageException.of(jsonMsg);
       }
     }
-
     // if failed, throws an HttpException
     response.failIfNotSuccessful();
   }
 
+  private static void logResponseDetailsIfDebug(WsResponse response) {
+    if (!LOG.isDebugEnabled()) {
+      return;
+    }
+    String content = response.hasContent() ? response.content() : "<no content>";
+    Map<String, List<String>> headers = response.headers();
+    LOG.debug("Error response content: {}, headers: {}", content, headers);
+  }
+
   private void checkAuthenticationWarnings(WsResponse response) {
     if (response.code() == HTTP_OK) {
       response.header(SQ_TOKEN_EXPIRATION_HEADER).ifPresent(expirationDate -> {
@@ -154,7 +166,7 @@ public class DefaultScannerWsClient implements ScannerWsClient {
       LOG.warn("Analysis executed with this token will fail after the expiration date.");
     }
     analysisWarnings.addUnique(warningMessage + "\nAfter this date, the token can no longer be used to execute the analysis. "
-                               + "Please consider generating a new token and updating it in the locations where it is in use.");
+      + "Please consider generating a new token and updating it in the locations where it is in use.");
   }
 
   /**
index 9b746403496eec2c08d44b6d8c0e3831881b02e2..0f44b363c25866b7653f2fcc783a8085b3cb4f6d 100644 (file)
@@ -56,7 +56,7 @@ public class DefaultScannerWsClientTest {
   private final AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
 
   @Test
-  public void log_and_profile_request_if_debug_level() {
+  public void call_whenDebugLevel_shouldLogAndProfileRequest() {
     WsRequest request = newRequest();
     WsResponse response = newResponse().setRequestUrl("https://local/api/issues/search");
     when(wsClient.wsConnector().call(request)).thenReturn(response);
@@ -76,63 +76,120 @@ public class DefaultScannerWsClientTest {
   }
 
   @Test
-  public void create_error_msg_from_json() {
+  public void createErrorMessage_whenJsonError_shouldCreateErrorMsg() {
     String content = "{\"errors\":[{\"msg\":\"missing scan permission\"}, {\"msg\":\"missing another permission\"}]}";
     assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).isEqualTo("missing scan permission, missing another permission");
   }
 
   @Test
-  public void create_error_msg_from_html() {
+  public void createErrorMessage_whenHtml_shouldCreateErrorMsg() {
     String content = "<!DOCTYPE html><html>something</html>";
     assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).isEqualTo("HTTP code 400");
   }
 
   @Test
-  public void create_error_msg_from_long_content() {
+  public void createErrorMessage_whenLongContent_shouldCreateErrorMsg() {
     String content = StringUtils.repeat("mystring", 1000);
     assertThat(DefaultScannerWsClient.createErrorMessage(new HttpException("url", 400, content))).hasSize(15 + 128);
   }
 
   @Test
-  public void fail_if_requires_credentials() {
+  public void call_whenUnauthorizedAndDebugEnabled_shouldLogResponseDetails() {
     WsRequest request = newRequest();
-    WsResponse response = newResponse().setCode(401);
+    WsResponse response = newResponse()
+      .setContent("Missing credentials")
+      .setHeader("Authorization: ", "Bearer ImNotAValidToken")
+      .setCode(403);
+
+    logTester.setLevel(LoggerLevel.DEBUG);
+
     when(wsClient.wsConnector().call(request)).thenReturn(response);
 
-    assertThatThrownBy(() -> new DefaultScannerWsClient(wsClient, false,
-      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings).call(request))
-        .isInstanceOf(MessageException.class)
-        .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
-                    "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+    DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
+      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+    assertThatThrownBy(() -> client.call(request))
+      .isInstanceOf(MessageException.class)
+      .hasMessage(
+        "You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
+
+    List<String> debugLogs = logTester.logs(Level.DEBUG);
+    assertThat(debugLogs).hasSize(2);
+    assertThat(debugLogs.get(1)).contains("Error response content: Missing credentials, headers: {Authorization: =[Bearer ImNotAValidToken]}");
   }
 
   @Test
-  public void fail_if_credentials_are_not_valid() {
+  public void call_whenUnauthenticatedAndDebugEnabled_shouldLogResponseDetails() {
     WsRequest request = newRequest();
-    WsResponse response = newResponse().setCode(401);
+    WsResponse response = newResponse()
+      .setContent("Missing authentication")
+      .setHeader("X-Test-Header: ", "ImATestHeader")
+      .setCode(401);
+
+    logTester.setLevel(LoggerLevel.DEBUG);
+
+    when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+    DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
+      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+    assertThatThrownBy(() -> client.call(request))
+      .isInstanceOf(MessageException.class)
+      .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
+        "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+
+    List<String> debugLogs = logTester.logs(Level.DEBUG);
+    assertThat(debugLogs).hasSize(2);
+    assertThat(debugLogs.get(1)).contains("Error response content: Missing authentication, headers: {X-Test-Header: =[ImATestHeader]}");
+  }
+
+  @Test
+  public void call_whenMissingCredentials_shouldFailWithMsg() {
+    WsRequest request = newRequest();
+    WsResponse response = newResponse()
+      .setContent("Missing authentication")
+      .setCode(401);
+    when(wsClient.wsConnector().call(request)).thenReturn(response);
+
+    DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, false,
+      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+    assertThatThrownBy(() -> client.call(request))
+      .isInstanceOf(MessageException.class)
+      .hasMessage("Not authorized. Analyzing this project requires authentication. Please check the user token in the property 'sonar.token' " +
+        "or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+  }
+
+  @Test
+  public void call_whenInvalidCredentials_shouldFailWithMsg() {
+    WsRequest request = newRequest();
+    WsResponse response = newResponse()
+      .setContent("Invalid credentials")
+      .setCode(401);
     when(wsClient.wsConnector().call(request)).thenReturn(response);
 
-    assertThatThrownBy(() -> new DefaultScannerWsClient(wsClient, /* credentials are configured */true,
-      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings).call(request))
-        .isInstanceOf(MessageException.class)
-        .hasMessage("Not authorized. Please check the user token in the property 'sonar.token' or the credentials in the properties 'sonar.login' and 'sonar.password'.");
+    DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, /* credentials are configured */true,
+      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+    assertThatThrownBy(() -> client.call(request))
+      .isInstanceOf(MessageException.class)
+      .hasMessage("Not authorized. Please check the user token in the property 'sonar.token' or the credentials in the properties 'sonar.login' and 'sonar.password'.");
   }
 
   @Test
-  public void fail_if_requires_permission() {
+  public void call_whenMissingPermissions_shouldFailWithMsg() {
     WsRequest request = newRequest();
     WsResponse response = newResponse()
+      .setContent("Unauthorized")
       .setCode(403);
     when(wsClient.wsConnector().call(request)).thenReturn(response);
 
-    assertThatThrownBy(() -> new DefaultScannerWsClient(wsClient, true,
-      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings).call(request))
-        .isInstanceOf(MessageException.class)
-        .hasMessage("You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
+    DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, true,
+      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+    assertThatThrownBy(() -> client.call(request))
+      .isInstanceOf(MessageException.class)
+      .hasMessage(
+        "You're not authorized to analyze this project or the project doesn't exist on SonarQube and you're not authorized to create it. Please contact an administrator.");
   }
 
   @Test
-  public void warnings_are_added_when_expiration_approaches() {
+  public void call_whenTokenExpirationApproaches_shouldLogWarnings() {
     WsRequest request = newRequest();
     var fiveDaysLatter = LocalDateTime.now().atZone(ZoneOffset.UTC).plusDays(5);
     String expirationDate = DateTimeFormatter
@@ -146,7 +203,7 @@ public class DefaultScannerWsClientTest {
     logTester.setLevel(LoggerLevel.DEBUG);
     DefaultScannerWsClient underTest = new DefaultScannerWsClient(wsClient, false, new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
     underTest.call(request);
-    //the second call should not add the same warning twice
+    // the second call should not add the same warning twice
     underTest.call(request);
 
     // check logs
@@ -157,17 +214,18 @@ public class DefaultScannerWsClientTest {
   }
 
   @Test
-  public void fail_if_bad_request() {
+  public void call_whenBadRequest_shouldFailWithMessage() {
     WsRequest request = newRequest();
     WsResponse response = newResponse()
       .setCode(400)
       .setContent("{\"errors\":[{\"msg\":\"Boo! bad request! bad!\"}]}");
     when(wsClient.wsConnector().call(request)).thenReturn(response);
 
-    assertThatThrownBy(() -> new DefaultScannerWsClient(wsClient, true,
-      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings).call(request))
-        .isInstanceOf(MessageException.class)
-        .hasMessage("Boo! bad request! bad!");
+    DefaultScannerWsClient client = new DefaultScannerWsClient(wsClient, true,
+      new GlobalAnalysisMode(new ScannerProperties(Collections.emptyMap())), analysisWarnings);
+    assertThatThrownBy(() -> client.call(request))
+      .isInstanceOf(MessageException.class)
+      .hasMessage("Boo! bad request! bad!");
   }
 
   private MockWsResponse newResponse() {
index 73abe1907a6b2c7f3ad70af4898909851e295857..3e806580ba649e966760e895b4a1dda61ae36789 100644 (file)
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -159,5 +160,10 @@ class LocalWsConnector implements WsConnector {
     public Optional<String> header(String name) {
       return Optional.empty();
     }
+
+    @Override
+    public Map<String, List<String>> headers() {
+      return new HashMap<>();
+    }
   }
 }
index 5230ef6afd606481da7b0f099e00618f0e58f169..30fabb9731d7af20cb80dcc40cedf7bf988f6c2c 100644 (file)
@@ -25,12 +25,15 @@ import java.io.Reader;
 import java.io.StringReader;
 import java.net.HttpURLConnection;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import org.sonarqube.ws.MediaTypes;
 
 import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.*;
 
 public class MockWsResponse extends BaseResponse {
 
@@ -62,6 +65,13 @@ public class MockWsResponse extends BaseResponse {
     return Optional.ofNullable(headers.get(name));
   }
 
+  @Override
+  public Map<String, List<String>> headers() {
+    return headers.entrySet()
+      .stream()
+      .collect(toMap(Map.Entry::getKey, e -> Collections.singletonList(e.getValue())));
+  }
+
   public MockWsResponse setContentType(String contentType) {
     headers.put(CONTENT_TYPE_HEADER, contentType);
     return this;
@@ -87,6 +97,11 @@ public class MockWsResponse extends BaseResponse {
     return this;
   }
 
+  public MockWsResponse setHeader(String key, String value) {
+    this.headers.put(key, value);
+    return this;
+  }
+
   @Override
   public boolean hasContent() {
     return content != null;
index 50ad2b6a1b0eb7e8b611dfa24fe9c7d291fe09b8..d67a7aa47c60a19142f7746d1150be9b1395b3ce 100644 (file)
@@ -22,6 +22,8 @@ package org.sonarqube.ws.client;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import okhttp3.Response;
 import okhttp3.ResponseBody;
@@ -54,6 +56,11 @@ class OkHttpResponse extends BaseResponse {
     return Optional.ofNullable(okResponse.header(name));
   }
 
+  @Override
+  public Map<String, List<String>> headers() {
+    return okResponse.headers().toMultimap();
+  }
+
   /**
    * Get stream of bytes
    */
index 957fce36e3d73b8410710890f1686a98282d83a4..419d519889aaf58cb3ec269c1fdcaa523db3311a 100644 (file)
@@ -22,6 +22,8 @@ package org.sonarqube.ws.client;
 import java.io.Closeable;
 import java.io.InputStream;
 import java.io.Reader;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 /**
@@ -54,6 +56,8 @@ public interface WsResponse extends Closeable {
 
   Optional<String> header(String name);
 
+  Map<String, List<String>> headers();
+
   boolean hasContent();
 
   InputStream contentStream();
index b3eb7f92d4792ba015b6ddc3f2eb50ce35e58026..5412858b670bd4c3da64b769fb61af01f94c555e 100644 (file)
@@ -38,11 +38,12 @@ public class BaseServiceTest {
 
       public void test() {
         GetRequest get = new GetRequest(path("issue")).setMediaType(MediaTypes.JSON);
-        when(wsConnector.call(get)).thenReturn(new MockWsResponse().setContent("ok"));
+        when(wsConnector.call(get)).thenReturn(new MockWsResponse().setContent("ok").setHeader("header", "value"));
 
         WsResponse response = call(get);
 
         assertThat(response.content()).isEqualTo("ok");
+        assertThat(response.headers()).hasEntrySatisfying("header", headerValues -> headerValues.contains("value"));
       }
 
     }.test();
index 040af9fc652df5782e4ed8b710fede5366689e54..c0af93da3a990d2120627a1e9cd372b8b41f7c02 100644 (file)
@@ -341,6 +341,8 @@ public class HttpConnectorTest {
     // verify response
     assertThat(response.hasContent()).isTrue();
     assertThat(response.content()).isEqualTo("hello, world!");
+    assertThat(response.headers()).hasEntrySatisfying("header", headerValues -> headerValues.contains("value"));
+    assertThat(response.header("header")).hasValue("value");
 
     // verify the request received by server
     RecordedRequest recordedRequest = server.takeRequest();
@@ -516,6 +518,6 @@ public class HttpConnectorTest {
   }
 
   private void answerHelloWorld() {
-    server.enqueue(new MockResponse().setBody("hello, world!"));
+    server.enqueue(new MockResponse().setBody("hello, world!").setHeader("header", "value"));
   }
 }
index 51636d09f20b5d16c9f56becadbd7d0b02158e21..1b4d8e6f2402d11ed80674896ac3f4c21a6ade00 100644 (file)
@@ -66,6 +66,7 @@ public class LocalWsConnectorTest {
     assertThat(IOUtils.toString(wsResponse.contentStream())).isEqualTo("{}");
     assertThat(wsResponse.contentType()).isEqualTo(MediaTypes.JSON);
     assertThat(wsResponse.requestUrl()).isEqualTo("api/issues/search");
+    assertThat(wsResponse.headers()).isEmpty();
   }
 
   @Test