3 * Copyright (C) 2009-2023 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.github;
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.config.GithubAppConfiguration;
33 import org.sonar.alm.client.github.security.AccessToken;
34 import org.sonar.alm.client.github.security.AppToken;
35 import org.sonar.alm.client.github.security.GithubAppSecurity;
36 import org.sonar.alm.client.github.security.UserAccessToken;
37 import org.sonar.api.utils.log.LogTester;
38 import org.sonar.api.utils.log.LoggerLevel;
40 import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
41 import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
42 import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
43 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
44 import static org.assertj.core.api.Assertions.assertThat;
45 import static org.assertj.core.api.Assertions.assertThatCode;
46 import static org.assertj.core.api.Assertions.assertThatThrownBy;
47 import static org.assertj.core.groups.Tuple.tuple;
48 import static org.mockito.ArgumentMatchers.any;
49 import static org.mockito.Mockito.mock;
50 import static org.mockito.Mockito.verify;
51 import static org.mockito.Mockito.when;
53 @RunWith(DataProviderRunner.class)
54 public class GithubApplicationClientImplTest {
57 public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
59 private GithubApplicationHttpClientImpl httpClient = mock(GithubApplicationHttpClientImpl.class);
60 private GithubAppSecurity appSecurity = mock(GithubAppSecurity.class);
61 private GithubAppConfiguration githubAppConfiguration = mock(GithubAppConfiguration.class);
62 private GithubApplicationClient underTest;
64 private String appUrl = "Any URL";
68 when(githubAppConfiguration.getApiEndpoint()).thenReturn(appUrl);
69 underTest = new GithubApplicationClientImpl(httpClient, appSecurity);
74 @UseDataProvider("invalidApiEndpoints")
75 public void checkApiEndpoint_Invalid(String url, String expectedMessage) {
76 GithubAppConfiguration configuration = new GithubAppConfiguration(1L, "", url);
78 assertThatThrownBy(() -> underTest.checkApiEndpoint(configuration))
79 .isInstanceOf(IllegalArgumentException.class)
80 .hasMessage(expectedMessage);
84 public static Object[][] invalidApiEndpoints() {
85 return new Object[][] {
87 {"ftp://api.github.com", "Only http and https schemes are supported"},
88 {"https://github.com", "Invalid GitHub URL"}
93 @UseDataProvider("validApiEndpoints")
94 public void checkApiEndpoint(String url) {
95 GithubAppConfiguration configuration = new GithubAppConfiguration(1L, "", url);
97 assertThatCode(() -> underTest.checkApiEndpoint(configuration)).isNull();
101 public static Object[][] validApiEndpoints() {
102 return new Object[][] {
103 {"https://github.sonarsource.com/api/v3"},
104 {"https://api.github.com"},
105 {"https://github.sonarsource.com/api/v3/"},
106 {"https://api.github.com/"},
107 {"HTTPS://api.github.com/"},
108 {"HTTP://api.github.com/"},
109 {"HtTpS://github.SonarSource.com/api/v3"},
110 {"HtTpS://github.sonarsource.com/api/V3"},
111 {"HtTpS://github.sonarsource.COM/ApI/v3"}
116 public void checkAppPermissions_IOException() throws IOException {
117 AppToken appToken = mockAppToken();
119 when(httpClient.get(appUrl, appToken, "/app")).thenThrow(new IOException("OOPS"));
121 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
122 .isInstanceOf(IllegalArgumentException.class)
123 .hasMessage("Failed to validate configuration, check URL and Private Key");
127 @UseDataProvider("checkAppPermissionsErrorCodes")
128 public void checkAppPermissions_ErrorCodes(int errorCode, String expectedMessage) throws IOException {
129 AppToken appToken = mockAppToken();
131 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new ErrorGetResponse(errorCode, null));
133 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
134 .isInstanceOf(IllegalArgumentException.class)
135 .hasMessage(expectedMessage);
139 public static Object[][] checkAppPermissionsErrorCodes() {
140 return new Object[][] {
141 {HTTP_UNAUTHORIZED, "Authentication failed, verify the Client Id, Client Secret and Private Key fields"},
142 {HTTP_FORBIDDEN, "Authentication failed, verify the Client Id, Client Secret and Private Key fields"},
143 {HTTP_NOT_FOUND, "Failed to check permissions with Github, check the configuration"}
148 public void checkAppPermissions_MissingPermissions() throws IOException {
149 AppToken appToken = mockAppToken();
151 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse("{}"));
153 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
154 .isInstanceOf(IllegalArgumentException.class)
155 .hasMessage("Failed to get app permissions, unexpected response body");
159 public void checkAppPermissions_IncorrectPermissions() throws IOException {
160 AppToken appToken = mockAppToken();
163 + " \"permissions\": {\n"
164 + " \"checks\": \"read\",\n"
165 + " \"metadata\": \"read\",\n"
166 + " \"statuses\": \"read\",\n"
167 + " \"pull_requests\": \"read\"\n"
171 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));
173 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
174 .isInstanceOf(IllegalArgumentException.class)
175 .hasMessage("Missing permissions; permission granted on pull_requests is 'read', should be 'write', checks is 'read', should be 'write'");
179 public void checkAppPermissions() throws IOException {
180 AppToken appToken = mockAppToken();
183 + " \"permissions\": {\n"
184 + " \"checks\": \"write\",\n"
185 + " \"metadata\": \"read\",\n"
186 + " \"statuses\": \"read\",\n"
187 + " \"pull_requests\": \"write\"\n"
191 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));
193 assertThatCode(() -> underTest.checkAppPermissions(githubAppConfiguration)).isNull();
197 @UseDataProvider("githubServers")
198 public void createUserAccessToken_returns_empty_if_access_token_cant_be_created(String apiUrl, String appUrl) throws IOException {
199 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
200 .thenReturn(new Response(400, null));
202 assertThatThrownBy(() -> underTest.createUserAccessToken(appUrl, "clientId", "clientSecret", "code"))
203 .isInstanceOf(IllegalStateException.class);
204 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
208 @UseDataProvider("githubServers")
209 public void createUserAccessToken_fail_if_access_token_request_fails(String apiUrl, String appUrl) throws IOException {
210 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
211 .thenThrow(new IOException("OOPS"));
213 assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
214 .isInstanceOf(IllegalStateException.class)
215 .hasMessage("Failed to create GitHub's user access token");
217 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
221 @UseDataProvider("githubServers")
222 public void createUserAccessToken_throws_illegal_argument_exception_if_access_token_code_is_expired(String apiUrl, String appUrl) throws IOException {
223 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
224 .thenReturn(new OkGetResponse("error_code=100&error=expired_or_invalid"));
226 assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
227 .isInstanceOf(IllegalArgumentException.class);
229 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
233 @UseDataProvider("githubServers")
234 public void createUserAccessToken_from_authorization_code_returns_access_token(String apiUrl, String appUrl) throws IOException {
235 String token = randomAlphanumeric(10);
236 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
237 .thenReturn(new OkGetResponse("access_token=" + token + "&status="));
239 UserAccessToken userAccessToken = underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code");
241 assertThat(userAccessToken)
242 .extracting(UserAccessToken::getValue, UserAccessToken::getAuthorizationHeaderPrefix)
243 .containsOnly(token, "token");
244 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
248 public static Object[][] githubServers() {
249 return new Object[][] {
250 {"https://github.sonarsource.com/api/v3", "https://github.sonarsource.com"},
251 {"https://api.github.com", "https://github.com"},
252 {"https://github.sonarsource.com/api/v3/", "https://github.sonarsource.com"},
253 {"https://api.github.com/", "https://github.com"},
258 public void listOrganizations_fail_on_failure() throws IOException {
259 String appUrl = "https://github.sonarsource.com";
260 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
262 when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
263 .thenThrow(new IOException("OOPS"));
265 assertThatThrownBy(() -> underTest.listOrganizations(appUrl, accessToken, 1, 100))
266 .isInstanceOf(IllegalStateException.class)
267 .hasMessage("Failed to list all organizations accessible by user access token on %s", appUrl);
271 public void listOrganizations_fail_if_pageIndex_out_of_bounds() {
272 UserAccessToken token = new UserAccessToken("token");
273 assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 0, 100))
274 .isInstanceOf(IllegalArgumentException.class)
275 .hasMessage("'page' must be larger than 0.");
279 public void listOrganizations_fail_if_pageSize_out_of_bounds() {
280 UserAccessToken token = new UserAccessToken("token");
281 assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 1, 0))
282 .isInstanceOf(IllegalArgumentException.class)
283 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
284 assertThatThrownBy(() -> underTest.listOrganizations("", token, 1, 101))
285 .isInstanceOf(IllegalArgumentException.class)
286 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
290 public void listOrganizations_returns_no_installations() throws IOException {
291 String appUrl = "https://github.sonarsource.com";
292 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
293 String responseJson = "{\n"
294 + " \"total_count\": 0\n"
297 when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
298 .thenReturn(new OkGetResponse(responseJson));
300 GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
302 assertThat(organizations.getTotal()).isZero();
303 assertThat(organizations.getOrganizations()).isNull();
307 public void listOrganizations_returns_pages_results() throws IOException {
308 String appUrl = "https://github.sonarsource.com";
309 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
310 String responseJson = "{\n"
311 + " \"total_count\": 2,\n"
312 + " \"installations\": [\n"
315 + " \"account\": {\n"
316 + " \"login\": \"github\",\n"
318 + " \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n"
319 + " \"url\": \"https://github.sonarsource.com/api/v3/orgs/github\",\n"
320 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/orgs/github/repos\",\n"
321 + " \"events_url\": \"https://github.sonarsource.com/api/v3/orgs/github/events\",\n"
322 + " \"hooks_url\": \"https://github.sonarsource.com/api/v3/orgs/github/hooks\",\n"
323 + " \"issues_url\": \"https://github.sonarsource.com/api/v3/orgs/github/issues\",\n"
324 + " \"members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/members{/member}\",\n"
325 + " \"public_members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/public_members{/member}\",\n"
326 + " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
327 + " \"description\": \"A great organization\"\n"
329 + " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
330 + " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
331 + " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
332 + " \"app_id\": 1,\n"
333 + " \"target_id\": 1,\n"
334 + " \"target_type\": \"Organization\",\n"
335 + " \"permissions\": {\n"
336 + " \"checks\": \"write\",\n"
337 + " \"metadata\": \"read\",\n"
338 + " \"contents\": \"read\"\n"
342 + " \"pull_request\"\n"
344 + " \"single_file_name\": \"config.yml\"\n"
348 + " \"account\": {\n"
349 + " \"login\": \"octocat\",\n"
351 + " \"node_id\": \"MDQ6VXNlcjE=\",\n"
352 + " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
353 + " \"gravatar_id\": \"\",\n"
354 + " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
355 + " \"html_url\": \"https://github.com/octocat\",\n"
356 + " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
357 + " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
358 + " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
359 + " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
360 + " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
361 + " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
362 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
363 + " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
364 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
365 + " \"type\": \"User\",\n"
366 + " \"site_admin\": false\n"
368 + " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
369 + " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
370 + " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
371 + " \"app_id\": 1,\n"
372 + " \"target_id\": 1,\n"
373 + " \"target_type\": \"Organization\",\n"
374 + " \"permissions\": {\n"
375 + " \"checks\": \"write\",\n"
376 + " \"metadata\": \"read\",\n"
377 + " \"contents\": \"read\"\n"
381 + " \"pull_request\"\n"
383 + " \"single_file_name\": \"config.yml\"\n"
388 when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
389 .thenReturn(new OkGetResponse(responseJson));
391 GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
393 assertThat(organizations.getTotal()).isEqualTo(2);
394 assertThat(organizations.getOrganizations()).extracting(GithubApplicationClient.Organization::getLogin).containsOnly("github", "octocat");
398 public void listRepositories_fail_on_failure() throws IOException {
399 String appUrl = "https://github.sonarsource.com";
400 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
402 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:test", 1, 100)))
403 .thenThrow(new IOException("OOPS"));
405 assertThatThrownBy(() -> underTest.listRepositories(appUrl, accessToken, "test", null, 1, 100))
406 .isInstanceOf(IllegalStateException.class)
407 .hasMessage("Failed to list all repositories of 'test' accessible by user access token on 'https://github.sonarsource.com' using query 'fork:true+org:test'");
411 public void listRepositories_fail_if_pageIndex_out_of_bounds() {
412 UserAccessToken token = new UserAccessToken("token");
413 assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 0, 100))
414 .isInstanceOf(IllegalArgumentException.class)
415 .hasMessage("'page' must be larger than 0.");
419 public void listRepositories_fail_if_pageSize_out_of_bounds() {
420 UserAccessToken token = new UserAccessToken("token");
421 assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 1, 0))
422 .isInstanceOf(IllegalArgumentException.class)
423 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
424 assertThatThrownBy(() -> underTest.listRepositories("", token, "test", null, 1, 101))
425 .isInstanceOf(IllegalArgumentException.class)
426 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
430 public void listRepositories_returns_empty_results() throws IOException {
431 String appUrl = "https://github.sonarsource.com";
432 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
433 String responseJson = "{\n"
434 + " \"total_count\": 0\n"
437 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
438 .thenReturn(new OkGetResponse(responseJson));
440 GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
442 assertThat(repositories.getTotal()).isZero();
443 assertThat(repositories.getRepositories()).isNull();
447 public void listRepositories_returns_pages_results() throws IOException {
448 String appUrl = "https://github.sonarsource.com";
449 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
450 String responseJson = "{\n"
451 + " \"total_count\": 2,\n"
452 + " \"incomplete_results\": false,\n"
455 + " \"id\": 3081286,\n"
456 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
457 + " \"name\": \"HelloWorld\",\n"
458 + " \"full_name\": \"github/HelloWorld\",\n"
460 + " \"login\": \"github\",\n"
461 + " \"id\": 872147,\n"
462 + " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
463 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
464 + " \"gravatar_id\": \"\",\n"
465 + " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
466 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
467 + " \"type\": \"User\"\n"
469 + " \"private\": false,\n"
470 + " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
471 + " \"description\": \"A C implementation of HelloWorld\",\n"
472 + " \"fork\": false,\n"
473 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
474 + " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
475 + " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
476 + " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
477 + " \"homepage\": \"\",\n"
478 + " \"size\": 524,\n"
479 + " \"stargazers_count\": 1,\n"
480 + " \"watchers_count\": 1,\n"
481 + " \"language\": \"Assembly\",\n"
482 + " \"forks_count\": 0,\n"
483 + " \"open_issues_count\": 0,\n"
484 + " \"master_branch\": \"master\",\n"
485 + " \"default_branch\": \"master\",\n"
486 + " \"score\": 1.0\n"
489 + " \"id\": 3081286,\n"
490 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
491 + " \"name\": \"HelloUniverse\",\n"
492 + " \"full_name\": \"github/HelloUniverse\",\n"
494 + " \"login\": \"github\",\n"
495 + " \"id\": 872147,\n"
496 + " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
497 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
498 + " \"gravatar_id\": \"\",\n"
499 + " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
500 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
501 + " \"type\": \"User\"\n"
503 + " \"private\": false,\n"
504 + " \"html_url\": \"https://github.com/github/HelloUniverse\",\n"
505 + " \"description\": \"A C implementation of HelloUniverse\",\n"
506 + " \"fork\": false,\n"
507 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloUniverse\",\n"
508 + " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
509 + " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
510 + " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
511 + " \"homepage\": \"\",\n"
512 + " \"size\": 524,\n"
513 + " \"stargazers_count\": 1,\n"
514 + " \"watchers_count\": 1,\n"
515 + " \"language\": \"Assembly\",\n"
516 + " \"forks_count\": 0,\n"
517 + " \"open_issues_count\": 0,\n"
518 + " \"master_branch\": \"master\",\n"
519 + " \"default_branch\": \"master\",\n"
520 + " \"score\": 1.0\n"
525 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
526 .thenReturn(new OkGetResponse(responseJson));
527 GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
529 assertThat(repositories.getTotal()).isEqualTo(2);
530 assertThat(repositories.getRepositories())
531 .extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName)
532 .containsOnly(tuple("HelloWorld", "github/HelloWorld"), tuple("HelloUniverse", "github/HelloUniverse"));
536 public void listRepositories_returns_search_results() throws IOException {
537 String appUrl = "https://github.sonarsource.com";
538 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
539 String responseJson = "{\n"
540 + " \"total_count\": 2,\n"
541 + " \"incomplete_results\": false,\n"
544 + " \"id\": 3081286,\n"
545 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
546 + " \"name\": \"HelloWorld\",\n"
547 + " \"full_name\": \"github/HelloWorld\",\n"
549 + " \"login\": \"github\",\n"
550 + " \"id\": 872147,\n"
551 + " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
552 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
553 + " \"gravatar_id\": \"\",\n"
554 + " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
555 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
556 + " \"type\": \"User\"\n"
558 + " \"private\": false,\n"
559 + " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
560 + " \"description\": \"A C implementation of HelloWorld\",\n"
561 + " \"fork\": false,\n"
562 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
563 + " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
564 + " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
565 + " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
566 + " \"homepage\": \"\",\n"
567 + " \"size\": 524,\n"
568 + " \"stargazers_count\": 1,\n"
569 + " \"watchers_count\": 1,\n"
570 + " \"language\": \"Assembly\",\n"
571 + " \"forks_count\": 0,\n"
572 + " \"open_issues_count\": 0,\n"
573 + " \"master_branch\": \"master\",\n"
574 + " \"default_branch\": \"master\",\n"
575 + " \"score\": 1.0\n"
580 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+fork:true+org:github", 1, 100)))
581 .thenReturn(new GithubApplicationHttpClient.GetResponse() {
583 public Optional<String> getNextEndPoint() {
584 return Optional.empty();
588 public int getCode() {
593 public Optional<String> getContent() {
594 return Optional.of(responseJson);
598 GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", "world", 1, 100);
600 assertThat(repositories.getTotal()).isEqualTo(2);
601 assertThat(repositories.getRepositories())
602 .extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName)
603 .containsOnly(tuple("HelloWorld", "github/HelloWorld"));
607 public void getRepository_returns_empty_when_repository_doesnt_exist() throws IOException {
608 when(httpClient.get(any(), any(), any()))
609 .thenReturn(new Response(404, null));
611 Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, new UserAccessToken("temp"), "octocat", "octocat/Hello-World");
613 assertThat(repository).isEmpty();
617 public void getRepository_fails_on_failure() throws IOException {
618 String repositoryKey = "octocat/Hello-World";
619 String organization = "octocat";
621 when(httpClient.get(any(), any(), any()))
622 .thenThrow(new IOException("OOPS"));
624 UserAccessToken token = new UserAccessToken("temp");
625 assertThatThrownBy(() -> underTest.getRepository(appUrl, token, organization, repositoryKey))
626 .isInstanceOf(IllegalStateException.class)
627 .hasMessage("Failed to get repository '%s' of '%s' accessible by user access token on '%s'", repositoryKey, organization, appUrl);
631 public void getRepository_returns_repository() throws IOException {
632 String appUrl = "https://github.sonarsource.com";
633 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
634 String responseJson = "{\n"
635 + " \"id\": 1296269,\n"
636 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n"
637 + " \"name\": \"Hello-World\",\n"
638 + " \"full_name\": \"octocat/Hello-World\",\n"
640 + " \"login\": \"octocat\",\n"
642 + " \"node_id\": \"MDQ6VXNlcjE=\",\n"
643 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
644 + " \"gravatar_id\": \"\",\n"
645 + " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
646 + " \"html_url\": \"https://github.com/octocat\",\n"
647 + " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
648 + " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
649 + " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
650 + " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
651 + " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
652 + " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
653 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
654 + " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
655 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
656 + " \"type\": \"User\",\n"
657 + " \"site_admin\": false\n"
659 + " \"private\": false,\n"
660 + " \"html_url\": \"https://github.com/octocat/Hello-World\",\n"
661 + " \"description\": \"This your first repo!\",\n"
662 + " \"fork\": false,\n"
663 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World\",\n"
664 + " \"archive_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/{archive_format}{/ref}\",\n"
665 + " \"assignees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/assignees{/user}\",\n"
666 + " \"blobs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/blobs{/sha}\",\n"
667 + " \"branches_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/branches{/branch}\",\n"
668 + " \"collaborators_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/collaborators{/collaborator}\",\n"
669 + " \"comments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/comments{/number}\",\n"
670 + " \"commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/commits{/sha}\",\n"
671 + " \"compare_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/compare/{base}...{head}\",\n"
672 + " \"contents_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contents/{+path}\",\n"
673 + " \"contributors_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contributors\",\n"
674 + " \"deployments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/deployments\",\n"
675 + " \"downloads_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/downloads\",\n"
676 + " \"events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/events\",\n"
677 + " \"forks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/forks\",\n"
678 + " \"git_commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/commits{/sha}\",\n"
679 + " \"git_refs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/refs{/sha}\",\n"
680 + " \"git_tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/tags{/sha}\",\n"
681 + " \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n"
682 + " \"issue_comment_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/comments{/number}\",\n"
683 + " \"issue_events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/events{/number}\",\n"
684 + " \"issues_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues{/number}\",\n"
685 + " \"keys_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/keys{/key_id}\",\n"
686 + " \"labels_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/labels{/name}\",\n"
687 + " \"languages_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/languages\",\n"
688 + " \"merges_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/merges\",\n"
689 + " \"milestones_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/milestones{/number}\",\n"
690 + " \"notifications_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n"
691 + " \"pulls_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/pulls{/number}\",\n"
692 + " \"releases_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/releases{/id}\",\n"
693 + " \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n"
694 + " \"stargazers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/stargazers\",\n"
695 + " \"statuses_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/statuses/{sha}\",\n"
696 + " \"subscribers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscribers\",\n"
697 + " \"subscription_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscription\",\n"
698 + " \"tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/tags\",\n"
699 + " \"teams_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/teams\",\n"
700 + " \"trees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/trees{/sha}\",\n"
701 + " \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n"
702 + " \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n"
703 + " \"hooks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/hooks\",\n"
704 + " \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n"
705 + " \"homepage\": \"https://github.com\",\n"
706 + " \"language\": null,\n"
707 + " \"forks_count\": 9,\n"
708 + " \"stargazers_count\": 80,\n"
709 + " \"watchers_count\": 80,\n"
710 + " \"size\": 108,\n"
711 + " \"default_branch\": \"master\",\n"
712 + " \"open_issues_count\": 0,\n"
713 + " \"is_template\": true,\n"
720 + " \"has_issues\": true,\n"
721 + " \"has_projects\": true,\n"
722 + " \"has_wiki\": true,\n"
723 + " \"has_pages\": false,\n"
724 + " \"has_downloads\": true,\n"
725 + " \"archived\": false,\n"
726 + " \"disabled\": false,\n"
727 + " \"visibility\": \"public\",\n"
728 + " \"pushed_at\": \"2011-01-26T19:06:43Z\",\n"
729 + " \"created_at\": \"2011-01-26T19:01:12Z\",\n"
730 + " \"updated_at\": \"2011-01-26T19:14:43Z\",\n"
731 + " \"permissions\": {\n"
732 + " \"admin\": false,\n"
733 + " \"push\": false,\n"
734 + " \"pull\": true\n"
736 + " \"allow_rebase_merge\": true,\n"
737 + " \"template_repository\": null,\n"
738 + " \"allow_squash_merge\": true,\n"
739 + " \"allow_merge_commit\": true,\n"
740 + " \"subscribers_count\": 42,\n"
741 + " \"network_count\": 0,\n"
742 + " \"anonymous_access_enabled\": false,\n"
743 + " \"license\": {\n"
744 + " \"key\": \"mit\",\n"
745 + " \"name\": \"MIT License\",\n"
746 + " \"spdx_id\": \"MIT\",\n"
747 + " \"url\": \"https://github.sonarsource.com/api/v3/licenses/mit\",\n"
748 + " \"node_id\": \"MDc6TGljZW5zZW1pdA==\"\n"
750 + " \"organization\": {\n"
751 + " \"login\": \"octocat\",\n"
753 + " \"node_id\": \"MDQ6VXNlcjE=\",\n"
754 + " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
755 + " \"gravatar_id\": \"\",\n"
756 + " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
757 + " \"html_url\": \"https://github.com/octocat\",\n"
758 + " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
759 + " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
760 + " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
761 + " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
762 + " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
763 + " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
764 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
765 + " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
766 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
767 + " \"type\": \"Organization\",\n"
768 + " \"site_admin\": false\n"
772 when(httpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World"))
773 .thenReturn(new GithubApplicationHttpClient.GetResponse() {
775 public Optional<String> getNextEndPoint() {
776 return Optional.empty();
780 public int getCode() {
785 public Optional<String> getContent() {
786 return Optional.of(responseJson);
790 Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, accessToken, "octocat", "octocat/Hello-World");
792 assertThat(repository)
795 .extracting(GithubApplicationClient.Repository::getId, GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName,
796 GithubApplicationClient.Repository::getUrl, GithubApplicationClient.Repository::isPrivate, GithubApplicationClient.Repository::getDefaultBranch)
797 .containsOnly(1296269L, "Hello-World", "octocat/Hello-World", "https://github.com/octocat/Hello-World", false, "master");
800 private AppToken mockAppToken() {
801 String jwt = randomAlphanumeric(5);
802 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(new AppToken(jwt));
803 return new AppToken(jwt);
806 private static class OkGetResponse extends Response {
807 private OkGetResponse(String content) {
812 private static class ErrorGetResponse extends Response {
817 ErrorGetResponse(int code, String content) {
818 super(code, content);
822 private static class Response implements GithubApplicationHttpClient.GetResponse {
823 private final int code;
824 private final String content;
825 private final String nextEndPoint;
827 private Response(int code, @Nullable String content) {
828 this(code, content, null);
831 private Response(int code, @Nullable String content, @Nullable String nextEndPoint) {
833 this.content = content;
834 this.nextEndPoint = nextEndPoint;
838 public int getCode() {
843 public Optional<String> getContent() {
844 return Optional.ofNullable(content);
848 public Optional<String> getNextEndPoint() {
849 return Optional.ofNullable(nextEndPoint);