You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BitbucketCloudRestClientTest.java 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.alm.client.bitbucket.bitbucketcloud;
  21. import com.google.gson.Gson;
  22. import java.io.IOException;
  23. import okhttp3.Call;
  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;
  36. import static java.util.Arrays.asList;
  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.assertj.core.api.Assertions.tuple;
  41. import static org.mockito.ArgumentMatchers.any;
  42. import static org.mockito.Mockito.mock;
  43. import static org.mockito.Mockito.when;
  44. 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;
  48. @Before
  49. public void prepare() throws IOException {
  50. server.start();
  51. underTest = new BitbucketCloudRestClient(new OkHttpClientBuilder().build(), server.url("/").toString(), server.url("/").toString());
  52. }
  53. @After
  54. public void stopServer() throws IOException {
  55. server.shutdown();
  56. }
  57. @Test
  58. public void get_repos() {
  59. server.enqueue(new MockResponse()
  60. .setHeader("Content-Type", "application/json;charset=UTF-8")
  61. .setBody("{\n" +
  62. " \"values\": [\n" +
  63. " {\n" +
  64. " \"slug\": \"banana\",\n" +
  65. " \"uuid\": \"BANANA-UUID\",\n" +
  66. " \"name\": \"banana\",\n" +
  67. " \"project\": {\n" +
  68. " \"key\": \"HOY\",\n" +
  69. " \"uuid\": \"BANANA-PROJECT-UUID\",\n" +
  70. " \"name\": \"hoy\"\n" +
  71. " }\n" +
  72. " },\n" +
  73. " {\n" +
  74. " \"slug\": \"potato\",\n" +
  75. " \"uuid\": \"POTATO-UUID\",\n" +
  76. " \"name\": \"potato\",\n" +
  77. " \"project\": {\n" +
  78. " \"key\": \"HEY\",\n" +
  79. " \"uuid\": \"POTATO-PROJECT-UUID\",\n" +
  80. " \"name\": \"hey\"\n" +
  81. " }\n" +
  82. " }\n" +
  83. " ]\n" +
  84. "}"));
  85. RepositoryList repositoryList = underTest.searchRepos("user:apppwd", "", null, 1, 100);
  86. assertThat(repositoryList.getNext()).isNull();
  87. assertThat(repositoryList.getValues())
  88. .hasSize(2)
  89. .extracting(Repository::getUuid, Repository::getName, Repository::getSlug,
  90. g -> g.getProject().getUuid(), g -> g.getProject().getKey(), g -> g.getProject().getName())
  91. .containsExactlyInAnyOrder(
  92. tuple("BANANA-UUID", "banana", "banana", "BANANA-PROJECT-UUID", "HOY", "hoy"),
  93. tuple("POTATO-UUID", "potato", "potato", "POTATO-PROJECT-UUID", "HEY", "hey"));
  94. }
  95. @Test
  96. public void get_repo() {
  97. server.enqueue(new MockResponse()
  98. .setHeader("Content-Type", "application/json;charset=UTF-8")
  99. .setBody(
  100. " {\n" +
  101. " \"slug\": \"banana\",\n" +
  102. " \"uuid\": \"BANANA-UUID\",\n" +
  103. " \"name\": \"banana\",\n" +
  104. " \"mainbranch\": {\n" +
  105. " \"type\": \"branch\",\n" +
  106. " \"name\": \"develop\"\n" +
  107. " },"+
  108. " \"project\": {\n" +
  109. " \"key\": \"HOY\",\n" +
  110. " \"uuid\": \"BANANA-PROJECT-UUID\",\n" +
  111. " \"name\": \"hoy\"\n" +
  112. " }\n" +
  113. " }"));
  114. Repository repository = underTest.getRepo("user:apppwd", "workspace", "rep");
  115. assertThat(repository.getUuid()).isEqualTo("BANANA-UUID");
  116. assertThat(repository.getName()).isEqualTo("banana");
  117. assertThat(repository.getSlug()).isEqualTo("banana");
  118. assertThat(repository.getProject())
  119. .extracting(Project::getUuid, Project::getKey, Project::getName)
  120. .contains("BANANA-PROJECT-UUID", "HOY", "hoy");
  121. assertThat(repository.getMainBranch())
  122. .extracting(MainBranch::getType, MainBranch::getName)
  123. .contains("branch", "develop");
  124. }
  125. @Test
  126. public void bbc_object_serialization_deserialization() {
  127. Project project = new Project("PROJECT-UUID-ONE", "projectKey", "projectName");
  128. MainBranch mainBranch = new MainBranch("branch", "develop");
  129. Repository repository = new Repository("REPO-UUID-ONE", "repo-slug", "repoName", project, mainBranch);
  130. RepositoryList repos = new RepositoryList(null, asList(repository), 1, 100);
  131. server.enqueue(new MockResponse()
  132. .setHeader("Content-Type", "application/json;charset=UTF-8")
  133. .setBody(new Gson().toJson(repos)));
  134. RepositoryList repositoryList = underTest.searchRepos("user:apppwd", "", null, 1, 100);
  135. assertThat(repositoryList.getNext()).isNull();
  136. assertThat(repositoryList.getPage()).isOne();
  137. assertThat(repositoryList.getPagelen()).isEqualTo(100);
  138. assertThat(repositoryList.getValues())
  139. .hasSize(1)
  140. .extracting(Repository::getUuid, Repository::getName, Repository::getSlug,
  141. g -> g.getProject().getUuid(), g -> g.getProject().getKey(), g -> g.getProject().getName(),
  142. g -> g.getMainBranch().getType(), g -> g.getMainBranch().getName())
  143. .containsExactlyInAnyOrder(
  144. tuple("REPO-UUID-ONE", "repoName", "repo-slug",
  145. "PROJECT-UUID-ONE", "projectKey", "projectName",
  146. "branch", "develop"));
  147. }
  148. @Test
  149. public void failIfUnauthorized() {
  150. server.enqueue(new MockResponse().setResponseCode(401).setBody("Unauthorized"));
  151. assertThatIllegalArgumentException()
  152. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  153. .withMessage("Unable to contact Bitbucket Cloud servers");
  154. }
  155. @Test
  156. public void validate_fails_with_IAE_if_timeout() {
  157. server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
  158. assertThatIllegalArgumentException()
  159. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"));
  160. }
  161. @Test
  162. public void validate_success() throws Exception {
  163. String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
  164. + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
  165. server.enqueue(new MockResponse().setBody(tokenResponse));
  166. server.enqueue(new MockResponse().setBody("OK"));
  167. underTest.validate("clientId", "clientSecret", "workspace");
  168. RecordedRequest request = server.takeRequest();
  169. assertThat(request.getPath()).isEqualTo("/");
  170. assertThat(request.getHeader("Authorization")).isNotNull();
  171. assertThat(request.getBody().readUtf8()).isEqualTo("grant_type=client_credentials");
  172. }
  173. @Test
  174. public void validate_with_invalid_workspace() {
  175. String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
  176. + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
  177. server.enqueue(new MockResponse().setBody(tokenResponse).setResponseCode(200).setHeader("Content-Type", JSON_MEDIA_TYPE));
  178. String response = "{\"type\": \"error\", \"error\": {\"message\": \"No workspace with identifier 'workspace'.\"}}";
  179. server.enqueue(new MockResponse().setBody(response).setResponseCode(404).setHeader("Content-Type", JSON_MEDIA_TYPE));
  180. assertThatExceptionOfType(IllegalArgumentException.class)
  181. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  182. .withMessage("Error returned by Bitbucket Cloud: No workspace with identifier 'workspace'.");
  183. }
  184. @Test
  185. public void validate_with_private_consumer() {
  186. String response = "{\"error_description\": \"Cannot use client_credentials with a consumer marked as \\\"public\\\". "
  187. + "Calls for auto generated consumers should use urn:bitbucket:oauth2:jwt instead.\", \"error\": \"invalid_grant\"}";
  188. server.enqueue(new MockResponse().setBody(response).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
  189. assertThatExceptionOfType(IllegalArgumentException.class)
  190. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  191. .withMessage("Unable to contact Bitbucket Cloud servers: Configure the OAuth consumer in the Bitbucket workspace to be a private consumer");
  192. }
  193. @Test
  194. public void validate_with_invalid_credentials() {
  195. String response = "{\"error_description\": \"Invalid OAuth client credentials\", \"error\": \"unauthorized_client\"}";
  196. server.enqueue(new MockResponse().setBody(response).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
  197. assertThatExceptionOfType(IllegalArgumentException.class)
  198. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  199. .withMessage("Unable to contact Bitbucket Cloud servers: Check your credentials");
  200. }
  201. @Test
  202. public void validate_with_insufficient_privileges() {
  203. String tokenResponse = "{\"scopes\": \"webhook pullrequest:write\", \"access_token\": \"token\", \"expires_in\": 7200, "
  204. + "\"token_type\": \"bearer\", \"state\": \"client_credentials\", \"refresh_token\": \"abc\"}";
  205. server.enqueue(new MockResponse().setBody(tokenResponse).setResponseCode(200).setHeader("Content-Type", JSON_MEDIA_TYPE));
  206. String error = "{\"type\": \"error\", \"error\": {\"message\": \"Your credentials lack one or more required privilege scopes.\", \"detail\": "
  207. + "{\"granted\": [\"email\"], \"required\": [\"account\"]}}}\n";
  208. server.enqueue(new MockResponse().setBody(error).setResponseCode(400).setHeader("Content-Type", JSON_MEDIA_TYPE));
  209. assertThatExceptionOfType(IllegalArgumentException.class)
  210. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  211. .withMessage("Error returned by Bitbucket Cloud: Your credentials lack one or more required privilege scopes.");
  212. }
  213. @Test
  214. public void validate_app_password_success() throws Exception {
  215. String reposResponse = "{\"pagelen\": 10,\n" +
  216. "\"values\": [],\n" +
  217. "\"page\": 1,\n" +
  218. "\"size\": 0\n" +
  219. "}";
  220. server.enqueue(new MockResponse().setBody(reposResponse));
  221. server.enqueue(new MockResponse().setBody("OK"));
  222. underTest.validateAppPassword("user:password", "workspace");
  223. RecordedRequest request = server.takeRequest();
  224. assertThat(request.getPath()).isEqualTo("/2.0/repositories/workspace");
  225. assertThat(request.getHeader("Authorization")).isNotNull();
  226. }
  227. @Test
  228. public void validate_app_password_with_invalid_credentials() {
  229. server.enqueue(new MockResponse().setResponseCode(401).setHeader("Content-Type", JSON_MEDIA_TYPE));
  230. assertThatExceptionOfType(IllegalArgumentException.class)
  231. .isThrownBy(() -> underTest.validateAppPassword("wrong:wrong", "workspace"))
  232. .withMessage("Unable to contact Bitbucket Cloud servers");
  233. }
  234. @Test
  235. public void nullErrorBodyIsSupported() throws IOException {
  236. OkHttpClient clientMock = mock(OkHttpClient.class);
  237. Call callMock = mock(Call.class);
  238. when(callMock.execute()).thenReturn(new Response.Builder()
  239. .request(new Request.Builder().url("http://any.test").build())
  240. .protocol(Protocol.HTTP_1_1)
  241. .code(500)
  242. .message("")
  243. .build());
  244. when(clientMock.newCall(any())).thenReturn(callMock);
  245. underTest = new BitbucketCloudRestClient(clientMock);
  246. assertThatIllegalArgumentException()
  247. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  248. .withMessage("Unable to contact Bitbucket Cloud servers");
  249. }
  250. @Test
  251. public void invalidJsonResponseBodyIsSupported() {
  252. server.enqueue(new MockResponse().setResponseCode(500)
  253. .setHeader("content-type", "application/json; charset=utf-8")
  254. .setBody("not a JSON string"));
  255. assertThatIllegalArgumentException()
  256. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  257. .withMessage("Unable to contact Bitbucket Cloud servers");
  258. }
  259. @Test
  260. public void responseBodyWithoutErrorFieldIsSupported() {
  261. server.enqueue(new MockResponse().setResponseCode(500)
  262. .setHeader("content-type", "application/json; charset=utf-8")
  263. .setBody("{\"foo\": \"bar\"}"));
  264. assertThatIllegalArgumentException()
  265. .isThrownBy(() -> underTest.validate("clientId", "clientSecret", "workspace"))
  266. .withMessage("Unable to contact Bitbucket Cloud servers");
  267. }
  268. }