]> source.dussan.org Git - sonarqube.git/blob
75d800ae4d41d80c6ea2b811a4b7c4e7ae2a598b
[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.github;
21
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.io.IOException;
26 import java.util.Optional;
27 import javax.annotation.Nullable;
28 import org.junit.Before;
29 import org.junit.ClassRule;
30 import org.junit.Test;
31 import org.junit.runner.RunWith;
32 import org.sonar.alm.client.github.security.AccessToken;
33 import org.sonar.alm.client.github.security.UserAccessToken;
34 import org.sonar.api.utils.log.LogTester;
35 import org.sonar.api.utils.log.LoggerLevel;
36
37 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
38 import static org.assertj.core.api.Assertions.assertThat;
39 import static org.assertj.core.api.Assertions.assertThatThrownBy;
40 import static org.assertj.core.groups.Tuple.tuple;
41 import static org.mockito.ArgumentMatchers.any;
42 import static org.mockito.Mockito.mock;
43 import static org.mockito.Mockito.verify;
44 import static org.mockito.Mockito.when;
45
46 @RunWith(DataProviderRunner.class)
47 public class GithubApplicationClientImplTest {
48
49   @ClassRule
50   public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
51
52   private GithubApplicationHttpClientImpl httpClient = mock(GithubApplicationHttpClientImpl.class);
53   private GithubApplicationClient underTest;
54
55   private String appUrl = "Any URL";
56
57   @Before
58   public void setup() {
59     underTest = new GithubApplicationClientImpl(httpClient);
60     logTester.clear();
61   }
62
63   @Test
64   @UseDataProvider("githubServers")
65   public void createUserAccessToken_returns_empty_if_access_token_cant_be_created(String apiUrl, String appUrl) throws IOException {
66     when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
67       .thenReturn(new Response(400, null));
68
69     assertThatThrownBy(() -> underTest.createUserAccessToken(appUrl, "clientId", "clientSecret", "code"))
70       .isInstanceOf(IllegalStateException.class);
71     verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
72   }
73
74   @Test
75   @UseDataProvider("githubServers")
76   public void createUserAccessToken_fail_if_access_token_request_fails(String apiUrl, String appUrl) throws IOException {
77     when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
78       .thenThrow(new IOException("OOPS"));
79
80     assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
81       .isInstanceOf(IllegalStateException.class)
82       .hasMessage("Failed to create GitHub's user access token");
83
84     verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
85   }
86
87   @Test
88   @UseDataProvider("githubServers")
89   public void createUserAccessToken_throws_illegal_argument_exception_if_access_token_code_is_expired(String apiUrl, String appUrl) throws IOException {
90     when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
91       .thenReturn(new OkGetResponse("error_code=100&error=expired_or_invalid"));
92
93     assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
94       .isInstanceOf(IllegalArgumentException.class);
95
96     verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
97   }
98
99   @Test
100   @UseDataProvider("githubServers")
101   public void createUserAccessToken_from_authorization_code_returns_access_token(String apiUrl, String appUrl) throws IOException {
102     String token = randomAlphanumeric(10);
103     when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
104       .thenReturn(new OkGetResponse("access_token=" + token + "&status="));
105
106     UserAccessToken userAccessToken = underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code");
107
108     assertThat(userAccessToken)
109       .extracting(UserAccessToken::getValue, UserAccessToken::getAuthorizationHeaderPrefix)
110       .containsOnly(token, "token");
111     verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
112   }
113
114   @DataProvider
115   public static Object[][] githubServers() {
116     return new Object[][] {
117       {"https://github.sonarsource.com/api/v3", "https://github.sonarsource.com"},
118       {"https://api.github.com", "https://github.com"},
119       {"https://github.sonarsource.com/api/v3/", "https://github.sonarsource.com"},
120       {"https://api.github.com/", "https://github.com"},
121     };
122   }
123
124   @Test
125   public void listOrganizations_fail_on_failure() throws IOException {
126     String appUrl = "https://github.sonarsource.com";
127     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
128
129     when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
130       .thenThrow(new IOException("OOPS"));
131
132     assertThatThrownBy(() -> underTest.listOrganizations(appUrl, accessToken, 1, 100))
133       .isInstanceOf(IllegalStateException.class)
134       .hasMessage("Failed to list all organizations accessible by user access token on %s", appUrl);
135   }
136
137   @Test
138   public void listOrganizations_fail_if_pageIndex_out_of_bounds() {
139     UserAccessToken token = new UserAccessToken("token");
140     assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 0, 100))
141       .isInstanceOf(IllegalArgumentException.class)
142       .hasMessage("'page' must be larger than 0.");
143   }
144
145   @Test
146   public void listOrganizations_fail_if_pageSize_out_of_bounds() {
147     UserAccessToken token = new UserAccessToken("token");
148     assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 1, 0))
149       .isInstanceOf(IllegalArgumentException.class)
150       .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
151     assertThatThrownBy(() -> underTest.listOrganizations("", token, 1, 101))
152       .isInstanceOf(IllegalArgumentException.class)
153       .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
154   }
155
156   @Test
157   public void listOrganizations_returns_no_installations() throws IOException {
158     String appUrl = "https://github.sonarsource.com";
159     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
160     String responseJson = "{\n"
161       + "  \"total_count\": 0\n"
162       + "} ";
163
164     when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
165       .thenReturn(new OkGetResponse(responseJson));
166
167     GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
168
169     assertThat(organizations.getTotal()).isZero();
170     assertThat(organizations.getOrganizations()).isNull();
171   }
172
173   @Test
174   public void listOrganizations_returns_pages_results() throws IOException {
175     String appUrl = "https://github.sonarsource.com";
176     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
177     String responseJson = "{\n"
178       + "  \"total_count\": 2,\n"
179       + "  \"installations\": [\n"
180       + "    {\n"
181       + "      \"id\": 1,\n"
182       + "      \"account\": {\n"
183       + "        \"login\": \"github\",\n"
184       + "        \"id\": 1,\n"
185       + "        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n"
186       + "        \"url\": \"https://github.sonarsource.com/api/v3/orgs/github\",\n"
187       + "        \"repos_url\": \"https://github.sonarsource.com/api/v3/orgs/github/repos\",\n"
188       + "        \"events_url\": \"https://github.sonarsource.com/api/v3/orgs/github/events\",\n"
189       + "        \"hooks_url\": \"https://github.sonarsource.com/api/v3/orgs/github/hooks\",\n"
190       + "        \"issues_url\": \"https://github.sonarsource.com/api/v3/orgs/github/issues\",\n"
191       + "        \"members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/members{/member}\",\n"
192       + "        \"public_members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/public_members{/member}\",\n"
193       + "        \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
194       + "        \"description\": \"A great organization\"\n"
195       + "      },\n"
196       + "      \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
197       + "      \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
198       + "      \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
199       + "      \"app_id\": 1,\n"
200       + "      \"target_id\": 1,\n"
201       + "      \"target_type\": \"Organization\",\n"
202       + "      \"permissions\": {\n"
203       + "        \"checks\": \"write\",\n"
204       + "        \"metadata\": \"read\",\n"
205       + "        \"contents\": \"read\"\n"
206       + "      },\n"
207       + "      \"events\": [\n"
208       + "        \"push\",\n"
209       + "        \"pull_request\"\n"
210       + "      ],\n"
211       + "      \"single_file_name\": \"config.yml\"\n"
212       + "    },\n"
213       + "    {\n"
214       + "      \"id\": 3,\n"
215       + "      \"account\": {\n"
216       + "        \"login\": \"octocat\",\n"
217       + "        \"id\": 2,\n"
218       + "        \"node_id\": \"MDQ6VXNlcjE=\",\n"
219       + "        \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
220       + "        \"gravatar_id\": \"\",\n"
221       + "        \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
222       + "        \"html_url\": \"https://github.com/octocat\",\n"
223       + "        \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
224       + "        \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
225       + "        \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
226       + "        \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
227       + "        \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
228       + "        \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
229       + "        \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
230       + "        \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
231       + "        \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
232       + "        \"type\": \"User\",\n"
233       + "        \"site_admin\": false\n"
234       + "      },\n"
235       + "      \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
236       + "      \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
237       + "      \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
238       + "      \"app_id\": 1,\n"
239       + "      \"target_id\": 1,\n"
240       + "      \"target_type\": \"Organization\",\n"
241       + "      \"permissions\": {\n"
242       + "        \"checks\": \"write\",\n"
243       + "        \"metadata\": \"read\",\n"
244       + "        \"contents\": \"read\"\n"
245       + "      },\n"
246       + "      \"events\": [\n"
247       + "        \"push\",\n"
248       + "        \"pull_request\"\n"
249       + "      ],\n"
250       + "      \"single_file_name\": \"config.yml\"\n"
251       + "    }\n"
252       + "  ]\n"
253       + "} ";
254
255     when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
256       .thenReturn(new OkGetResponse(responseJson));
257
258     GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
259
260     assertThat(organizations.getTotal()).isEqualTo(2);
261     assertThat(organizations.getOrganizations()).extracting(GithubApplicationClient.Organization::getLogin).containsOnly("github", "octocat");
262   }
263
264   @Test
265   public void listRepositories_fail_on_failure() throws IOException {
266     String appUrl = "https://github.sonarsource.com";
267     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
268
269     when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:test", 1, 100)))
270       .thenThrow(new IOException("OOPS"));
271
272     assertThatThrownBy(() -> underTest.listRepositories(appUrl, accessToken, "test", null, 1, 100))
273       .isInstanceOf(IllegalStateException.class)
274       .hasMessage("Failed to list all repositories of 'test' accessible by user access token on 'https://github.sonarsource.com' using query 'org:test'");
275   }
276
277   @Test
278   public void listRepositories_fail_if_pageIndex_out_of_bounds() {
279     UserAccessToken token = new UserAccessToken("token");
280     assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 0, 100))
281       .isInstanceOf(IllegalArgumentException.class)
282       .hasMessage("'page' must be larger than 0.");
283   }
284
285   @Test
286   public void listRepositories_fail_if_pageSize_out_of_bounds() {
287     UserAccessToken token = new UserAccessToken("token");
288     assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 1, 0))
289       .isInstanceOf(IllegalArgumentException.class)
290       .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
291     assertThatThrownBy(() -> underTest.listRepositories("", token, "test", null, 1, 101))
292       .isInstanceOf(IllegalArgumentException.class)
293       .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
294   }
295
296   @Test
297   public void listRepositories_returns_empty_results() throws IOException {
298     String appUrl = "https://github.sonarsource.com";
299     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
300     String responseJson = "{\n"
301       + "  \"total_count\": 0\n"
302       + "}";
303
304     when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:github", 1, 100)))
305       .thenReturn(new OkGetResponse(responseJson));
306
307     GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
308
309     assertThat(repositories.getTotal()).isZero();
310     assertThat(repositories.getRepositories()).isNull();
311   }
312
313   @Test
314   public void listRepositories_returns_pages_results() throws IOException {
315     String appUrl = "https://github.sonarsource.com";
316     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
317     String responseJson = "{\n"
318       + "  \"total_count\": 2,\n"
319       + "  \"incomplete_results\": false,\n"
320       + "  \"items\": [\n"
321       + "    {\n"
322       + "      \"id\": 3081286,\n"
323       + "      \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
324       + "      \"name\": \"HelloWorld\",\n"
325       + "      \"full_name\": \"github/HelloWorld\",\n"
326       + "      \"owner\": {\n"
327       + "        \"login\": \"github\",\n"
328       + "        \"id\": 872147,\n"
329       + "        \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
330       + "        \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
331       + "        \"gravatar_id\": \"\",\n"
332       + "        \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
333       + "        \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
334       + "        \"type\": \"User\"\n"
335       + "      },\n"
336       + "      \"private\": false,\n"
337       + "      \"html_url\": \"https://github.com/github/HelloWorld\",\n"
338       + "      \"description\": \"A C implementation of HelloWorld\",\n"
339       + "      \"fork\": false,\n"
340       + "      \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
341       + "      \"created_at\": \"2012-01-01T00:31:50Z\",\n"
342       + "      \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
343       + "      \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
344       + "      \"homepage\": \"\",\n"
345       + "      \"size\": 524,\n"
346       + "      \"stargazers_count\": 1,\n"
347       + "      \"watchers_count\": 1,\n"
348       + "      \"language\": \"Assembly\",\n"
349       + "      \"forks_count\": 0,\n"
350       + "      \"open_issues_count\": 0,\n"
351       + "      \"master_branch\": \"master\",\n"
352       + "      \"default_branch\": \"master\",\n"
353       + "      \"score\": 1.0\n"
354       + "    },\n"
355       + "    {\n"
356       + "      \"id\": 3081286,\n"
357       + "      \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
358       + "      \"name\": \"HelloUniverse\",\n"
359       + "      \"full_name\": \"github/HelloUniverse\",\n"
360       + "      \"owner\": {\n"
361       + "        \"login\": \"github\",\n"
362       + "        \"id\": 872147,\n"
363       + "        \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
364       + "        \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
365       + "        \"gravatar_id\": \"\",\n"
366       + "        \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
367       + "        \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
368       + "        \"type\": \"User\"\n"
369       + "      },\n"
370       + "      \"private\": false,\n"
371       + "      \"html_url\": \"https://github.com/github/HelloUniverse\",\n"
372       + "      \"description\": \"A C implementation of HelloUniverse\",\n"
373       + "      \"fork\": false,\n"
374       + "      \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloUniverse\",\n"
375       + "      \"created_at\": \"2012-01-01T00:31:50Z\",\n"
376       + "      \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
377       + "      \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
378       + "      \"homepage\": \"\",\n"
379       + "      \"size\": 524,\n"
380       + "      \"stargazers_count\": 1,\n"
381       + "      \"watchers_count\": 1,\n"
382       + "      \"language\": \"Assembly\",\n"
383       + "      \"forks_count\": 0,\n"
384       + "      \"open_issues_count\": 0,\n"
385       + "      \"master_branch\": \"master\",\n"
386       + "      \"default_branch\": \"master\",\n"
387       + "      \"score\": 1.0\n"
388       + "    }\n"
389       + "  ]\n"
390       + "}";
391
392     when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:github", 1, 100)))
393       .thenReturn(new OkGetResponse(responseJson));
394     GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
395
396     assertThat(repositories.getTotal()).isEqualTo(2);
397     assertThat(repositories.getRepositories())
398       .extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName)
399       .containsOnly(tuple("HelloWorld", "github/HelloWorld"), tuple("HelloUniverse", "github/HelloUniverse"));
400   }
401
402   @Test
403   public void listRepositories_returns_search_results() throws IOException {
404     String appUrl = "https://github.sonarsource.com";
405     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
406     String responseJson = "{\n"
407       + "  \"total_count\": 2,\n"
408       + "  \"incomplete_results\": false,\n"
409       + "  \"items\": [\n"
410       + "    {\n"
411       + "      \"id\": 3081286,\n"
412       + "      \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
413       + "      \"name\": \"HelloWorld\",\n"
414       + "      \"full_name\": \"github/HelloWorld\",\n"
415       + "      \"owner\": {\n"
416       + "        \"login\": \"github\",\n"
417       + "        \"id\": 872147,\n"
418       + "        \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
419       + "        \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
420       + "        \"gravatar_id\": \"\",\n"
421       + "        \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
422       + "        \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
423       + "        \"type\": \"User\"\n"
424       + "      },\n"
425       + "      \"private\": false,\n"
426       + "      \"html_url\": \"https://github.com/github/HelloWorld\",\n"
427       + "      \"description\": \"A C implementation of HelloWorld\",\n"
428       + "      \"fork\": false,\n"
429       + "      \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
430       + "      \"created_at\": \"2012-01-01T00:31:50Z\",\n"
431       + "      \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
432       + "      \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
433       + "      \"homepage\": \"\",\n"
434       + "      \"size\": 524,\n"
435       + "      \"stargazers_count\": 1,\n"
436       + "      \"watchers_count\": 1,\n"
437       + "      \"language\": \"Assembly\",\n"
438       + "      \"forks_count\": 0,\n"
439       + "      \"open_issues_count\": 0,\n"
440       + "      \"master_branch\": \"master\",\n"
441       + "      \"default_branch\": \"master\",\n"
442       + "      \"score\": 1.0\n"
443       + "    }\n"
444       + "  ]\n"
445       + "}";
446
447     when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+org:github", 1, 100)))
448       .thenReturn(new GithubApplicationHttpClient.GetResponse() {
449         @Override
450         public Optional<String> getNextEndPoint() {
451           return Optional.empty();
452         }
453
454         @Override
455         public int getCode() {
456           return 200;
457         }
458
459         @Override
460         public Optional<String> getContent() {
461           return Optional.of(responseJson);
462         }
463       });
464
465     GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", "world", 1, 100);
466
467     assertThat(repositories.getTotal()).isEqualTo(2);
468     assertThat(repositories.getRepositories())
469       .extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName)
470       .containsOnly(tuple("HelloWorld", "github/HelloWorld"));
471   }
472
473   @Test
474   public void getRepository_returns_empty_when_repository_doesnt_exist() throws IOException {
475     when(httpClient.get(any(), any(), any()))
476       .thenReturn(new Response(404, null));
477
478     Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, new UserAccessToken("temp"), "octocat", "octocat/Hello-World");
479
480     assertThat(repository).isEmpty();
481   }
482
483   @Test
484   public void getRepository_fails_on_failure() throws IOException {
485     String repositoryKey = "octocat/Hello-World";
486     String organization = "octocat";
487
488     when(httpClient.get(any(), any(), any()))
489       .thenThrow(new IOException("OOPS"));
490
491     UserAccessToken token = new UserAccessToken("temp");
492     assertThatThrownBy(() -> underTest.getRepository(appUrl, token, organization, repositoryKey))
493       .isInstanceOf(IllegalStateException.class)
494       .hasMessage("Failed to get repository '%s' of '%s' accessible by user access token on '%s'", repositoryKey, organization, appUrl);
495   }
496
497   @Test
498   public void getRepository_returns_repository() throws IOException {
499     String appUrl = "https://github.sonarsource.com";
500     AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
501     String responseJson = "{\n"
502       + "  \"id\": 1296269,\n"
503       + "  \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n"
504       + "  \"name\": \"Hello-World\",\n"
505       + "  \"full_name\": \"octocat/Hello-World\",\n"
506       + "  \"owner\": {\n"
507       + "    \"login\": \"octocat\",\n"
508       + "    \"id\": 1,\n"
509       + "    \"node_id\": \"MDQ6VXNlcjE=\",\n"
510       + "    \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
511       + "    \"gravatar_id\": \"\",\n"
512       + "    \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
513       + "    \"html_url\": \"https://github.com/octocat\",\n"
514       + "    \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
515       + "    \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
516       + "    \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
517       + "    \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
518       + "    \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
519       + "    \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
520       + "    \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
521       + "    \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
522       + "    \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
523       + "    \"type\": \"User\",\n"
524       + "    \"site_admin\": false\n"
525       + "  },\n"
526       + "  \"private\": false,\n"
527       + "  \"html_url\": \"https://github.com/octocat/Hello-World\",\n"
528       + "  \"description\": \"This your first repo!\",\n"
529       + "  \"fork\": false,\n"
530       + "  \"url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World\",\n"
531       + "  \"archive_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/{archive_format}{/ref}\",\n"
532       + "  \"assignees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/assignees{/user}\",\n"
533       + "  \"blobs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/blobs{/sha}\",\n"
534       + "  \"branches_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/branches{/branch}\",\n"
535       + "  \"collaborators_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/collaborators{/collaborator}\",\n"
536       + "  \"comments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/comments{/number}\",\n"
537       + "  \"commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/commits{/sha}\",\n"
538       + "  \"compare_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/compare/{base}...{head}\",\n"
539       + "  \"contents_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contents/{+path}\",\n"
540       + "  \"contributors_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contributors\",\n"
541       + "  \"deployments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/deployments\",\n"
542       + "  \"downloads_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/downloads\",\n"
543       + "  \"events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/events\",\n"
544       + "  \"forks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/forks\",\n"
545       + "  \"git_commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/commits{/sha}\",\n"
546       + "  \"git_refs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/refs{/sha}\",\n"
547       + "  \"git_tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/tags{/sha}\",\n"
548       + "  \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n"
549       + "  \"issue_comment_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/comments{/number}\",\n"
550       + "  \"issue_events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/events{/number}\",\n"
551       + "  \"issues_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues{/number}\",\n"
552       + "  \"keys_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/keys{/key_id}\",\n"
553       + "  \"labels_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/labels{/name}\",\n"
554       + "  \"languages_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/languages\",\n"
555       + "  \"merges_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/merges\",\n"
556       + "  \"milestones_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/milestones{/number}\",\n"
557       + "  \"notifications_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n"
558       + "  \"pulls_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/pulls{/number}\",\n"
559       + "  \"releases_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/releases{/id}\",\n"
560       + "  \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n"
561       + "  \"stargazers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/stargazers\",\n"
562       + "  \"statuses_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/statuses/{sha}\",\n"
563       + "  \"subscribers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscribers\",\n"
564       + "  \"subscription_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscription\",\n"
565       + "  \"tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/tags\",\n"
566       + "  \"teams_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/teams\",\n"
567       + "  \"trees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/trees{/sha}\",\n"
568       + "  \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n"
569       + "  \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n"
570       + "  \"hooks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/hooks\",\n"
571       + "  \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n"
572       + "  \"homepage\": \"https://github.com\",\n"
573       + "  \"language\": null,\n"
574       + "  \"forks_count\": 9,\n"
575       + "  \"stargazers_count\": 80,\n"
576       + "  \"watchers_count\": 80,\n"
577       + "  \"size\": 108,\n"
578       + "  \"default_branch\": \"master\",\n"
579       + "  \"open_issues_count\": 0,\n"
580       + "  \"is_template\": true,\n"
581       + "  \"topics\": [\n"
582       + "    \"octocat\",\n"
583       + "    \"atom\",\n"
584       + "    \"electron\",\n"
585       + "    \"api\"\n"
586       + "  ],\n"
587       + "  \"has_issues\": true,\n"
588       + "  \"has_projects\": true,\n"
589       + "  \"has_wiki\": true,\n"
590       + "  \"has_pages\": false,\n"
591       + "  \"has_downloads\": true,\n"
592       + "  \"archived\": false,\n"
593       + "  \"disabled\": false,\n"
594       + "  \"visibility\": \"public\",\n"
595       + "  \"pushed_at\": \"2011-01-26T19:06:43Z\",\n"
596       + "  \"created_at\": \"2011-01-26T19:01:12Z\",\n"
597       + "  \"updated_at\": \"2011-01-26T19:14:43Z\",\n"
598       + "  \"permissions\": {\n"
599       + "    \"admin\": false,\n"
600       + "    \"push\": false,\n"
601       + "    \"pull\": true\n"
602       + "  },\n"
603       + "  \"allow_rebase_merge\": true,\n"
604       + "  \"template_repository\": null,\n"
605       + "  \"allow_squash_merge\": true,\n"
606       + "  \"allow_merge_commit\": true,\n"
607       + "  \"subscribers_count\": 42,\n"
608       + "  \"network_count\": 0,\n"
609       + "  \"anonymous_access_enabled\": false,\n"
610       + "  \"license\": {\n"
611       + "    \"key\": \"mit\",\n"
612       + "    \"name\": \"MIT License\",\n"
613       + "    \"spdx_id\": \"MIT\",\n"
614       + "    \"url\": \"https://github.sonarsource.com/api/v3/licenses/mit\",\n"
615       + "    \"node_id\": \"MDc6TGljZW5zZW1pdA==\"\n"
616       + "  },\n"
617       + "  \"organization\": {\n"
618       + "    \"login\": \"octocat\",\n"
619       + "    \"id\": 1,\n"
620       + "    \"node_id\": \"MDQ6VXNlcjE=\",\n"
621       + "    \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
622       + "    \"gravatar_id\": \"\",\n"
623       + "    \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
624       + "    \"html_url\": \"https://github.com/octocat\",\n"
625       + "    \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
626       + "    \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
627       + "    \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
628       + "    \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
629       + "    \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
630       + "    \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
631       + "    \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
632       + "    \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
633       + "    \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
634       + "    \"type\": \"Organization\",\n"
635       + "    \"site_admin\": false\n"
636       + "  }"
637       + "}";
638
639     when(httpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World"))
640       .thenReturn(new GithubApplicationHttpClient.GetResponse() {
641         @Override
642         public Optional<String> getNextEndPoint() {
643           return Optional.empty();
644         }
645
646         @Override
647         public int getCode() {
648           return 200;
649         }
650
651         @Override
652         public Optional<String> getContent() {
653           return Optional.of(responseJson);
654         }
655       });
656
657     Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, accessToken, "octocat", "octocat/Hello-World");
658
659     assertThat(repository)
660       .isPresent()
661       .get()
662       .extracting(GithubApplicationClient.Repository::getId, GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName,
663         GithubApplicationClient.Repository::getUrl, GithubApplicationClient.Repository::isPrivate)
664       .containsOnly(1296269L, "Hello-World", "octocat/Hello-World", "https://github.sonarsource.com/api/v3/repos/octocat/Hello-World", false);
665   }
666
667   private static class OkGetResponse extends Response {
668     private OkGetResponse(String content) {
669       super(200, content);
670     }
671   }
672
673   private static class Response implements GithubApplicationHttpClient.GetResponse {
674     private final int code;
675     private final String content;
676     private final String nextEndPoint;
677
678     private Response(int code, @Nullable String content) {
679       this(code, content, null);
680     }
681
682     private Response(int code, @Nullable String content, @Nullable String nextEndPoint) {
683       this.code = code;
684       this.content = content;
685       this.nextEndPoint = nextEndPoint;
686     }
687
688     @Override
689     public int getCode() {
690       return code;
691     }
692
693     @Override
694     public Optional<String> getContent() {
695       return Optional.ofNullable(content);
696     }
697
698     @Override
699     public Optional<String> getNextEndPoint() {
700       return Optional.ofNullable(nextEndPoint);
701     }
702   }
703 }