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.bitbucketserver;
22 import java.io.IOException;
23 import okhttp3.mockwebserver.MockResponse;
24 import okhttp3.mockwebserver.MockWebServer;
25 import org.junit.After;
26 import org.junit.Before;
27 import org.junit.Rule;
28 import org.junit.Test;
29 import org.sonar.alm.client.ConstantTimeoutConfiguration;
30 import org.sonar.api.utils.log.LogTester;
32 import static org.assertj.core.api.Assertions.assertThat;
33 import static org.assertj.core.api.Assertions.assertThatThrownBy;
34 import static org.assertj.core.api.Assertions.tuple;
36 public class BitbucketServerRestClientTest {
37 private final MockWebServer server = new MockWebServer();
38 private static final String REPOS_BODY = "{\n" +
39 " \"isLastPage\": true,\n" +
42 " \"slug\": \"banana\",\n" +
44 " \"name\": \"banana\",\n" +
46 " \"key\": \"HOY\",\n" +
48 " \"name\": \"hoy\"\n" +
52 " \"slug\": \"potato\",\n" +
54 " \"name\": \"potato\",\n" +
56 " \"key\": \"HEY\",\n" +
58 " \"name\": \"hey\"\n" +
65 public LogTester logTester = new LogTester();
67 private BitbucketServerRestClient underTest;
70 public void prepare() throws IOException {
73 underTest = new BitbucketServerRestClient(new ConstantTimeoutConfiguration(500));
77 public void stopServer() throws IOException {
82 public void get_repos() {
83 server.enqueue(new MockResponse()
84 .setHeader("Content-Type", "application/json;charset=UTF-8")
86 " \"isLastPage\": true,\n" +
89 " \"slug\": \"banana\",\n" +
91 " \"name\": \"banana\",\n" +
93 " \"key\": \"HOY\",\n" +
95 " \"name\": \"hoy\"\n" +
99 " \"slug\": \"potato\",\n" +
101 " \"name\": \"potato\",\n" +
102 " \"project\": {\n" +
103 " \"key\": \"HEY\",\n" +
105 " \"name\": \"hey\"\n" +
111 RepositoryList gsonBBSRepoList = underTest.getRepos(server.url("/").toString(), "token", "", "");
112 assertThat(gsonBBSRepoList.isLastPage()).isTrue();
113 assertThat(gsonBBSRepoList.getValues()).hasSize(2);
114 assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug,
115 g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName())
116 .containsExactlyInAnyOrder(
117 tuple(2L, "banana", "banana", 2L, "HOY", "hoy"),
118 tuple(1L, "potato", "potato", 1L, "HEY", "hey"));
122 public void get_recent_repos() {
123 server.enqueue(new MockResponse()
124 .setHeader("Content-Type", "application/json;charset=UTF-8")
126 " \"isLastPage\": true,\n" +
129 " \"slug\": \"banana\",\n" +
131 " \"name\": \"banana\",\n" +
132 " \"project\": {\n" +
133 " \"key\": \"HOY\",\n" +
135 " \"name\": \"hoy\"\n" +
139 " \"slug\": \"potato\",\n" +
141 " \"name\": \"potato\",\n" +
142 " \"project\": {\n" +
143 " \"key\": \"HEY\",\n" +
145 " \"name\": \"hey\"\n" +
151 RepositoryList gsonBBSRepoList = underTest.getRecentRepo(server.url("/").toString(), "token");
152 assertThat(gsonBBSRepoList.isLastPage()).isTrue();
153 assertThat(gsonBBSRepoList.getValues()).hasSize(2);
154 assertThat(gsonBBSRepoList.getValues()).extracting(Repository::getId, Repository::getName, Repository::getSlug,
155 g -> g.getProject().getId(), g -> g.getProject().getKey(), g -> g.getProject().getName())
156 .containsExactlyInAnyOrder(
157 tuple(2L, "banana", "banana", 2L, "HOY", "hoy"),
158 tuple(1L, "potato", "potato", 1L, "HEY", "hey"));
162 public void get_repo() {
163 server.enqueue(new MockResponse()
164 .setHeader("Content-Type", "application/json;charset=UTF-8")
167 " \"slug\": \"banana-slug\"," +
169 " \"name\": \"banana\"," +
170 " \"project\": {\n" +
171 " \"key\": \"HOY\"," +
173 " \"name\": \"hoy\"" +
177 Repository repository = underTest.getRepo(server.url("/").toString(), "token", "", "");
178 assertThat(repository.getId()).isEqualTo(2L);
179 assertThat(repository.getName()).isEqualTo("banana");
180 assertThat(repository.getSlug()).isEqualTo("banana-slug");
181 assertThat(repository.getProject())
182 .extracting(Project::getId, Project::getKey, Project::getName)
183 .contains(3L, "HOY", "hoy");
187 public void get_projects() {
188 server.enqueue(new MockResponse()
189 .setHeader("Content-Type", "application/json;charset=UTF-8")
191 " \"isLastPage\": true,\n" +
194 " \"key\": \"HEY\",\n" +
196 " \"name\": \"hey\"\n" +
199 " \"key\": \"HOY\",\n" +
201 " \"name\": \"hoy\"\n" +
206 final ProjectList gsonBBSProjectList = underTest.getProjects(server.url("/").toString(), "token");
207 assertThat(gsonBBSProjectList.getValues()).hasSize(2);
208 assertThat(gsonBBSProjectList.getValues()).extracting(Project::getId, Project::getKey, Project::getName)
209 .containsExactlyInAnyOrder(
210 tuple(1L, "HEY", "hey"),
211 tuple(2L, "HOY", "hoy"));
215 public void getBranches_given0Branches_returnEmptyList() {
216 String bodyWith0Branches = "{\n" +
218 " \"limit\": 25,\n" +
219 " \"isLastPage\": true,\n" +
220 " \"values\": [],\n" +
223 server.enqueue(new MockResponse()
224 .setHeader("Content-Type", "application/json;charset=UTF-8")
225 .setBody(bodyWith0Branches));
227 BranchesList branches = underTest.getBranches(server.url("/").toString(), "token", "projectSlug", "repoSlug");
229 assertThat(branches.getBranches()).isEmpty();
233 public void getBranches_given1Branch_returnListWithOneBranch() {
234 String bodyWith1Branch = "{\n" +
236 " \"limit\": 25,\n" +
237 " \"isLastPage\": true,\n" +
238 " \"values\": [{\n" +
239 " \"id\": \"refs/heads/demo\",\n" +
240 " \"displayId\": \"demo\",\n" +
241 " \"type\": \"BRANCH\",\n" +
242 " \"latestCommit\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" +
243 " \"latestChangeset\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" +
244 " \"isDefault\": false\n" +
248 server.enqueue(new MockResponse()
249 .setHeader("Content-Type", "application/json;charset=UTF-8")
250 .setBody(bodyWith1Branch));
252 BranchesList branches = underTest.getBranches(server.url("/").toString(), "token", "projectSlug", "repoSlug");
253 assertThat(branches.getBranches()).hasSize(1);
255 Branch branch = branches.getBranches().get(0);
256 assertThat(branch.getName()).isEqualTo("demo");
257 assertThat(branch.isDefault()).isFalse();
262 public void getBranches_given2Branches_returnListWithTwoBranches() {
263 String bodyWith2Branches = "{\n" +
265 " \"limit\": 25,\n" +
266 " \"isLastPage\": true,\n" +
267 " \"values\": [{\n" +
268 " \"id\": \"refs/heads/demo\",\n" +
269 " \"displayId\": \"demo\",\n" +
270 " \"type\": \"BRANCH\",\n" +
271 " \"latestCommit\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" +
272 " \"latestChangeset\": \"3e30a6701af6f29f976e9a6609a6076b32a69ac3\",\n" +
273 " \"isDefault\": false\n" +
275 " \"id\": \"refs/heads/master\",\n" +
276 " \"displayId\": \"master\",\n" +
277 " \"type\": \"BRANCH\",\n" +
278 " \"latestCommit\": \"66633864d27c531ff43892f6dfea6d91632682fa\",\n" +
279 " \"latestChangeset\": \"66633864d27c531ff43892f6dfea6d91632682fa\",\n" +
280 " \"isDefault\": true\n" +
284 server.enqueue(new MockResponse()
285 .setHeader("Content-Type", "application/json;charset=UTF-8")
286 .setBody(bodyWith2Branches));
288 BranchesList branches = underTest.getBranches(server.url("/").toString(), "token", "projectSlug", "repoSlug");
290 assertThat(branches.getBranches()).hasSize(2);
294 public void invalid_url() {
295 assertThatThrownBy(() -> BitbucketServerRestClient.buildUrl("file://wrong-url", ""))
296 .isInstanceOf(IllegalArgumentException.class)
297 .hasMessage("url must start with http:// or https://");
301 public void malformed_json() {
302 server.enqueue(new MockResponse()
303 .setHeader("Content-Type", "application/json;charset=UTF-8")
305 "I'm malformed JSON"));
307 String serverUrl = server.url("/").toString();
308 assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
309 .isInstanceOf(IllegalArgumentException.class)
310 .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
314 public void error_handling() {
315 server.enqueue(new MockResponse()
316 .setHeader("Content-Type", "application/json;charset=UTF-8")
317 .setResponseCode(400)
321 " \"context\": null,\n" +
322 " \"message\": \"Bad message\",\n" +
323 " \"exceptionName\": \"com.atlassian.bitbucket.auth.BadException\"\n" +
328 String serverUrl = server.url("/").toString();
329 assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
330 .isInstanceOf(IllegalArgumentException.class)
331 .hasMessage("Unable to contact Bitbucket server");
335 public void unauthorized_error() {
336 server.enqueue(new MockResponse()
337 .setHeader("Content-Type", "application/json;charset=UTF-8")
338 .setResponseCode(401)
342 " \"context\": null,\n" +
343 " \"message\": \"Bad message\",\n" +
344 " \"exceptionName\": \"com.atlassian.bitbucket.auth.BadException\"\n" +
349 String serverUrl = server.url("/").toString();
350 assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
351 .isInstanceOf(IllegalArgumentException.class)
352 .hasMessage("Invalid personal access token");
356 public void fail_validate_on_io_exception() throws IOException {
359 String serverUrl = server.url("/").toString();
360 assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
361 .isInstanceOf(IllegalArgumentException.class)
362 .hasMessage("Unable to contact Bitbucket server");
364 assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server: Failed to connect");
368 public void fail_validate_url_on_non_json_result_log_correctly_the_response() {
369 server.enqueue(new MockResponse()
370 .setHeader("Content-Type", "application/json;charset=UTF-8")
371 .setResponseCode(500)
372 .setBody("not json"));
374 String serverUrl = server.url("/").toString();
375 assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
376 .isInstanceOf(IllegalArgumentException.class)
377 .hasMessage("Unable to contact Bitbucket server");
379 assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server: 500 not json");
383 public void fail_validate_url_on_text_result_log_the_returned_payload() {
384 server.enqueue(new MockResponse()
385 .setResponseCode(500)
386 .setBody("this is a text payload"));
388 String serverUrl = server.url("/").toString();
389 assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
390 .isInstanceOf(IllegalArgumentException.class)
391 .hasMessage("Unable to contact Bitbucket server");
393 assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server: 500 this is a text payload");
397 public void validate_url_success() {
398 server.enqueue(new MockResponse().setResponseCode(200)
399 .setBody(REPOS_BODY));
401 underTest.validateUrl(server.url("/").toString());
405 public void validate_url_fail_when_not_starting_with_protocol() {
406 assertThatThrownBy(() -> underTest.validateUrl("any_url_not_starting_with_http.com"))
407 .isInstanceOf(IllegalArgumentException.class)
408 .hasMessage("url must start with http:// or https://");
412 public void validate_token_success() {
413 server.enqueue(new MockResponse().setResponseCode(200)
417 " \"isLastPage\":true,\n" +
420 " \"name\":\"jean.michel\",\n" +
421 " \"emailAddress\":\"jean.michel@sonarsource.com\",\n" +
423 " \"displayName\":\"Jean Michel\",\n" +
424 " \"active\":true,\n" +
425 " \"slug\":\"jean.michel\",\n" +
426 " \"type\":\"NORMAL\",\n" +
430 " \"href\":\"https://bitbucket-testing.valiantys.sonarsource.com/users/jean.michel\"\n" +
436 " \"name\":\"prince.de.lu\",\n" +
437 " \"emailAddress\":\"prince.de.lu@sonarsource.com\",\n" +
439 " \"displayName\":\"Prince de Lu\",\n" +
440 " \"active\":true,\n" +
441 " \"slug\":\"prince.de.lu\",\n" +
442 " \"type\":\"NORMAL\",\n" +
446 " \"href\":\"https://bitbucket-testing.valiantys.sonarsource.com/users/prince.de.lu\"\n" +
455 underTest.validateToken(server.url("/").toString(), "token");
459 public void validate_read_permission_success() {
460 server.enqueue(new MockResponse().setResponseCode(200)
461 .setBody(REPOS_BODY));
463 underTest.validateReadPermission(server.url("/").toString(), "token");
467 public void fail_validate_url_when_on_http_error() {
468 server.enqueue(new MockResponse().setResponseCode(500)
469 .setBody("something unexpected"));
471 String serverUrl = server.url("/").toString();
472 assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
473 .isInstanceOf(IllegalArgumentException.class)
474 .hasMessage("Unable to contact Bitbucket server");
478 public void fail_validate_url_when_not_found_is_returned() {
479 server.enqueue(new MockResponse().setResponseCode(404)
480 .setBody("something unexpected"));
482 String serverUrl = server.url("/").toString();
483 assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
484 .isInstanceOf(BitbucketServerException.class)
485 .hasMessage("something unexpected")
486 .extracting(e -> ((BitbucketServerException) e).getHttpStatus()).isEqualTo(404);
490 public void fail_validate_url_when_validate_url_return_non_json_payload() {
491 server.enqueue(new MockResponse().setResponseCode(400)
492 .setBody("this is not a json payload"));
494 String serverUrl = server.url("/").toString();
495 assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
496 .isInstanceOf(IllegalArgumentException.class)
497 .hasMessage("Unable to contact Bitbucket server");
501 public void fail_validate_url_when_returning_non_json_payload_with_a_200_code() {
502 server.enqueue(new MockResponse().setResponseCode(200)
503 .setBody("this is not a json payload"));
505 String serverUrl = server.url("/").toString();
506 assertThatThrownBy(() -> {
507 underTest.validateUrl(serverUrl);
509 .isInstanceOf(IllegalArgumentException.class)
510 .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
514 public void fail_validate_token_when_server_return_non_json_payload() {
515 server.enqueue(new MockResponse().setResponseCode(400)
516 .setBody("this is not a json payload"));
518 String serverUrl = server.url("/").toString();
519 assertThatThrownBy(() -> underTest.validateToken(serverUrl, "token"))
520 .isInstanceOf(IllegalArgumentException.class)
521 .hasMessage("Unable to contact Bitbucket server");
525 public void fail_validate_token_when_returning_non_json_payload_with_a_200_code() {
526 server.enqueue(new MockResponse().setResponseCode(200)
527 .setBody("this is not a json payload"));
529 String serverUrl = server.url("/").toString();
530 assertThatThrownBy(() -> underTest.validateToken(serverUrl, "token"))
531 .isInstanceOf(IllegalArgumentException.class)
532 .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
536 public void fail_validate_token_when_using_an_invalid_token() {
537 server.enqueue(new MockResponse().setResponseCode(401)
538 .setBody("com.atlassian.bitbucket.AuthorisationException You are not permitted to access this resource"));
540 String serverUrl = server.url("/").toString();
541 assertThatThrownBy(() -> underTest.validateToken(serverUrl, "token"))
542 .isInstanceOf(IllegalArgumentException.class)
543 .hasMessage("Invalid personal access token");
547 public void fail_validate_read_permission_when_server_return_non_json_payload() {
548 server.enqueue(new MockResponse().setResponseCode(400)
549 .setBody("this is not a json payload"));
551 String serverUrl = server.url("/").toString();
552 assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
553 .isInstanceOf(IllegalArgumentException.class)
554 .hasMessage("Unable to contact Bitbucket server");
558 public void fail_validate_read_permission_when_returning_non_json_payload_with_a_200_code() {
559 server.enqueue(new MockResponse().setResponseCode(200)
560 .setBody("this is not a json payload"));
562 String serverUrl = server.url("/").toString();
563 assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
564 .isInstanceOf(IllegalArgumentException.class)
565 .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
569 public void fail_validate_read_permission_when_permissions_are_not_granted() {
570 server.enqueue(new MockResponse().setResponseCode(401)
571 .setBody("com.atlassian.bitbucket.AuthorisationException You are not permitted to access this resource"));
573 String serverUrl = server.url("/").toString();
574 assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
575 .isInstanceOf(IllegalArgumentException.class)
576 .hasMessage("Invalid personal access token");