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;
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) {
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) {
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);
}
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);
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);
}
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)
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;
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" +
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"));
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"));
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" +
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", ""))
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
.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();
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);
}
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);
}
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
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
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
.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();
+ }
+
}