diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProvider.java index 6f9717a90..864b629da 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProvider.java @@ -27,6 +27,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.AppToken; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.InstallationRepositories; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.Repository; +import com.google.common.annotations.VisibleForTesting; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.DefaultJwtBuilder; import org.bouncycastle.openssl.PEMKeyPair; @@ -43,7 +44,10 @@ import java.time.Clock; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Optional; public class RestApplicationAuthenticationProvider implements GithubApplicationAuthenticationProvider { @@ -69,6 +73,27 @@ public RestApplicationAuthenticationProvider(Clock clock, LinkHeaderReader linkH this.linkHeaderReader = linkHeaderReader; } + @VisibleForTesting + protected List getAppInstallations(ObjectMapper objectMapper, String apiUrl, String jwtToken) throws IOException { + + List appInstallations = new ArrayList<>(); + + URLConnection appConnection = urlProvider.createUrlConnection(apiUrl); + appConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER); + appConnection.setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken); + + try (Reader reader = new InputStreamReader(appConnection.getInputStream())) { + appInstallations.addAll(Arrays.asList(objectMapper.readerFor(AppInstallation[].class).readValue(reader))); + } + + Optional nextLink = linkHeaderReader.findNextLink(appConnection.getHeaderField("Link")); + if (nextLink.isPresent()) { + appInstallations.addAll(getAppInstallations(objectMapper, nextLink.get(), jwtToken)); + } + + return appInstallations; + } + @Override public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey, String projectPath) throws IOException { @@ -80,15 +105,7 @@ public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - URLConnection appConnection = urlProvider.createUrlConnection(getV3Url(apiUrl) + "/app/installations"); - appConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER); - appConnection.setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken); - - AppInstallation[] appInstallations; - try (Reader reader = new InputStreamReader(appConnection.getInputStream())) { - appInstallations = objectMapper.readerFor(AppInstallation[].class).readValue(reader); - } - + List appInstallations = getAppInstallations(objectMapper, getV3Url(apiUrl) + "/app/installations", jwtToken); for (AppInstallation installation : appInstallations) { URLConnection accessTokenConnection = urlProvider.createUrlConnection(installation.getAccessTokensUrl()); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProviderTest.java index c8a8ab574..0b3e27fdc 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v3/RestApplicationAuthenticationProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Michael Clarke + * Copyright (C) 2020-2022 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,8 +18,11 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.LinkHeaderReader; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.RepositoryAuthenticationToken; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v3.model.AppInstallation; import org.apache.commons.io.IOUtils; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -34,6 +37,7 @@ import java.time.Instant; import java.time.ZoneId; import java.util.Arrays; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +49,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class RestApplicationAuthenticationProviderTest { @@ -140,6 +145,32 @@ private void testTokenForUrl(String apiUrl, String fullUrl) throws IOException { requestPropertyArgumentCaptor.getAllValues()); } + @Test + public void testAppInstallationsPagination() throws IOException { + + UrlConnectionProvider urlProvider = mock(UrlConnectionProvider.class); + Clock clock = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneId.of("UTC")); + + String apiUrl = "apiUrl"; + + int pages=4; + + for(int i=1; i<=pages;i++) { + HttpURLConnection installationsUrlConnection = mock(HttpURLConnection.class); + doReturn(installationsUrlConnection).when(urlProvider).createUrlConnection(eq(apiUrl + "/app/installations?page=" + i)); + when(installationsUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream( + "[{\"repositories_url\": \"repositories_url\", \"access_tokens_url\": \"tokens_url\"}]" + .getBytes(StandardCharsets.UTF_8))); + when(installationsUrlConnection.getHeaderField("Link")).thenReturn(i == pages ? null: apiUrl + "/app/installations?page=" + (i+1) ); + } + + RestApplicationAuthenticationProvider testCase = new RestApplicationAuthenticationProvider(clock, Optional::ofNullable, urlProvider); + ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + List token = testCase.getAppInstallations(objectMapper, apiUrl + "/app/installations?page=1", "token"); + assertThat(token).hasSize(pages); + + } + @Test public void testTokenRetrievedPaginatedHappyPath() throws IOException { UrlConnectionProvider urlProvider = mock(UrlConnectionProvider.class);