dependencies {
compile project(path: ':sonar-plugin-api', configuration: 'shadow')
compile project(':sonar-ws')
+ compile project(':server:sonar-webserver-api')
compile 'com.google.code.gson:gson'
compile 'com.google.guava:guava'
compile 'com.squareup.okhttp3:okhttp'
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.alm.client.bitbucket.bitbucketcloud;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.Objects;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import okhttp3.Credentials;
+import okhttp3.FormBody;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.sonar.api.internal.apachecommons.io.IOUtils;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.server.exceptions.NotFoundException;
+
+import static org.sonar.api.internal.apachecommons.lang.StringUtils.removeEnd;
+
+@ServerSide
+public class BitbucketCloudRestClient {
+ private static final Logger LOG = Loggers.get(BitbucketCloudRestClient.class);
+ private static final String GET = "GET";
+ 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";
+ protected static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
+
+ private final OkHttpClient client;
+ private final String bitbucketCloudEndpoint;
+ private final String accessTokenEndpoint;
+
+ public BitbucketCloudRestClient(OkHttpClient okHttpClient) {
+ this(okHttpClient, ENDPOINT, ACCESS_TOKEN_ENDPOINT);
+ }
+
+ protected BitbucketCloudRestClient(OkHttpClient okHttpClient, String bitbucketCloudEndpoint, String accessTokenEndpoint) {
+ this.client = okHttpClient;
+ this.bitbucketCloudEndpoint = bitbucketCloudEndpoint;
+ this.accessTokenEndpoint = accessTokenEndpoint;
+ }
+
+ /**
+ * Validate parameters provided.
+ */
+ public void validate(String clientId, String clientSecret, String workspace) {
+ String accessToken = validateAccessToken(clientId, clientSecret);
+ doGet(accessToken, buildUrl("/workspaces/" + workspace + "/permissions"), r -> null, true);
+ }
+
+ private String validateAccessToken(String clientId, String clientSecret) {
+ Response response = null;
+ try {
+ Request request = createAccessTokenRequest(clientId, clientSecret);
+ response = client.newCall(request).execute();
+ if (response.isSuccessful()) {
+ return buildGson().fromJson(response.body().charStream(), Token.class).getAccessToken();
+ }
+
+ ErrorDetails errorMsg = getTokenError(response.body());
+ if (errorMsg.parsedErrorMsg != null) {
+ switch (errorMsg.parsedErrorMsg) {
+ case "invalid_grant":
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS +
+ ": Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
+ case "unauthorized_client":
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": Check your credentials");
+ default:
+ LOG.info("Validation failed: " + errorMsg.parsedErrorMsg);
+ }
+ } else {
+ LOG.info("Validation failed: " + errorMsg.body);
+ }
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS);
+
+ } catch (IOException e) {
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
+ } finally {
+ if (response != null && response.body() != null) {
+ IOUtils.closeQuietly(response);
+ }
+ }
+ }
+
+ public String createAccessToken(String clientId, String clientSecret) {
+ Request request = createAccessTokenRequest(clientId, clientSecret);
+ return doCall(request, r -> buildGson().fromJson(r.body().charStream(), Token.class), false).getAccessToken();
+ }
+
+ private Request createAccessTokenRequest(String clientId, String clientSecret) {
+ RequestBody body = new FormBody.Builder()
+ .add("grant_type", "client_credentials")
+ .build();
+ HttpUrl url = HttpUrl.parse(accessTokenEndpoint);
+ String credential = Credentials.basic(clientId, clientSecret);
+ return new Request.Builder()
+ .method("POST", body)
+ .url(url)
+ .header("Authorization", credential)
+ .build();
+ }
+
+ protected HttpUrl buildUrl(String relativeUrl) {
+ return HttpUrl.parse(removeEnd(bitbucketCloudEndpoint, "/") + "/" + VERSION + relativeUrl);
+ }
+
+ protected <G> G doGet(String accessToken, HttpUrl url, Function<Response, G> handler, boolean throwErrorDetails) {
+ Request request = prepareRequestWithAccessToken(accessToken, GET, url, null);
+ return doCall(request, handler, throwErrorDetails);
+ }
+
+ protected <G> G doGet(String accessToken, HttpUrl url, Function<Response, G> handler) {
+ Request request = prepareRequestWithAccessToken(accessToken, GET, url, null);
+ return doCall(request, handler, false);
+ }
+
+ protected void doPost(String accessToken, HttpUrl url, RequestBody body) {
+ Request request = prepareRequestWithAccessToken(accessToken, "POST", url, body);
+ doCall(request, r -> null, false);
+ }
+
+ protected void doPut(String accessToken, HttpUrl url, String json) {
+ RequestBody body = RequestBody.create(json, JSON_MEDIA_TYPE);
+ Request request = prepareRequestWithAccessToken(accessToken, "PUT", url, body);
+ doCall(request, r -> null, false);
+ }
+
+ protected void doDelete(String accessToken, HttpUrl url) {
+ Request request = prepareRequestWithAccessToken(accessToken, "DELETE", url, null);
+ doCall(request, r -> null, false);
+ }
+
+ private <G> G doCall(Request request, Function<Response, G> handler, boolean throwErrorDetails) {
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ handleError(response, throwErrorDetails);
+ }
+ return handler.apply(response);
+ } catch (IOException e) {
+ throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS, e);
+ }
+ }
+
+ private static Request prepareRequestWithAccessToken(String accessToken, String method, HttpUrl url, @Nullable RequestBody body) {
+ return new Request.Builder()
+ .method(method, body)
+ .url(url)
+ .header("Authorization", "Bearer " + accessToken)
+ .build();
+ }
+
+ public static Gson buildGson() {
+ return new GsonBuilder().create();
+ }
+
+ private static ErrorDetails getTokenError(@Nullable ResponseBody body) throws IOException {
+ return getErrorDetails(body, s -> {
+ TokenError gsonError = buildGson().fromJson(s, TokenError.class);
+ if (gsonError != null && gsonError.error != null) {
+ return gsonError.error;
+ }
+ return null;
+ });
+ }
+
+ private static <T> ErrorDetails getErrorDetails(@Nullable ResponseBody body, Function<String, String> parser) throws IOException {
+ if (body == null) {
+ return new ErrorDetails("", null);
+ }
+ String bodyStr = body.string();
+ if (body.contentType() != null && Objects.equals(JSON_MEDIA_TYPE.type(), body.contentType().type())) {
+ try {
+ return new ErrorDetails(bodyStr, parser.apply(bodyStr));
+ } catch (JsonParseException e) {
+ // ignore
+ }
+ }
+ return new ErrorDetails(bodyStr, null);
+ }
+
+ private static void handleError(Response response, boolean throwErrorDetails) throws IOException {
+ int responseCode = response.code();
+ ErrorDetails error = getError(response.body());
+ if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
+ String errorMsg = error.parsedErrorMsg != null ? error.parsedErrorMsg : "";
+ if (throwErrorDetails) {
+ throw new IllegalArgumentException(errorMsg);
+ } else {
+ throw new NotFoundException(errorMsg);
+ }
+ }
+ LOG.info(UNABLE_TO_CONTACT_BBC_SERVERS + ": {} {}", responseCode, error.parsedErrorMsg != null ? error.parsedErrorMsg : error.body);
+
+ if (throwErrorDetails && error.parsedErrorMsg != null) {
+ throw new IllegalArgumentException(UNABLE_TO_CONTACT_BBC_SERVERS + ": " + error.parsedErrorMsg);
+ } else {
+ throw new IllegalStateException(UNABLE_TO_CONTACT_BBC_SERVERS);
+ }
+ }
+
+ private static ErrorDetails getError(@Nullable ResponseBody body) throws IOException {
+ return getErrorDetails(body, s -> {
+ Error gsonError = buildGson().fromJson(s, Error.class);
+ if (gsonError != null && gsonError.errorMsg != null && gsonError.errorMsg.message != null) {
+ return gsonError.errorMsg.message;
+ }
+ return null;
+ });
+ }
+
+ private static class ErrorDetails {
+ private String body;
+ @Nullable
+ private String parsedErrorMsg;
+
+ public ErrorDetails(String body, @Nullable String parsedErrorMsg) {
+ this.body = body;
+ this.parsedErrorMsg = parsedErrorMsg;
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.alm.client.bitbucket.bitbucketcloud;
+
+import com.google.gson.annotations.SerializedName;
+
+public class Error {
+ @SerializedName("error")
+ public ErrorMsg errorMsg;
+
+ public static class ErrorMsg {
+ @SerializedName("message")
+ public String message;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.alm.client.bitbucket.bitbucketcloud;
+
+import com.google.gson.annotations.SerializedName;
+
+public class Token {
+ @SerializedName("scope")
+ private String scopes;
+ @SerializedName("access_token")
+ private String accessToken;
+ @SerializedName("exires_in")
+ private long expiresIn;
+ @SerializedName("token_type")
+ private String tokenType;
+ @SerializedName("state")
+ private String state;
+ @SerializedName("refresh_token")
+ private String refreshToken;
+
+ public Token() {
+ // nothing to do here
+ }
+
+ public String getScopes() {
+ return scopes;
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public long getExpiresIn() {
+ return expiresIn;
+ }
+
+ public String getTokenType() {
+ return tokenType;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.alm.client.bitbucket.bitbucketcloud;
+
+import com.google.gson.annotations.SerializedName;
+
+public class TokenError {
+ @SerializedName("error")
+ String error;
+
+ @SerializedName("error_description")
+ String errorDescription;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.alm.client.bitbucket.bitbucketcloud;
+
+import java.io.IOException;
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okhttp3.mockwebserver.SocketPolicy;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonarqube.ws.client.OkHttpClientBuilder;
+
+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.JSON_MEDIA_TYPE;
+
+public class BitbucketCloudRestClientTest {
+ private final MockWebServer server = new MockWebServer();
+ private BitbucketCloudRestClient underTest;
+
+ @Before
+ public void prepare() throws IOException {
+ server.start();
+
+ underTest = new BitbucketCloudRestClient(new OkHttpClientBuilder().build(), server.url("/").toString(), server.url("/").toString());
+ }
+
+ @After
+ public void stopServer() throws IOException {
+ server.shutdown();
+ }
+
+ @Test
+ public void failIfUnauthorized() {
+ server.enqueue(new MockResponse().setResponseCode(401).setBody("Unauthorized"));
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("Unable to contact Bitbucket Cloud servers");
+ }
+
+ @Test
+ public void validate_fails_with_IAE_if_timeout() {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"));
+ }
+
+ @Test
+ public void validate_success() throws Exception {
+ 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));
+ server.enqueue(new MockResponse().setBody("OK"));
+
+ underTest.validate("clientId", "clientSecret", "workspace");
+
+ RecordedRequest request = server.takeRequest();
+ assertThat(request.getPath()).isEqualTo("/");
+ assertThat(request.getHeader("Authorization")).isNotNull();
+ assertThat(request.getBody().readUtf8()).isEqualTo("grant_type=client_credentials");
+ }
+
+ @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'.\"}}";
+
+ server.enqueue(new MockResponse().setBody(response).setResponseCode(404).setHeader("Content-Type", JSON_MEDIA_TYPE));
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("No workspace with identifier 'workspace'.");
+ }
+
+ @Test
+ public void validate_with_private_consumer() {
+ String response = "{\"error_description\": \"Cannot use client_credentials with a consumer marked as \\\"public\\\". "
+ + "Calls for auto generated consumers should use urn:bitbucket:oauth2:jwt instead.\", \"error\": \"invalid_grant\"}";
+
+ server.enqueue(new MockResponse().setBody(response).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
+
+ 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");
+ }
+
+ @Test
+ public void validate_with_invalid_credentials() {
+ String response = "{\"error_description\": \"Invalid OAuth client credentials\", \"error\": \"unauthorized_client\"}";
+
+ server.enqueue(new MockResponse().setBody(response).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("Unable to contact Bitbucket Cloud servers: Check your credentials");
+ }
+
+ @Test
+ public void validate_with_insufficient_privileges() {
+ 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 error = "{\"type\": \"error\", \"error\": {\"message\": \"Your credentials lack one or more required privilege scopes.\", \"detail\": "
+ + "{\"granted\": [\"email\"], \"required\": [\"account\"]}}}\n";
+ server.enqueue(new MockResponse().setBody(error).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("Unable to contact Bitbucket Cloud servers: Your credentials lack one or more required privilege scopes.");
+ }
+
+ @Test
+ public void nullErrorBodyIsSupported() throws IOException {
+ OkHttpClient clientMock = mock(OkHttpClient.class);
+ Call callMock = mock(Call.class);
+
+ when(callMock.execute()).thenReturn(new Response.Builder()
+ .request(new Request.Builder().url("http://any.test").build())
+ .protocol(Protocol.HTTP_1_1)
+ .code(500)
+ .message("")
+ .build());
+ when(clientMock.newCall(any())).thenReturn(callMock);
+
+ underTest = new BitbucketCloudRestClient(clientMock);
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("Unable to contact Bitbucket Cloud servers");
+ }
+
+ @Test
+ public void invalidJsonResponseBodyIsSupported() {
+ server.enqueue(new MockResponse().setResponseCode(500)
+ .setHeader("content-type", "application/json; charset=utf-8")
+ .setBody("not a JSON string"));
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("Unable to contact Bitbucket Cloud servers");
+ }
+
+ @Test
+ public void responseBodyWithoutErrorFieldIsSupported() {
+ server.enqueue(new MockResponse().setResponseCode(500)
+ .setHeader("content-type", "application/json; charset=utf-8")
+ .setBody("{\"foo\": \"bar\"}"));
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
+ .withMessage("Unable to contact Bitbucket Cloud servers");
+ }
+}
package org.sonar.server.almsettings.ws;
import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient;
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient;
import org.sonar.alm.client.github.GithubApplicationClient;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
private final GitlabHttpClient gitlabHttpClient;
private final GithubApplicationClient githubApplicationClient;
private final BitbucketServerRestClient bitbucketServerRestClient;
+ private final BitbucketCloudRestClient bitbucketCloudRestClient;
public ValidateAction(DbClient dbClient, UserSession userSession, AlmSettingsSupport almSettingsSupport,
AzureDevOpsHttpClient azureDevOpsHttpClient,
GithubApplicationClientImpl githubApplicationClient, GitlabHttpClient gitlabHttpClient,
- BitbucketServerRestClient bitbucketServerRestClient) {
+ BitbucketServerRestClient bitbucketServerRestClient, BitbucketCloudRestClient bitbucketCloudRestClient) {
this.dbClient = dbClient;
this.userSession = userSession;
this.almSettingsSupport = almSettingsSupport;
this.githubApplicationClient = githubApplicationClient;
this.gitlabHttpClient = gitlabHttpClient;
this.bitbucketServerRestClient = bitbucketServerRestClient;
+ this.bitbucketCloudRestClient = bitbucketCloudRestClient;
}
@Override
validateBitbucketServer(almSettingDto);
break;
case BITBUCKET_CLOUD:
- // TODO implement
+ validateBitbucketCloud(almSettingDto);
break;
case AZURE_DEVOPS:
validateAzure(almSettingDto);
bitbucketServerRestClient.validateToken(almSettingDto.getUrl(), almSettingDto.getPersonalAccessToken());
bitbucketServerRestClient.validateReadPermission(almSettingDto.getUrl(), almSettingDto.getPersonalAccessToken());
}
+
+ private void validateBitbucketCloud(AlmSettingDto almSettingDto) {
+ bitbucketCloudRestClient.validate(almSettingDto.getClientId(), almSettingDto.getClientSecret(), almSettingDto.getAppId());
+ }
}
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient;
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.config.GithubAppConfiguration;
private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
private final GithubApplicationClientImpl githubApplicationClient = mock(GithubApplicationClientImpl.class);
private final BitbucketServerRestClient bitbucketServerRestClient = mock(BitbucketServerRestClient.class);
+ private final BitbucketCloudRestClient bitbucketCloudRestClient = mock(BitbucketCloudRestClient.class);
private final WsActionTester ws = new WsActionTester(
new ValidateAction(db.getDbClient(), userSession, almSettingsSupport, azureDevOpsHttpClient, githubApplicationClient, gitlabHttpClient,
- bitbucketServerRestClient));
+ bitbucketServerRestClient, bitbucketCloudRestClient));
@Test
public void fail_when_key_does_not_match_existing_alm_setting() {
package org.sonar.server.platform.platformlevel;
import java.util.List;
-
import org.sonar.alm.client.TimeoutConfigurationImpl;
import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient;
import org.sonar.alm.client.bitbucketserver.BitbucketServerRestClient;
import org.sonar.alm.client.github.GithubApplicationClientImpl;
import org.sonar.alm.client.github.GithubApplicationHttpClientImpl;
GithubApplicationClientImpl.class,
GithubApplicationHttpClientImpl.class,
BitbucketServerRestClient.class,
+ BitbucketCloudRestClient.class,
GitlabHttpClient.class,
AzureDevOpsHttpClient.class,
AlmIntegrationsWSModule.class,