]> source.dussan.org Git - sonarqube.git/blob
8106af46ba068b2a0e1151de23746d7893f26ae8
[sonarqube.git] /
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.bitbucketserver;
21
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;
31
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;
35
36 public class BitbucketServerRestClientTest {
37   private final MockWebServer server = new MockWebServer();
38   private static final String REPOS_BODY = "{\n" +
39     "  \"isLastPage\": true,\n" +
40     "  \"values\": [\n" +
41     "    {\n" +
42     "      \"slug\": \"banana\",\n" +
43     "      \"id\": 2,\n" +
44     "      \"name\": \"banana\",\n" +
45     "      \"project\": {\n" +
46     "        \"key\": \"HOY\",\n" +
47     "        \"id\": 2,\n" +
48     "        \"name\": \"hoy\"\n" +
49     "      }\n" +
50     "    },\n" +
51     "    {\n" +
52     "      \"slug\": \"potato\",\n" +
53     "      \"id\": 1,\n" +
54     "      \"name\": \"potato\",\n" +
55     "      \"project\": {\n" +
56     "        \"key\": \"HEY\",\n" +
57     "        \"id\": 1,\n" +
58     "        \"name\": \"hey\"\n" +
59     "      }\n" +
60     "    }\n" +
61     "  ]\n" +
62     "}";
63
64   @Rule
65   public LogTester logTester = new LogTester();
66
67   private BitbucketServerRestClient underTest;
68
69   @Before
70   public void prepare() throws IOException {
71     server.start();
72
73     underTest = new BitbucketServerRestClient(new ConstantTimeoutConfiguration(500));
74   }
75
76   @After
77   public void stopServer() throws IOException {
78     server.shutdown();
79   }
80
81   @Test
82   public void get_repos() {
83     server.enqueue(new MockResponse()
84       .setHeader("Content-Type", "application/json;charset=UTF-8")
85       .setBody("{\n" +
86         "  \"isLastPage\": true,\n" +
87         "  \"values\": [\n" +
88         "    {\n" +
89         "      \"slug\": \"banana\",\n" +
90         "      \"id\": 2,\n" +
91         "      \"name\": \"banana\",\n" +
92         "      \"project\": {\n" +
93         "        \"key\": \"HOY\",\n" +
94         "        \"id\": 2,\n" +
95         "        \"name\": \"hoy\"\n" +
96         "      }\n" +
97         "    },\n" +
98         "    {\n" +
99         "      \"slug\": \"potato\",\n" +
100         "      \"id\": 1,\n" +
101         "      \"name\": \"potato\",\n" +
102         "      \"project\": {\n" +
103         "        \"key\": \"HEY\",\n" +
104         "        \"id\": 1,\n" +
105         "        \"name\": \"hey\"\n" +
106         "      }\n" +
107         "    }\n" +
108         "  ]\n" +
109         "}"));
110
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"));
119   }
120
121   @Test
122   public void get_recent_repos() {
123     server.enqueue(new MockResponse()
124       .setHeader("Content-Type", "application/json;charset=UTF-8")
125       .setBody("{\n" +
126         "  \"isLastPage\": true,\n" +
127         "  \"values\": [\n" +
128         "    {\n" +
129         "      \"slug\": \"banana\",\n" +
130         "      \"id\": 2,\n" +
131         "      \"name\": \"banana\",\n" +
132         "      \"project\": {\n" +
133         "        \"key\": \"HOY\",\n" +
134         "        \"id\": 2,\n" +
135         "        \"name\": \"hoy\"\n" +
136         "      }\n" +
137         "    },\n" +
138         "    {\n" +
139         "      \"slug\": \"potato\",\n" +
140         "      \"id\": 1,\n" +
141         "      \"name\": \"potato\",\n" +
142         "      \"project\": {\n" +
143         "        \"key\": \"HEY\",\n" +
144         "        \"id\": 1,\n" +
145         "        \"name\": \"hey\"\n" +
146         "      }\n" +
147         "    }\n" +
148         "  ]\n" +
149         "}"));
150
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"));
159   }
160
161   @Test
162   public void get_repo() {
163     server.enqueue(new MockResponse()
164       .setHeader("Content-Type", "application/json;charset=UTF-8")
165       .setBody(
166         "    {" +
167           "      \"slug\": \"banana-slug\"," +
168           "      \"id\": 2,\n" +
169           "      \"name\": \"banana\"," +
170           "      \"project\": {\n" +
171           "        \"key\": \"HOY\"," +
172           "        \"id\": 3,\n" +
173           "        \"name\": \"hoy\"" +
174           "      }" +
175           "    }"));
176
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");
184   }
185
186   @Test
187   public void get_projects() {
188     server.enqueue(new MockResponse()
189       .setHeader("Content-Type", "application/json;charset=UTF-8")
190       .setBody("{\n" +
191         "  \"isLastPage\": true,\n" +
192         "  \"values\": [\n" +
193         "    {\n" +
194         "      \"key\": \"HEY\",\n" +
195         "      \"id\": 1,\n" +
196         "      \"name\": \"hey\"\n" +
197         "    },\n" +
198         "    {\n" +
199         "      \"key\": \"HOY\",\n" +
200         "      \"id\": 2,\n" +
201         "      \"name\": \"hoy\"\n" +
202         "    }\n" +
203         "  ]\n" +
204         "}"));
205
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"));
212   }
213
214   @Test
215   public void getBranches_given0Branches_returnEmptyList() {
216     String bodyWith0Branches = "{\n" +
217       "  \"size\": 0,\n" +
218       "  \"limit\": 25,\n" +
219       "  \"isLastPage\": true,\n" +
220       "  \"values\": [],\n" +
221       "  \"start\": 0\n" +
222       "}";
223     server.enqueue(new MockResponse()
224       .setHeader("Content-Type", "application/json;charset=UTF-8")
225       .setBody(bodyWith0Branches));
226
227     BranchesList branches = underTest.getBranches(server.url("/").toString(), "token", "projectSlug", "repoSlug");
228
229     assertThat(branches.getBranches()).isEmpty();
230   }
231
232   @Test
233   public void getBranches_given1Branch_returnListWithOneBranch() {
234     String bodyWith1Branch = "{\n" +
235       "  \"size\": 1,\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" +
245       "  }],\n" +
246       "  \"start\": 0\n" +
247       "}";
248     server.enqueue(new MockResponse()
249       .setHeader("Content-Type", "application/json;charset=UTF-8")
250       .setBody(bodyWith1Branch));
251
252     BranchesList branches = underTest.getBranches(server.url("/").toString(), "token", "projectSlug", "repoSlug");
253     assertThat(branches.getBranches()).hasSize(1);
254
255     Branch branch = branches.getBranches().get(0);
256     assertThat(branch.getName()).isEqualTo("demo");
257     assertThat(branch.isDefault()).isFalse();
258
259   }
260
261   @Test
262   public void getBranches_given2Branches_returnListWithTwoBranches() {
263     String bodyWith2Branches = "{\n" +
264       "  \"size\": 2,\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" +
274       "  }, {\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" +
281       "  }],\n" +
282       "  \"start\": 0\n" +
283       "}";
284     server.enqueue(new MockResponse()
285       .setHeader("Content-Type", "application/json;charset=UTF-8")
286       .setBody(bodyWith2Branches));
287
288     BranchesList branches = underTest.getBranches(server.url("/").toString(), "token", "projectSlug", "repoSlug");
289
290     assertThat(branches.getBranches()).hasSize(2);
291   }
292
293   @Test
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://");
298   }
299
300   @Test
301   public void malformed_json() {
302     server.enqueue(new MockResponse()
303       .setHeader("Content-Type", "application/json;charset=UTF-8")
304       .setBody(
305         "I'm malformed JSON"));
306
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");
311   }
312
313   @Test
314   public void error_handling() {
315     server.enqueue(new MockResponse()
316       .setHeader("Content-Type", "application/json;charset=UTF-8")
317       .setResponseCode(400)
318       .setBody("{\n" +
319         "  \"errors\": [\n" +
320         "    {\n" +
321         "      \"context\": null,\n" +
322         "      \"message\": \"Bad message\",\n" +
323         "      \"exceptionName\": \"com.atlassian.bitbucket.auth.BadException\"\n" +
324         "    }\n" +
325         "  ]\n" +
326         "}"));
327
328     String serverUrl = server.url("/").toString();
329     assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
330       .isInstanceOf(IllegalArgumentException.class)
331       .hasMessage("Unable to contact Bitbucket server");
332   }
333
334   @Test
335   public void unauthorized_error() {
336     server.enqueue(new MockResponse()
337       .setHeader("Content-Type", "application/json;charset=UTF-8")
338       .setResponseCode(401)
339       .setBody("{\n" +
340         "  \"errors\": [\n" +
341         "    {\n" +
342         "      \"context\": null,\n" +
343         "      \"message\": \"Bad message\",\n" +
344         "      \"exceptionName\": \"com.atlassian.bitbucket.auth.BadException\"\n" +
345         "    }\n" +
346         "  ]\n" +
347         "}"));
348
349     String serverUrl = server.url("/").toString();
350     assertThatThrownBy(() -> underTest.getRepo(serverUrl, "token", "", ""))
351       .isInstanceOf(IllegalArgumentException.class)
352       .hasMessage("Invalid personal access token");
353   }
354
355   @Test
356   public void fail_validate_on_io_exception() throws IOException {
357     server.shutdown();
358
359     String serverUrl = server.url("/").toString();
360     assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
361       .isInstanceOf(IllegalArgumentException.class)
362       .hasMessage("Unable to contact Bitbucket server");
363
364     assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server: Failed to connect");
365   }
366
367   @Test
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"));
373
374     String serverUrl = server.url("/").toString();
375     assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
376       .isInstanceOf(IllegalArgumentException.class)
377       .hasMessage("Unable to contact Bitbucket server");
378
379     assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server: 500 not json");
380   }
381
382   @Test
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"));
387
388     String serverUrl = server.url("/").toString();
389     assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
390       .isInstanceOf(IllegalArgumentException.class)
391       .hasMessage("Unable to contact Bitbucket server");
392
393     assertThat(String.join(", ", logTester.logs())).contains("Unable to contact Bitbucket server: 500 this is a text payload");
394   }
395
396   @Test
397   public void validate_url_success() {
398     server.enqueue(new MockResponse().setResponseCode(200)
399       .setBody(REPOS_BODY));
400
401     underTest.validateUrl(server.url("/").toString());
402   }
403
404   @Test
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://");
409   }
410
411   @Test
412   public void validate_token_success() {
413     server.enqueue(new MockResponse().setResponseCode(200)
414       .setBody("{\n" +
415         "   \"size\":10,\n" +
416         "   \"limit\":25,\n" +
417         "   \"isLastPage\":true,\n" +
418         "   \"values\":[\n" +
419         "      {\n" +
420         "         \"name\":\"jean.michel\",\n" +
421         "         \"emailAddress\":\"jean.michel@sonarsource.com\",\n" +
422         "         \"id\":2,\n" +
423         "         \"displayName\":\"Jean Michel\",\n" +
424         "         \"active\":true,\n" +
425         "         \"slug\":\"jean.michel\",\n" +
426         "         \"type\":\"NORMAL\",\n" +
427         "         \"links\":{\n" +
428         "            \"self\":[\n" +
429         "               {\n" +
430         "                  \"href\":\"https://bitbucket-testing.valiantys.sonarsource.com/users/jean.michel\"\n" +
431         "               }\n" +
432         "            ]\n" +
433         "         }\n" +
434         "      },\n" +
435         "      {\n" +
436         "         \"name\":\"prince.de.lu\",\n" +
437         "         \"emailAddress\":\"prince.de.lu@sonarsource.com\",\n" +
438         "         \"id\":103,\n" +
439         "         \"displayName\":\"Prince de Lu\",\n" +
440         "         \"active\":true,\n" +
441         "         \"slug\":\"prince.de.lu\",\n" +
442         "         \"type\":\"NORMAL\",\n" +
443         "         \"links\":{\n" +
444         "            \"self\":[\n" +
445         "               {\n" +
446         "                  \"href\":\"https://bitbucket-testing.valiantys.sonarsource.com/users/prince.de.lu\"\n" +
447         "               }\n" +
448         "            ]\n" +
449         "         }\n" +
450         "      },\n" +
451         "   ],\n" +
452         "   \"start\":0\n" +
453         "}"));
454
455     underTest.validateToken(server.url("/").toString(), "token");
456   }
457
458   @Test
459   public void validate_read_permission_success() {
460     server.enqueue(new MockResponse().setResponseCode(200)
461       .setBody(REPOS_BODY));
462
463     underTest.validateReadPermission(server.url("/").toString(), "token");
464   }
465
466   @Test
467   public void fail_validate_url_when_on_http_error() {
468     server.enqueue(new MockResponse().setResponseCode(500)
469       .setBody("something unexpected"));
470
471     String serverUrl = server.url("/").toString();
472     assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
473       .isInstanceOf(IllegalArgumentException.class)
474       .hasMessage("Unable to contact Bitbucket server");
475   }
476
477   @Test
478   public void fail_validate_url_when_not_found_is_returned() {
479     server.enqueue(new MockResponse().setResponseCode(404)
480       .setBody("something unexpected"));
481
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);
487   }
488
489   @Test
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"));
493
494     String serverUrl = server.url("/").toString();
495     assertThatThrownBy(() -> underTest.validateUrl(serverUrl))
496       .isInstanceOf(IllegalArgumentException.class)
497       .hasMessage("Unable to contact Bitbucket server");
498   }
499
500   @Test
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"));
504
505     String serverUrl = server.url("/").toString();
506     assertThatThrownBy(() -> {
507       underTest.validateUrl(serverUrl);
508     })
509       .isInstanceOf(IllegalArgumentException.class)
510       .hasMessage("Unable to contact Bitbucket server, got an unexpected response");
511   }
512
513   @Test
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"));
517
518     String serverUrl = server.url("/").toString();
519     assertThatThrownBy(() -> underTest.validateToken(serverUrl, "token"))
520       .isInstanceOf(IllegalArgumentException.class)
521       .hasMessage("Unable to contact Bitbucket server");
522   }
523
524   @Test
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"));
528
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");
533   }
534
535   @Test
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"));
539
540     String serverUrl = server.url("/").toString();
541     assertThatThrownBy(() -> underTest.validateToken(serverUrl, "token"))
542       .isInstanceOf(IllegalArgumentException.class)
543       .hasMessage("Invalid personal access token");
544   }
545
546   @Test
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"));
550
551     String serverUrl = server.url("/").toString();
552     assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
553       .isInstanceOf(IllegalArgumentException.class)
554       .hasMessage("Unable to contact Bitbucket server");
555   }
556
557   @Test
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"));
561
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");
566   }
567
568   @Test
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"));
572
573     String serverUrl = server.url("/").toString();
574     assertThatThrownBy(() -> underTest.validateReadPermission(serverUrl, "token"))
575       .isInstanceOf(IllegalArgumentException.class)
576       .hasMessage("Invalid personal access token");
577   }
578
579 }