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.nio.charset.StandardCharsets;
27 import java.util.List;
28 import java.util.Optional;
30 import java.util.function.Function;
31 import javax.annotation.Nullable;
32 import org.apache.commons.io.IOUtils;
33 import org.junit.Before;
34 import org.junit.ClassRule;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.mockito.ArgumentCaptor;
38 import org.slf4j.event.Level;
39 import org.sonar.alm.client.github.GithubApplicationHttpClient.RateLimit;
40 import org.sonar.alm.client.github.api.GsonRepositoryCollaborator;
41 import org.sonar.alm.client.github.api.GsonRepositoryTeam;
42 import org.sonar.alm.client.github.config.GithubAppConfiguration;
43 import org.sonar.alm.client.github.config.GithubAppInstallation;
44 import org.sonar.alm.client.github.security.AccessToken;
45 import org.sonar.alm.client.github.security.AppToken;
46 import org.sonar.alm.client.github.security.GithubAppSecurity;
47 import org.sonar.alm.client.github.security.UserAccessToken;
48 import org.sonar.api.testfixtures.log.LogAndArguments;
49 import org.sonar.api.testfixtures.log.LogTester;
50 import org.sonar.api.utils.log.LoggerLevel;
51 import org.sonar.auth.github.GitHubSettings;
52 import org.sonar.auth.github.GsonRepositoryPermissions;
53 import org.sonarqube.ws.client.HttpException;
55 import static java.net.HttpURLConnection.HTTP_CREATED;
56 import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
57 import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
58 import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
59 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
60 import static org.assertj.core.api.Assertions.assertThat;
61 import static org.assertj.core.api.Assertions.assertThatCode;
62 import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
63 import static org.assertj.core.api.Assertions.assertThatThrownBy;
64 import static org.assertj.core.groups.Tuple.tuple;
65 import static org.mockito.ArgumentMatchers.any;
66 import static org.mockito.ArgumentMatchers.anyString;
67 import static org.mockito.ArgumentMatchers.eq;
68 import static org.mockito.Mockito.mock;
69 import static org.mockito.Mockito.verify;
70 import static org.mockito.Mockito.when;
71 import static org.sonar.alm.client.github.GithubApplicationHttpClient.GetResponse;
73 @RunWith(DataProviderRunner.class)
74 public class GithubApplicationClientImplTest {
75 private static final String ORG_NAME = "ORG_NAME";
76 private static final String TEAM_NAME = "team1";
77 private static final String REPO_NAME = "repo1";
78 private static final String APP_URL = "https://github.com/";
79 private static final String REPO_TEAMS_ENDPOINT = "/repos/ORG_NAME/repo1/teams";
80 private static final String REPO_COLLABORATORS_ENDPOINT = "/repos/ORG_NAME/repo1/collaborators?affiliation=direct";
81 private static final int INSTALLATION_ID = 1;
82 private static final String APP_JWT_TOKEN = "APP_TOKEN_JWT";
83 private static final String PAYLOAD_2_ORGS = """
89 "type": "Organization"
91 "target_type": "Organization",
96 "suspended_at": "2023-05-30T08:40:55Z"
102 "type": "Organization"
104 "target_type": "Organization",
112 private static final RateLimit RATE_LIMIT = new RateLimit(Integer.MAX_VALUE, Integer.MAX_VALUE, 0L);
115 public static LogTester logTester = new LogTester().setLevel(LoggerLevel.WARN);
117 private GithubApplicationHttpClientImpl httpClient = mock();
118 private GithubAppSecurity appSecurity = mock();
119 private GithubAppConfiguration githubAppConfiguration = mock();
120 private GitHubSettings gitHubSettings = mock();
122 private GithubPaginatedHttpClient githubPaginatedHttpClient = mock();
123 private AppInstallationToken appInstallationToken = mock();
124 private GithubApplicationClient underTest;
127 private String appUrl = "Any URL";
130 public void setup() {
131 when(githubAppConfiguration.getApiEndpoint()).thenReturn(appUrl);
132 underTest = new GithubApplicationClientImpl(httpClient, appSecurity, gitHubSettings, githubPaginatedHttpClient);
137 @UseDataProvider("invalidApiEndpoints")
138 public void checkApiEndpoint_Invalid(String url, String expectedMessage) {
139 GithubAppConfiguration configuration = new GithubAppConfiguration(1L, "", url);
141 assertThatThrownBy(() -> underTest.checkApiEndpoint(configuration))
142 .isInstanceOf(IllegalArgumentException.class)
143 .hasMessage(expectedMessage);
147 public static Object[][] invalidApiEndpoints() {
148 return new Object[][] {
150 {"ftp://api.github.com", "Only http and https schemes are supported"},
151 {"https://github.com", "Invalid GitHub URL"}
156 @UseDataProvider("validApiEndpoints")
157 public void checkApiEndpoint(String url) {
158 GithubAppConfiguration configuration = new GithubAppConfiguration(1L, "", url);
160 assertThatCode(() -> underTest.checkApiEndpoint(configuration)).isNull();
164 public static Object[][] validApiEndpoints() {
165 return new Object[][] {
166 {"https://github.sonarsource.com/api/v3"},
167 {"https://api.github.com"},
168 {"https://github.sonarsource.com/api/v3/"},
169 {"https://api.github.com/"},
170 {"HTTPS://api.github.com/"},
171 {"HTTP://api.github.com/"},
172 {"HtTpS://github.SonarSource.com/api/v3"},
173 {"HtTpS://github.sonarsource.com/api/V3"},
174 {"HtTpS://github.sonarsource.COM/ApI/v3"}
179 public void checkAppPermissions_IOException() throws IOException {
180 AppToken appToken = mockAppToken();
182 when(httpClient.get(appUrl, appToken, "/app")).thenThrow(new IOException("OOPS"));
184 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
185 .isInstanceOf(IllegalArgumentException.class)
186 .hasMessage("Failed to validate configuration, check URL and Private Key");
190 @UseDataProvider("checkAppPermissionsErrorCodes")
191 public void checkAppPermissions_ErrorCodes(int errorCode, String expectedMessage) throws IOException {
192 AppToken appToken = mockAppToken();
194 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new ErrorGetResponse(errorCode, null));
196 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
197 .isInstanceOf(IllegalArgumentException.class)
198 .hasMessage(expectedMessage);
202 public static Object[][] checkAppPermissionsErrorCodes() {
203 return new Object[][] {
204 {HTTP_UNAUTHORIZED, "Authentication failed, verify the Client Id, Client Secret and Private Key fields"},
205 {HTTP_FORBIDDEN, "Authentication failed, verify the Client Id, Client Secret and Private Key fields"},
206 {HTTP_NOT_FOUND, "Failed to check permissions with Github, check the configuration"}
211 public void checkAppPermissions_MissingPermissions() throws IOException {
212 AppToken appToken = mockAppToken();
214 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse("{}"));
216 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
217 .isInstanceOf(IllegalArgumentException.class)
218 .hasMessage("Failed to get app permissions, unexpected response body");
222 public void checkAppPermissions_IncorrectPermissions() throws IOException {
223 AppToken appToken = mockAppToken();
226 + " \"permissions\": {\n"
227 + " \"checks\": \"read\",\n"
228 + " \"metadata\": \"read\",\n"
229 + " \"pull_requests\": \"read\"\n"
233 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));
235 assertThatThrownBy(() -> underTest.checkAppPermissions(githubAppConfiguration))
236 .isInstanceOf(IllegalArgumentException.class)
237 .hasMessage("Missing permissions; permission granted on pull_requests is 'read', should be 'write', checks is 'read', should be 'write'");
241 public void checkAppPermissions() throws IOException {
242 AppToken appToken = mockAppToken();
245 + " \"permissions\": {\n"
246 + " \"checks\": \"write\",\n"
247 + " \"metadata\": \"read\",\n"
248 + " \"pull_requests\": \"write\"\n"
252 when(httpClient.get(appUrl, appToken, "/app")).thenReturn(new OkGetResponse(json));
254 assertThatCode(() -> underTest.checkAppPermissions(githubAppConfiguration)).isNull();
258 public void getInstallationId_returns_installation_id_of_given_account() throws IOException {
259 AppToken appToken = new AppToken(APP_JWT_TOKEN);
260 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
261 when(httpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
262 .thenReturn(new OkGetResponse("{" +
265 " \"login\": \"torvalds\"" +
269 assertThat(underTest.getInstallationId(githubAppConfiguration, "torvalds/linux")).hasValue(2L);
273 public void getInstallationId_throws_IAE_if_fail_to_create_app_token() {
274 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenThrow(IllegalArgumentException.class);
276 assertThatThrownBy(() -> underTest.getInstallationId(githubAppConfiguration, "torvalds"))
277 .isInstanceOf(IllegalArgumentException.class);
281 public void getInstallationId_return_empty_if_no_installation_found_for_githubAccount() throws IOException {
282 AppToken appToken = new AppToken(APP_JWT_TOKEN);
283 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
284 when(httpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
285 .thenReturn(new ErrorGetResponse(404, null));
287 assertThat(underTest.getInstallationId(githubAppConfiguration, "torvalds")).isEmpty();
291 @UseDataProvider("githubServers")
292 public void createUserAccessToken_returns_empty_if_access_token_cant_be_created(String apiUrl, String appUrl) throws IOException {
293 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
294 .thenReturn(new Response(400, null));
296 assertThatThrownBy(() -> underTest.createUserAccessToken(appUrl, "clientId", "clientSecret", "code"))
297 .isInstanceOf(IllegalStateException.class);
298 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
302 @UseDataProvider("githubServers")
303 public void createUserAccessToken_fail_if_access_token_request_fails(String apiUrl, String appUrl) throws IOException {
304 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
305 .thenThrow(new IOException("OOPS"));
307 assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
308 .isInstanceOf(IllegalStateException.class)
309 .hasMessage("Failed to create GitHub's user access token");
311 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
315 @UseDataProvider("githubServers")
316 public void createUserAccessToken_throws_illegal_argument_exception_if_access_token_code_is_expired(String apiUrl, String appUrl) throws IOException {
317 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
318 .thenReturn(new OkGetResponse("error_code=100&error=expired_or_invalid"));
320 assertThatThrownBy(() -> underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code"))
321 .isInstanceOf(IllegalArgumentException.class);
323 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
327 @UseDataProvider("githubServers")
328 public void createUserAccessToken_from_authorization_code_returns_access_token(String apiUrl, String appUrl) throws IOException {
329 String token = randomAlphanumeric(10);
330 when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
331 .thenReturn(new OkGetResponse("access_token=" + token + "&status="));
333 UserAccessToken userAccessToken = underTest.createUserAccessToken(apiUrl, "clientId", "clientSecret", "code");
335 assertThat(userAccessToken)
336 .extracting(UserAccessToken::getValue, UserAccessToken::getAuthorizationHeaderPrefix)
337 .containsOnly(token, "token");
338 verify(httpClient).post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code");
342 public void getApp_returns_id() throws IOException {
343 AppToken appToken = new AppToken(APP_JWT_TOKEN);
344 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
345 when(httpClient.get(appUrl, appToken, "/app"))
346 .thenReturn(new OkGetResponse("{\"installations_count\": 2}"));
348 assertThat(underTest.getApp(githubAppConfiguration).getInstallationsCount()).isEqualTo(2L);
352 public void getApp_whenStatusCodeIsNotOk_shouldThrowHttpException() throws IOException {
353 AppToken appToken = new AppToken(APP_JWT_TOKEN);
354 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
355 when(httpClient.get(appUrl, appToken, "/app"))
356 .thenReturn(new ErrorGetResponse(418, "I'm a teapot"));
358 assertThatThrownBy(() -> underTest.getApp(githubAppConfiguration))
359 .isInstanceOfSatisfying(HttpException.class, httpException -> {
360 assertThat(httpException.code()).isEqualTo(418);
361 assertThat(httpException.url()).isEqualTo("Any URL/app");
362 assertThat(httpException.content()).isEqualTo("I'm a teapot");
367 public static Object[][] githubServers() {
368 return new Object[][] {
369 {"https://github.sonarsource.com/api/v3", "https://github.sonarsource.com"},
370 {"https://api.github.com", "https://github.com"},
371 {"https://github.sonarsource.com/api/v3/", "https://github.sonarsource.com"},
372 {"https://api.github.com/", "https://github.com"},
377 public void listOrganizations_fail_on_failure() throws IOException {
378 String appUrl = "https://github.sonarsource.com";
379 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
381 when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
382 .thenThrow(new IOException("OOPS"));
384 assertThatThrownBy(() -> underTest.listOrganizations(appUrl, accessToken, 1, 100))
385 .isInstanceOf(IllegalStateException.class)
386 .hasMessage("Failed to list all organizations accessible by user access token on %s", appUrl);
390 public void listOrganizations_fail_if_pageIndex_out_of_bounds() {
391 UserAccessToken token = new UserAccessToken("token");
392 assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 0, 100))
393 .isInstanceOf(IllegalArgumentException.class)
394 .hasMessage("'page' must be larger than 0.");
398 public void listOrganizations_fail_if_pageSize_out_of_bounds() {
399 UserAccessToken token = new UserAccessToken("token");
400 assertThatThrownBy(() -> underTest.listOrganizations(appUrl, token, 1, 0))
401 .isInstanceOf(IllegalArgumentException.class)
402 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
403 assertThatThrownBy(() -> underTest.listOrganizations("", token, 1, 101))
404 .isInstanceOf(IllegalArgumentException.class)
405 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
409 public void listOrganizations_returns_no_installations() throws IOException {
410 String appUrl = "https://github.sonarsource.com";
411 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
412 String responseJson = "{\n"
413 + " \"total_count\": 0\n"
416 when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
417 .thenReturn(new OkGetResponse(responseJson));
419 GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
421 assertThat(organizations.getTotal()).isZero();
422 assertThat(organizations.getOrganizations()).isNull();
426 public void listOrganizations_returns_pages_results() throws IOException {
427 String appUrl = "https://github.sonarsource.com";
428 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
429 String responseJson = "{\n"
430 + " \"total_count\": 2,\n"
431 + " \"installations\": [\n"
434 + " \"account\": {\n"
435 + " \"login\": \"github\",\n"
437 + " \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjE=\",\n"
438 + " \"url\": \"https://github.sonarsource.com/api/v3/orgs/github\",\n"
439 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/orgs/github/repos\",\n"
440 + " \"events_url\": \"https://github.sonarsource.com/api/v3/orgs/github/events\",\n"
441 + " \"hooks_url\": \"https://github.sonarsource.com/api/v3/orgs/github/hooks\",\n"
442 + " \"issues_url\": \"https://github.sonarsource.com/api/v3/orgs/github/issues\",\n"
443 + " \"members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/members{/member}\",\n"
444 + " \"public_members_url\": \"https://github.sonarsource.com/api/v3/orgs/github/public_members{/member}\",\n"
445 + " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
446 + " \"description\": \"A great organization\"\n"
448 + " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
449 + " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
450 + " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
451 + " \"app_id\": 1,\n"
452 + " \"target_id\": 1,\n"
453 + " \"target_type\": \"Organization\",\n"
454 + " \"permissions\": {\n"
455 + " \"checks\": \"write\",\n"
456 + " \"metadata\": \"read\",\n"
457 + " \"contents\": \"read\"\n"
461 + " \"pull_request\"\n"
463 + " \"single_file_name\": \"config.yml\"\n"
467 + " \"account\": {\n"
468 + " \"login\": \"octocat\",\n"
470 + " \"node_id\": \"MDQ6VXNlcjE=\",\n"
471 + " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
472 + " \"gravatar_id\": \"\",\n"
473 + " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
474 + " \"html_url\": \"https://github.com/octocat\",\n"
475 + " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
476 + " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
477 + " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
478 + " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
479 + " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
480 + " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
481 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
482 + " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
483 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
484 + " \"type\": \"User\",\n"
485 + " \"site_admin\": false\n"
487 + " \"access_tokens_url\": \"https://github.sonarsource.com/api/v3/app/installations/1/access_tokens\",\n"
488 + " \"repositories_url\": \"https://github.sonarsource.com/api/v3/installation/repositories\",\n"
489 + " \"html_url\": \"https://github.com/organizations/github/settings/installations/1\",\n"
490 + " \"app_id\": 1,\n"
491 + " \"target_id\": 1,\n"
492 + " \"target_type\": \"Organization\",\n"
493 + " \"permissions\": {\n"
494 + " \"checks\": \"write\",\n"
495 + " \"metadata\": \"read\",\n"
496 + " \"contents\": \"read\"\n"
500 + " \"pull_request\"\n"
502 + " \"single_file_name\": \"config.yml\"\n"
507 when(httpClient.get(appUrl, accessToken, String.format("/user/installations?page=%s&per_page=%s", 1, 100)))
508 .thenReturn(new OkGetResponse(responseJson));
510 GithubApplicationClient.Organizations organizations = underTest.listOrganizations(appUrl, accessToken, 1, 100);
512 assertThat(organizations.getTotal()).isEqualTo(2);
513 assertThat(organizations.getOrganizations()).extracting(GithubApplicationClient.Organization::getLogin).containsOnly("github", "octocat");
517 public void getWhitelistedGithubAppInstallations_whenWhitelistNotSpecified_doesNotFilter() throws IOException {
518 List<GithubAppInstallation> allOrgInstallations = getGithubAppInstallationsFromGithubResponse(PAYLOAD_2_ORGS);
519 assertOrgDeserialization(allOrgInstallations);
522 private static void assertOrgDeserialization(List<GithubAppInstallation> orgs) {
523 GithubAppInstallation org1 = orgs.get(0);
524 assertThat(org1.installationId()).isEqualTo("1");
525 assertThat(org1.organizationName()).isEqualTo("org1");
526 assertThat(org1.permissions().getMembers()).isEqualTo("read");
527 assertThat(org1.isSuspended()).isTrue();
529 GithubAppInstallation org2 = orgs.get(1);
530 assertThat(org2.installationId()).isEqualTo("2");
531 assertThat(org2.organizationName()).isEqualTo("org2");
532 assertThat(org2.permissions().getMembers()).isEqualTo("read");
533 assertThat(org2.isSuspended()).isFalse();
537 public void getWhitelistedGithubAppInstallations_whenWhitelistSpecified_filtersWhitelistedOrgs() throws IOException {
538 when(gitHubSettings.getOrganizations()).thenReturn(Set.of("org2"));
539 List<GithubAppInstallation> orgInstallations = getGithubAppInstallationsFromGithubResponse(PAYLOAD_2_ORGS);
540 assertThat(orgInstallations)
542 .extracting(GithubAppInstallation::organizationName)
543 .containsExactlyInAnyOrder("org2");
547 public void getWhitelistedGithubAppInstallations_whenEmptyResponse_shouldReturnEmpty() throws IOException {
548 List<GithubAppInstallation> allOrgInstallations = getGithubAppInstallationsFromGithubResponse("[]");
549 assertThat(allOrgInstallations).isEmpty();
553 public void getWhitelistedGithubAppInstallations_whenNoOrganization_shouldReturnEmpty() throws IOException {
554 List<GithubAppInstallation> allOrgInstallations = getGithubAppInstallationsFromGithubResponse("""
562 "target_type": "User",
568 assertThat(allOrgInstallations).isEmpty();
571 @SuppressWarnings("unchecked")
572 private List<GithubAppInstallation> getGithubAppInstallationsFromGithubResponse(String content) throws IOException {
573 AppToken appToken = new AppToken(APP_JWT_TOKEN);
574 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
575 when(githubPaginatedHttpClient.get(eq(appUrl), eq(appToken), eq("/app/installations"), any()))
576 .thenAnswer(invocation -> {
577 Function<String, List<GithubBinding.GsonInstallation>> deserializingFunction = invocation.getArgument(3, Function.class);
578 return deserializingFunction.apply(content);
580 return underTest.getWhitelistedGithubAppInstallations(githubAppConfiguration);
584 public void getWhitelistedGithubAppInstallations_whenGithubReturnsError_shouldThrow() throws IOException {
585 AppToken appToken = new AppToken(APP_JWT_TOKEN);
586 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
587 when(githubPaginatedHttpClient.get(any(), any(), any(), any())).thenThrow(new IOException("io exception"));
589 assertThatThrownBy(() -> underTest.getWhitelistedGithubAppInstallations(githubAppConfiguration))
590 .isInstanceOf(IllegalStateException.class)
592 "SonarQube was not able to retrieve resources from GitHub. "
593 + "This is likely due to a connectivity problem or a temporary network outage: "
594 + "Error while executing a paginated call to GitHub - appUrl: Any URL, path: /app/installations. io exception"
599 public void listRepositories_fail_on_failure() throws IOException {
600 String appUrl = "https://github.sonarsource.com";
601 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
603 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "org:test", 1, 100)))
604 .thenThrow(new IOException("OOPS"));
606 assertThatThrownBy(() -> underTest.listRepositories(appUrl, accessToken, "test", null, 1, 100))
607 .isInstanceOf(IllegalStateException.class)
608 .hasMessage("Failed to list all repositories of 'test' accessible by user access token on 'https://github.sonarsource.com' using query 'fork:true+org:test'");
612 public void listRepositories_fail_if_pageIndex_out_of_bounds() {
613 UserAccessToken token = new UserAccessToken("token");
614 assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 0, 100))
615 .isInstanceOf(IllegalArgumentException.class)
616 .hasMessage("'page' must be larger than 0.");
620 public void listRepositories_fail_if_pageSize_out_of_bounds() {
621 UserAccessToken token = new UserAccessToken("token");
622 assertThatThrownBy(() -> underTest.listRepositories(appUrl, token, "test", null, 1, 0))
623 .isInstanceOf(IllegalArgumentException.class)
624 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
625 assertThatThrownBy(() -> underTest.listRepositories("", token, "test", null, 1, 101))
626 .isInstanceOf(IllegalArgumentException.class)
627 .hasMessage("'pageSize' must be a value larger than 0 and smaller or equal to 100.");
631 public void listRepositories_returns_empty_results() throws IOException {
632 String appUrl = "https://github.sonarsource.com";
633 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
634 String responseJson = "{\n"
635 + " \"total_count\": 0\n"
638 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
639 .thenReturn(new OkGetResponse(responseJson));
641 GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
643 assertThat(repositories.getTotal()).isZero();
644 assertThat(repositories.getRepositories()).isNull();
648 public void listRepositories_returns_pages_results() throws IOException {
649 String appUrl = "https://github.sonarsource.com";
650 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
651 String responseJson = "{\n"
652 + " \"total_count\": 2,\n"
653 + " \"incomplete_results\": false,\n"
656 + " \"id\": 3081286,\n"
657 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
658 + " \"name\": \"HelloWorld\",\n"
659 + " \"full_name\": \"github/HelloWorld\",\n"
661 + " \"login\": \"github\",\n"
662 + " \"id\": 872147,\n"
663 + " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
664 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
665 + " \"gravatar_id\": \"\",\n"
666 + " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
667 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
668 + " \"type\": \"User\"\n"
670 + " \"private\": false,\n"
671 + " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
672 + " \"description\": \"A C implementation of HelloWorld\",\n"
673 + " \"fork\": false,\n"
674 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
675 + " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
676 + " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
677 + " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
678 + " \"homepage\": \"\",\n"
679 + " \"size\": 524,\n"
680 + " \"stargazers_count\": 1,\n"
681 + " \"watchers_count\": 1,\n"
682 + " \"language\": \"Assembly\",\n"
683 + " \"forks_count\": 0,\n"
684 + " \"open_issues_count\": 0,\n"
685 + " \"master_branch\": \"master\",\n"
686 + " \"default_branch\": \"master\",\n"
687 + " \"score\": 1.0\n"
690 + " \"id\": 3081286,\n"
691 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
692 + " \"name\": \"HelloUniverse\",\n"
693 + " \"full_name\": \"github/HelloUniverse\",\n"
695 + " \"login\": \"github\",\n"
696 + " \"id\": 872147,\n"
697 + " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
698 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
699 + " \"gravatar_id\": \"\",\n"
700 + " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
701 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
702 + " \"type\": \"User\"\n"
704 + " \"private\": false,\n"
705 + " \"html_url\": \"https://github.com/github/HelloUniverse\",\n"
706 + " \"description\": \"A C implementation of HelloUniverse\",\n"
707 + " \"fork\": false,\n"
708 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloUniverse\",\n"
709 + " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
710 + " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
711 + " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
712 + " \"homepage\": \"\",\n"
713 + " \"size\": 524,\n"
714 + " \"stargazers_count\": 1,\n"
715 + " \"watchers_count\": 1,\n"
716 + " \"language\": \"Assembly\",\n"
717 + " \"forks_count\": 0,\n"
718 + " \"open_issues_count\": 0,\n"
719 + " \"master_branch\": \"master\",\n"
720 + " \"default_branch\": \"master\",\n"
721 + " \"score\": 1.0\n"
726 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "fork:true+org:github", 1, 100)))
727 .thenReturn(new OkGetResponse(responseJson));
728 GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", null, 1, 100);
730 assertThat(repositories.getTotal()).isEqualTo(2);
731 assertThat(repositories.getRepositories())
732 .extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName)
733 .containsOnly(tuple("HelloWorld", "github/HelloWorld"), tuple("HelloUniverse", "github/HelloUniverse"));
737 public void listRepositories_returns_search_results() throws IOException {
738 String appUrl = "https://github.sonarsource.com";
739 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
740 String responseJson = "{\n"
741 + " \"total_count\": 2,\n"
742 + " \"incomplete_results\": false,\n"
745 + " \"id\": 3081286,\n"
746 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkzMDgxMjg2\",\n"
747 + " \"name\": \"HelloWorld\",\n"
748 + " \"full_name\": \"github/HelloWorld\",\n"
750 + " \"login\": \"github\",\n"
751 + " \"id\": 872147,\n"
752 + " \"node_id\": \"MDQ6VXNlcjg3MjE0Nw==\",\n"
753 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
754 + " \"gravatar_id\": \"\",\n"
755 + " \"url\": \"https://github.sonarsource.com/api/v3/users/github\",\n"
756 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/github/received_events\",\n"
757 + " \"type\": \"User\"\n"
759 + " \"private\": false,\n"
760 + " \"html_url\": \"https://github.com/github/HelloWorld\",\n"
761 + " \"description\": \"A C implementation of HelloWorld\",\n"
762 + " \"fork\": false,\n"
763 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/github/HelloWorld\",\n"
764 + " \"created_at\": \"2012-01-01T00:31:50Z\",\n"
765 + " \"updated_at\": \"2013-01-05T17:58:47Z\",\n"
766 + " \"pushed_at\": \"2012-01-01T00:37:02Z\",\n"
767 + " \"homepage\": \"\",\n"
768 + " \"size\": 524,\n"
769 + " \"stargazers_count\": 1,\n"
770 + " \"watchers_count\": 1,\n"
771 + " \"language\": \"Assembly\",\n"
772 + " \"forks_count\": 0,\n"
773 + " \"open_issues_count\": 0,\n"
774 + " \"master_branch\": \"master\",\n"
775 + " \"default_branch\": \"master\",\n"
776 + " \"score\": 1.0\n"
781 when(httpClient.get(appUrl, accessToken, String.format("/search/repositories?q=%s&page=%s&per_page=%s", "world+fork:true+org:github", 1, 100)))
782 .thenReturn(new GetResponse() {
784 public Optional<String> getNextEndPoint() {
785 return Optional.empty();
789 public int getCode() {
794 public Optional<String> getContent() {
795 return Optional.of(responseJson);
799 public RateLimit getRateLimit() {
804 GithubApplicationClient.Repositories repositories = underTest.listRepositories(appUrl, accessToken, "github", "world", 1, 100);
806 assertThat(repositories.getTotal()).isEqualTo(2);
807 assertThat(repositories.getRepositories())
808 .extracting(GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName)
809 .containsOnly(tuple("HelloWorld", "github/HelloWorld"));
813 public void getRepository_returns_empty_when_repository_doesnt_exist() throws IOException {
814 when(httpClient.get(any(), any(), any()))
815 .thenReturn(new Response(404, null));
817 Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, new UserAccessToken("temp"), "octocat/Hello-World");
819 assertThat(repository).isEmpty();
823 public void getRepository_fails_on_failure() throws IOException {
824 String repositoryKey = "octocat/Hello-World";
826 when(httpClient.get(any(), any(), any()))
827 .thenThrow(new IOException("OOPS"));
829 UserAccessToken token = new UserAccessToken("temp");
830 assertThatThrownBy(() -> underTest.getRepository(appUrl, token, repositoryKey))
831 .isInstanceOf(IllegalStateException.class)
832 .hasMessage("Failed to get repository 'octocat/Hello-World' on 'Any URL' (this might be related to the GitHub App installation scope)");
836 public void getRepository_returns_repository() throws IOException {
837 String appUrl = "https://github.sonarsource.com";
838 AccessToken accessToken = new UserAccessToken(randomAlphanumeric(10));
839 String responseJson = "{\n"
840 + " \"id\": 1296269,\n"
841 + " \"node_id\": \"MDEwOlJlcG9zaXRvcnkxMjk2MjY5\",\n"
842 + " \"name\": \"Hello-World\",\n"
843 + " \"full_name\": \"octocat/Hello-World\",\n"
845 + " \"login\": \"octocat\",\n"
847 + " \"node_id\": \"MDQ6VXNlcjE=\",\n"
848 + " \"avatar_url\": \"https://github.sonarsource.com/images/error/octocat_happy.gif\",\n"
849 + " \"gravatar_id\": \"\",\n"
850 + " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
851 + " \"html_url\": \"https://github.com/octocat\",\n"
852 + " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
853 + " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
854 + " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
855 + " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
856 + " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
857 + " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
858 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
859 + " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
860 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
861 + " \"type\": \"User\",\n"
862 + " \"site_admin\": false\n"
864 + " \"private\": false,\n"
865 + " \"html_url\": \"https://github.com/octocat/Hello-World\",\n"
866 + " \"description\": \"This your first repo!\",\n"
867 + " \"fork\": false,\n"
868 + " \"url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World\",\n"
869 + " \"archive_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/{archive_format}{/ref}\",\n"
870 + " \"assignees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/assignees{/user}\",\n"
871 + " \"blobs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/blobs{/sha}\",\n"
872 + " \"branches_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/branches{/branch}\",\n"
873 + " \"collaborators_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/collaborators{/collaborator}\",\n"
874 + " \"comments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/comments{/number}\",\n"
875 + " \"commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/commits{/sha}\",\n"
876 + " \"compare_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/compare/{base}...{head}\",\n"
877 + " \"contents_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contents/{+path}\",\n"
878 + " \"contributors_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/contributors\",\n"
879 + " \"deployments_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/deployments\",\n"
880 + " \"downloads_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/downloads\",\n"
881 + " \"events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/events\",\n"
882 + " \"forks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/forks\",\n"
883 + " \"git_commits_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/commits{/sha}\",\n"
884 + " \"git_refs_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/refs{/sha}\",\n"
885 + " \"git_tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/tags{/sha}\",\n"
886 + " \"git_url\": \"git:github.com/octocat/Hello-World.git\",\n"
887 + " \"issue_comment_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/comments{/number}\",\n"
888 + " \"issue_events_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues/events{/number}\",\n"
889 + " \"issues_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/issues{/number}\",\n"
890 + " \"keys_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/keys{/key_id}\",\n"
891 + " \"labels_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/labels{/name}\",\n"
892 + " \"languages_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/languages\",\n"
893 + " \"merges_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/merges\",\n"
894 + " \"milestones_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/milestones{/number}\",\n"
895 + " \"notifications_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/notifications{?since,all,participating}\",\n"
896 + " \"pulls_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/pulls{/number}\",\n"
897 + " \"releases_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/releases{/id}\",\n"
898 + " \"ssh_url\": \"git@github.com:octocat/Hello-World.git\",\n"
899 + " \"stargazers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/stargazers\",\n"
900 + " \"statuses_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/statuses/{sha}\",\n"
901 + " \"subscribers_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscribers\",\n"
902 + " \"subscription_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/subscription\",\n"
903 + " \"tags_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/tags\",\n"
904 + " \"teams_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/teams\",\n"
905 + " \"trees_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/git/trees{/sha}\",\n"
906 + " \"clone_url\": \"https://github.com/octocat/Hello-World.git\",\n"
907 + " \"mirror_url\": \"git:git.example.com/octocat/Hello-World\",\n"
908 + " \"hooks_url\": \"https://github.sonarsource.com/api/v3/repos/octocat/Hello-World/hooks\",\n"
909 + " \"svn_url\": \"https://svn.github.com/octocat/Hello-World\",\n"
910 + " \"homepage\": \"https://github.com\",\n"
911 + " \"language\": null,\n"
912 + " \"forks_count\": 9,\n"
913 + " \"stargazers_count\": 80,\n"
914 + " \"watchers_count\": 80,\n"
915 + " \"size\": 108,\n"
916 + " \"default_branch\": \"master\",\n"
917 + " \"open_issues_count\": 0,\n"
918 + " \"is_template\": true,\n"
925 + " \"has_issues\": true,\n"
926 + " \"has_projects\": true,\n"
927 + " \"has_wiki\": true,\n"
928 + " \"has_pages\": false,\n"
929 + " \"has_downloads\": true,\n"
930 + " \"archived\": false,\n"
931 + " \"disabled\": false,\n"
932 + " \"visibility\": \"public\",\n"
933 + " \"pushed_at\": \"2011-01-26T19:06:43Z\",\n"
934 + " \"created_at\": \"2011-01-26T19:01:12Z\",\n"
935 + " \"updated_at\": \"2011-01-26T19:14:43Z\",\n"
936 + " \"permissions\": {\n"
937 + " \"admin\": false,\n"
938 + " \"push\": false,\n"
939 + " \"pull\": true\n"
941 + " \"allow_rebase_merge\": true,\n"
942 + " \"template_repository\": null,\n"
943 + " \"allow_squash_merge\": true,\n"
944 + " \"allow_merge_commit\": true,\n"
945 + " \"subscribers_count\": 42,\n"
946 + " \"network_count\": 0,\n"
947 + " \"anonymous_access_enabled\": false,\n"
948 + " \"license\": {\n"
949 + " \"key\": \"mit\",\n"
950 + " \"name\": \"MIT License\",\n"
951 + " \"spdx_id\": \"MIT\",\n"
952 + " \"url\": \"https://github.sonarsource.com/api/v3/licenses/mit\",\n"
953 + " \"node_id\": \"MDc6TGljZW5zZW1pdA==\"\n"
955 + " \"organization\": {\n"
956 + " \"login\": \"octocat\",\n"
958 + " \"node_id\": \"MDQ6VXNlcjE=\",\n"
959 + " \"avatar_url\": \"https://github.com/images/error/octocat_happy.gif\",\n"
960 + " \"gravatar_id\": \"\",\n"
961 + " \"url\": \"https://github.sonarsource.com/api/v3/users/octocat\",\n"
962 + " \"html_url\": \"https://github.com/octocat\",\n"
963 + " \"followers_url\": \"https://github.sonarsource.com/api/v3/users/octocat/followers\",\n"
964 + " \"following_url\": \"https://github.sonarsource.com/api/v3/users/octocat/following{/other_user}\",\n"
965 + " \"gists_url\": \"https://github.sonarsource.com/api/v3/users/octocat/gists{/gist_id}\",\n"
966 + " \"starred_url\": \"https://github.sonarsource.com/api/v3/users/octocat/starred{/owner}{/repo}\",\n"
967 + " \"subscriptions_url\": \"https://github.sonarsource.com/api/v3/users/octocat/subscriptions\",\n"
968 + " \"organizations_url\": \"https://github.sonarsource.com/api/v3/users/octocat/orgs\",\n"
969 + " \"repos_url\": \"https://github.sonarsource.com/api/v3/users/octocat/repos\",\n"
970 + " \"events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/events{/privacy}\",\n"
971 + " \"received_events_url\": \"https://github.sonarsource.com/api/v3/users/octocat/received_events\",\n"
972 + " \"type\": \"Organization\",\n"
973 + " \"site_admin\": false\n"
977 when(httpClient.get(appUrl, accessToken, "/repos/octocat/Hello-World"))
978 .thenReturn(new GetResponse() {
980 public Optional<String> getNextEndPoint() {
981 return Optional.empty();
985 public int getCode() {
990 public Optional<String> getContent() {
991 return Optional.of(responseJson);
995 public RateLimit getRateLimit() {
1000 Optional<GithubApplicationClient.Repository> repository = underTest.getRepository(appUrl, accessToken, "octocat/Hello-World");
1002 assertThat(repository)
1005 .extracting(GithubApplicationClient.Repository::getId, GithubApplicationClient.Repository::getName, GithubApplicationClient.Repository::getFullName,
1006 GithubApplicationClient.Repository::getUrl, GithubApplicationClient.Repository::isPrivate, GithubApplicationClient.Repository::getDefaultBranch)
1007 .containsOnly(1296269L, "Hello-World", "octocat/Hello-World", "https://github.com/octocat/Hello-World", false, "master");
1011 public void createAppInstallationToken_throws_IAE_if_application_token_cant_be_created() {
1012 mockNoApplicationJwtToken();
1014 assertThatThrownBy(() -> underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID))
1015 .isInstanceOf(IllegalArgumentException.class);
1018 private void mockNoApplicationJwtToken() {
1019 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenThrow(IllegalArgumentException.class);
1023 public void createAppInstallationToken_returns_empty_if_post_throws_IOE() throws IOException {
1025 when(httpClient.post(anyString(), any(AccessToken.class), anyString())).thenThrow(IOException.class);
1026 Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);
1028 assertThat(accessToken).isEmpty();
1029 assertThat(logTester.getLogs(Level.WARN)).extracting(LogAndArguments::getRawMsg).anyMatch(s -> s.startsWith("Failed to request"));
1033 public void createAppInstallationToken_returns_empty_if_access_token_cant_be_created() throws IOException {
1034 AppToken appToken = mockAppToken();
1035 mockAccessTokenCallingGithubFailure();
1037 Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);
1039 assertThat(accessToken).isEmpty();
1040 verify(httpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
1044 public void createAppInstallationToken_from_installation_id_returns_access_token() throws IOException {
1045 AppToken appToken = mockAppToken();
1046 AppInstallationToken installToken = mockCreateAccessTokenCallingGithub();
1048 Optional<AppInstallationToken> accessToken = underTest.createAppInstallationToken(githubAppConfiguration, INSTALLATION_ID);
1050 assertThat(accessToken).hasValue(installToken);
1051 verify(httpClient).post(appUrl, appToken, "/app/installations/" + INSTALLATION_ID + "/access_tokens");
1055 public void getRepositoryTeams_returnsRepositoryTeams() throws IOException {
1056 ArgumentCaptor<Function<String, List<GsonRepositoryTeam>>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);
1058 when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_TEAMS_ENDPOINT), deserializerCaptor.capture())).thenReturn(expectedTeams());
1060 Set<GsonRepositoryTeam> repoTeams = underTest.getRepositoryTeams(APP_URL, appInstallationToken, ORG_NAME, REPO_NAME);
1062 assertThat(repoTeams)
1063 .containsExactlyInAnyOrderElementsOf(expectedTeams());
1065 String responseContent = getResponseContent("repo-teams-full-response.json");
1066 assertThat(deserializerCaptor.getValue().apply(responseContent)).containsExactlyElementsOf(expectedTeams());
1070 public void getRepositoryTeams_whenGitHubCallThrowsIOException_shouldLogAndThrow() throws IOException {
1071 when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_TEAMS_ENDPOINT), any())).thenThrow(new IOException("error"));
1073 assertThatIllegalStateException()
1074 .isThrownBy(() -> underTest.getRepositoryTeams(APP_URL, appInstallationToken, ORG_NAME, REPO_NAME))
1075 .isInstanceOf(IllegalStateException.class)
1077 "SonarQube was not able to retrieve resources from GitHub. This is likely due to a connectivity problem or a temporary network outage: Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/teams. error");
1079 assertThat(logTester.logs()).hasSize(1);
1080 assertThat(logTester.logs(Level.WARN))
1081 .containsExactly("Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/teams.");
1084 private static List<GsonRepositoryTeam> expectedTeams() {
1086 new GsonRepositoryTeam("team1", 1, "team1", "pull", new GsonRepositoryPermissions(true, true, true, true, true)),
1087 new GsonRepositoryTeam("team2", 2, "team2", "push", new GsonRepositoryPermissions(false, false, true, true, true)));
1091 public void getRepositoryCollaborators_returnsCollaboratorsFromGithub() throws IOException {
1092 ArgumentCaptor<Function<String, List<GsonRepositoryCollaborator>>> deserializerCaptor = ArgumentCaptor.forClass(Function.class);
1094 when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_COLLABORATORS_ENDPOINT), deserializerCaptor.capture())).thenReturn(expectedCollaborators());
1096 Set<GsonRepositoryCollaborator> repoTeams = underTest.getRepositoryCollaborators(APP_URL, appInstallationToken, ORG_NAME, REPO_NAME);
1098 assertThat(repoTeams)
1099 .containsExactlyInAnyOrderElementsOf(expectedCollaborators());
1101 String responseContent = getResponseContent("repo-collaborators-full-response.json");
1102 assertThat(deserializerCaptor.getValue().apply(responseContent)).containsExactlyElementsOf(expectedCollaborators());
1107 public void getRepositoryCollaborators_whenGitHubCallThrowsIOException_shouldLogAndThrow() throws IOException {
1108 when(githubPaginatedHttpClient.get(eq(APP_URL), eq(appInstallationToken), eq(REPO_COLLABORATORS_ENDPOINT), any())).thenThrow(new IOException("error"));
1110 assertThatIllegalStateException()
1111 .isThrownBy(() -> underTest.getRepositoryCollaborators(APP_URL, appInstallationToken, ORG_NAME, REPO_NAME))
1112 .isInstanceOf(IllegalStateException.class)
1114 "SonarQube was not able to retrieve resources from GitHub. This is likely due to a connectivity problem or a temporary network outage: "
1115 + "Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/collaborators?affiliation=direct. error");
1117 assertThat(logTester.logs()).hasSize(1);
1118 assertThat(logTester.logs(Level.WARN))
1119 .containsExactly("Error while executing a paginated call to GitHub - appUrl: https://github.com/, path: /repos/ORG_NAME/repo1/collaborators?affiliation=direct.");
1122 private static String getResponseContent(String path) throws IOException {
1123 return IOUtils.toString(GithubApplicationClientImplTest.class.getResourceAsStream(path), StandardCharsets.UTF_8);
1126 private static List<GsonRepositoryCollaborator> expectedCollaborators() {
1128 new GsonRepositoryCollaborator("jean-michel", 1, "role1", new GsonRepositoryPermissions(true, true, true, true, true)),
1129 new GsonRepositoryCollaborator("jean-pierre", 2, "role2", new GsonRepositoryPermissions(false, false, true, true, true)));
1132 private void mockAccessTokenCallingGithubFailure() throws IOException {
1133 Response response = mock(Response.class);
1134 when(response.getContent()).thenReturn(Optional.empty());
1135 when(response.getCode()).thenReturn(HTTP_UNAUTHORIZED);
1136 when(httpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
1139 private AppToken mockAppToken() {
1140 String jwt = randomAlphanumeric(5);
1141 when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(new AppToken(jwt));
1142 return new AppToken(jwt);
1145 private AppInstallationToken mockCreateAccessTokenCallingGithub() throws IOException {
1146 String token = randomAlphanumeric(5);
1147 Response response = mock(Response.class);
1148 when(response.getContent()).thenReturn(Optional.of("{" +
1149 " \"token\": \"" + token + "\"" +
1151 when(response.getCode()).thenReturn(HTTP_CREATED);
1152 when(httpClient.post(eq(appUrl), any(AppToken.class), eq("/app/installations/" + INSTALLATION_ID + "/access_tokens"))).thenReturn(response);
1153 return new AppInstallationToken(token);
1156 private static class OkGetResponse extends Response {
1157 private OkGetResponse(String content) {
1158 super(200, content);
1162 private static class ErrorGetResponse extends Response {
1163 ErrorGetResponse() {
1167 ErrorGetResponse(int code, String content) {
1168 super(code, content);
1172 private static class Response implements GetResponse {
1173 private final int code;
1174 private final String content;
1175 private final String nextEndPoint;
1177 private Response(int code, @Nullable String content) {
1178 this(code, content, null);
1181 private Response(int code, @Nullable String content, @Nullable String nextEndPoint) {
1183 this.content = content;
1184 this.nextEndPoint = nextEndPoint;
1188 public int getCode() {
1193 public Optional<String> getContent() {
1194 return Optional.ofNullable(content);
1198 public RateLimit getRateLimit() {
1203 public Optional<String> getNextEndPoint() {
1204 return Optional.ofNullable(nextEndPoint);