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");
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 {
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);
}
}
}
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 {
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;
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;
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
@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
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)));
.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
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
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
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
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
@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
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);
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"));
}
}