3 * Copyright (C) 2009-2021 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.alm.client.bitbucket.bitbucketcloud;
22 import java.io.IOException;
24 import okhttp3.OkHttpClient;
25 import okhttp3.Protocol;
26 import okhttp3.Request;
27 import okhttp3.Response;
28 import okhttp3.mockwebserver.MockResponse;
29 import okhttp3.mockwebserver.MockWebServer;
30 import okhttp3.mockwebserver.RecordedRequest;
31 import okhttp3.mockwebserver.SocketPolicy;
32 import org.junit.After;
33 import org.junit.Before;
34 import org.junit.Test;
35 import org.sonarqube.ws.client.OkHttpClientBuilder;
37 import static org.assertj.core.api.Assertions.assertThat;
38 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
39 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
40 import static org.mockito.ArgumentMatchers.any;
41 import static org.mockito.Mockito.mock;
42 import static org.mockito.Mockito.when;
43 import static org.sonar.alm.client.bitbucket.bitbucketcloud.BitbucketCloudRestClient.JSON_MEDIA_TYPE;
45 public class BitbucketCloudRestClientTest {
46 private final MockWebServer server = new MockWebServer();
47 private BitbucketCloudRestClient underTest;
50 public void prepare() throws IOException {
53 underTest = new BitbucketCloudRestClient(new OkHttpClientBuilder().build(), server.url("/").toString(), server.url("/").toString());
57 public void stopServer() throws IOException {
62 public void failIfUnauthorized() {
63 server.enqueue(new MockResponse().setResponseCode(401).setBody("Unauthorized"));
65 assertThatIllegalArgumentException()
66 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
67 .withMessage("Unable to contact Bitbucket Cloud servers");
71 public void validate_fails_with_IAE_if_timeout() {
72 server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
74 assertThatIllegalArgumentException()
75 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"));
79 public void validate_success() throws Exception {
80 String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
81 + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
83 server.enqueue(new MockResponse().setBody(tokenResponse));
84 server.enqueue(new MockResponse().setBody("OK"));
86 underTest.validate("clientId", "clientSecret", "workspace");
88 RecordedRequest request = server.takeRequest();
89 assertThat(request.getPath()).isEqualTo("/");
90 assertThat(request.getHeader("Authorization")).isNotNull();
91 assertThat(request.getBody().readUtf8()).isEqualTo("grant_type=client_credentials");
95 public void validate_with_invalid_workspace() {
96 String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
97 + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
98 server.enqueue(new MockResponse().setBody(tokenResponse).setResponseCode(200).setHeader("Content-Type", JSON_MEDIA_TYPE));
99 String response = "{\"type\": \"error\", \"error\": {\"message\": \"No workspace with identifier 'workspace'.\"}}";
101 server.enqueue(new MockResponse().setBody(response).setResponseCode(404).setHeader("Content-Type", JSON_MEDIA_TYPE));
103 assertThatExceptionOfType(IllegalArgumentException.class)
104 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
105 .withMessage("No workspace with identifier 'workspace'.");
109 public void validate_with_private_consumer() {
110 String response = "{\"error_description\": \"Cannot use client_credentials with a consumer marked as \\\"public\\\". "
111 + "Calls for auto generated consumers should use urn:bitbucket:oauth2:jwt instead.\", \"error\": \"invalid_grant\"}";
113 server.enqueue(new MockResponse().setBody(response).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
115 assertThatExceptionOfType(IllegalArgumentException.class)
116 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
117 .withMessage("Unable to contact Bitbucket Cloud servers: Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
121 public void validate_with_invalid_credentials() {
122 String response = "{\"error_description\": \"Invalid OAuth client credentials\", \"error\": \"unauthorized_client\"}";
124 server.enqueue(new MockResponse().setBody(response).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
126 assertThatExceptionOfType(IllegalArgumentException.class)
127 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
128 .withMessage("Unable to contact Bitbucket Cloud servers: Check your credentials");
132 public void validate_with_insufficient_privileges() {
133 String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
134 + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
135 server.enqueue(new MockResponse().setBody(tokenResponse).setResponseCode(200).setHeader("Content-Type", JSON_MEDIA_TYPE));
137 String error = "{\"type\": \"error\", \"error\": {\"message\": \"Your credentials lack one or more required privilege scopes.\", \"detail\": "
138 + "{\"granted\": [\"email\"], \"required\": [\"account\"]}}}\n";
139 server.enqueue(new MockResponse().setBody(error).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
141 assertThatExceptionOfType(IllegalArgumentException.class)
142 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
143 .withMessage("Unable to contact Bitbucket Cloud servers: Your credentials lack one or more required privilege scopes.");
147 public void nullErrorBodyIsSupported() throws IOException {
148 OkHttpClient clientMock = mock(OkHttpClient.class);
149 Call callMock = mock(Call.class);
151 when(callMock.execute()).thenReturn(new Response.Builder()
152 .request(new Request.Builder().url("http://any.test").build())
153 .protocol(Protocol.HTTP_1_1)
157 when(clientMock.newCall(any())).thenReturn(callMock);
159 underTest = new BitbucketCloudRestClient(clientMock);
161 assertThatIllegalArgumentException()
162 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
163 .withMessage("Unable to contact Bitbucket Cloud servers");
167 public void invalidJsonResponseBodyIsSupported() {
168 server.enqueue(new MockResponse().setResponseCode(500)
169 .setHeader("content-type", "application/json; charset=utf-8")
170 .setBody("not a JSON string"));
172 assertThatIllegalArgumentException()
173 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
174 .withMessage("Unable to contact Bitbucket Cloud servers");
178 public void responseBodyWithoutErrorFieldIsSupported() {
179 server.enqueue(new MockResponse().setResponseCode(500)
180 .setHeader("content-type", "application/json; charset=utf-8")
181 .setBody("{\"foo\": \"bar\"}"));
183 assertThatIllegalArgumentException()
184 .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
185 .withMessage("Unable to contact Bitbucket Cloud servers");