diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java index 0c2e0f1f9..a606fecdc 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2022 Michael Clarke + * Copyright (C) 2020-2023 Michael Clarke * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,9 +28,14 @@ import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.AppToken; import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.InstallationRepositories; import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.Repository; -import com.google.common.annotations.VisibleForTesting; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.DefaultJwtBuilder; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; + import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; @@ -41,16 +46,8 @@ 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; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; -import org.sonar.api.ce.ComputeEngineSide; -import org.sonar.api.server.ServerSide; @ServerSide @ComputeEngineSide @@ -75,68 +72,70 @@ public RestApplicationAuthenticationProvider(Clock clock, LinkHeaderReader linkH this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } - @VisibleForTesting - protected List getAppInstallations(ObjectMapper objectMapper, String apiUrl, String jwtToken) throws IOException { + @Override + public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey, + String projectPath) throws IOException { + + Instant issued = clock.instant().minus(10, ChronoUnit.SECONDS); + Instant expiry = issued.plus(2, ChronoUnit.MINUTES); + String jwtToken = new DefaultJwtBuilder().setIssuedAt(Date.from(issued)).setExpiration(Date.from(expiry)) + .claim("iss", appId).signWith(createPrivateKey(apiPrivateKey), SignatureAlgorithm.RS256).compact(); - List appInstallations = new ArrayList<>(); + Optional repositoryAuthenticationToken = findTokenFromAppInstallationList(getV3Url(apiUrl) + "/app/installations", jwtToken, projectPath); + + return repositoryAuthenticationToken.orElseThrow(() -> new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, + "No token could be found with access to the requested repository using the given application ID and key")); + } + private Optional findTokenFromAppInstallationList(String apiUrl, String jwtToken, String projectPath) throws IOException { 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))); + AppInstallation[] appInstallations = objectMapper.readerFor(AppInstallation[].class).readValue(reader); + for (AppInstallation appInstallation : appInstallations) { + Optional repositoryAuthenticationToken = findAppTokenFromAppInstallation(appInstallation, jwtToken, projectPath); + + if (repositoryAuthenticationToken.isPresent()) { + return repositoryAuthenticationToken; + } + } } Optional nextLink = linkHeaderReader.findNextLink(appConnection.getHeaderField("Link")); - if (nextLink.isPresent()) { - appInstallations.addAll(getAppInstallations(objectMapper, nextLink.get(), jwtToken)); + if (nextLink.isEmpty()) { + return Optional.empty(); } - return appInstallations; + return findTokenFromAppInstallationList(nextLink.get(), jwtToken, projectPath); } - @Override - public RepositoryAuthenticationToken getInstallationToken(String apiUrl, String appId, String apiPrivateKey, - String projectPath) throws IOException { - - Instant issued = clock.instant().minus(10, ChronoUnit.SECONDS); - Instant expiry = issued.plus(2, ChronoUnit.MINUTES); - String jwtToken = new DefaultJwtBuilder().setIssuedAt(Date.from(issued)).setExpiration(Date.from(expiry)) - .claim("iss", appId).signWith(createPrivateKey(apiPrivateKey), SignatureAlgorithm.RS256).compact(); - - ObjectMapper objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - List appInstallations = getAppInstallations(objectMapper, getV3Url(apiUrl) + "/app/installations", jwtToken); + private Optional findAppTokenFromAppInstallation(AppInstallation installation, String jwtToken, String projectPath) throws IOException { + URLConnection accessTokenConnection = urlProvider.createUrlConnection(installation.getAccessTokensUrl()); + ((HttpURLConnection) accessTokenConnection).setRequestMethod("POST"); + accessTokenConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER); + accessTokenConnection + .setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken); - for (AppInstallation installation : appInstallations) { - URLConnection accessTokenConnection = urlProvider.createUrlConnection(installation.getAccessTokensUrl()); - ((HttpURLConnection) accessTokenConnection).setRequestMethod("POST"); - accessTokenConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER); - accessTokenConnection - .setRequestProperty(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION_HEADER_PREFIX + jwtToken); + try (Reader reader = new InputStreamReader(accessTokenConnection.getInputStream())) { + AppToken appToken = objectMapper.readerFor(AppToken.class).readValue(reader); + String targetUrl = installation.getRepositoriesUrl(); - try (Reader reader = new InputStreamReader(accessTokenConnection.getInputStream())) { - AppToken appToken = objectMapper.readerFor(AppToken.class).readValue(reader); - - String targetUrl = installation.getRepositoriesUrl(); - - Optional potentialRepositoryAuthenticationToken = findRepositoryAuthenticationToken(appToken, targetUrl, projectPath, objectMapper); - - if (potentialRepositoryAuthenticationToken.isPresent()) { - return potentialRepositoryAuthenticationToken.get(); - } + Optional potentialRepositoryAuthenticationToken = findRepositoryAuthenticationToken(appToken, targetUrl, projectPath); + if (potentialRepositoryAuthenticationToken.isPresent()) { + return potentialRepositoryAuthenticationToken; } + } - throw new InvalidConfigurationException(InvalidConfigurationException.Scope.PROJECT, - "No token could be found with access to the requested repository using the given application ID and key"); + return Optional.empty(); } private Optional findRepositoryAuthenticationToken(AppToken appToken, String targetUrl, - String projectPath, ObjectMapper objectMapper) throws IOException { + String projectPath) throws IOException { URLConnection installationRepositoriesConnection = urlProvider.createUrlConnection(targetUrl); ((HttpURLConnection) installationRepositoriesConnection).setRequestMethod("GET"); installationRepositoriesConnection.setRequestProperty(ACCEPT_HEADER, APP_PREVIEW_ACCEPT_HEADER); @@ -161,7 +160,7 @@ private Optional findRepositoryAuthenticationToke return Optional.empty(); } - return findRepositoryAuthenticationToken(appToken, nextLink.get(), projectPath, objectMapper); + return findRepositoryAuthenticationToken(appToken, nextLink.get(), projectPath); } private static String getV3Url(String apiUrl) { diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java index 1c6d58e1e..e22f4dde5 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/almclient/github/v3/RestApplicationAuthenticationProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2022 Michael Clarke + * Copyright (C) 2020-2023 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,16 +18,17 @@ */ package com.github.mc1arke.sonarqube.plugin.almclient.github.v3; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.InvalidConfigurationException; import com.github.mc1arke.sonarqube.plugin.almclient.LinkHeaderReader; import com.github.mc1arke.sonarqube.plugin.almclient.github.RepositoryAuthenticationToken; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.AppInstallation; -import java.util.List; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.AppToken; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.InstallationRepositories; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.Owner; +import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.model.Repository; import org.apache.commons.io.IOUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import java.io.ByteArrayInputStream; @@ -39,210 +40,127 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; 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 { - - @Test - public void testTokenRetrievedHappyPath() throws IOException { - testTokenForUrl("apiUrl", "apiUrl/app/installations"); - } - - @Test - public void testTokenRetrievedHappyPathApiPath() throws IOException { - testTokenForUrl("apiUrl/api", "apiUrl/api/v3/app/installations"); - } - - @Test - public void testTokenRetrievedHappyPathApiPathTrailingSlash() throws IOException { - testTokenForUrl("apiUrl/api/", "apiUrl/api/v3/app/installations"); - } - - @Test - public void testTokenRetrievedHappyPathV3Path() throws IOException { - testTokenForUrl("apiUrl/api/v3", "apiUrl/api/v3/app/installations"); - } +class RestApplicationAuthenticationProviderTest { @Test - public void testTokenRetrievedHappyPathV3PathTrailingSlash() throws IOException { - testTokenForUrl("apiUrl/api/v3/", "apiUrl/api/v3/app/installations"); - } - - private void testTokenForUrl(String apiUrl, String fullUrl) throws IOException { - UrlConnectionProvider urlProvider = mock(UrlConnectionProvider.class); + void shouldReturnTokenForPaginatedInstallationsAndTokens() throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + UrlConnectionProvider urlConnectionProvider = mock(UrlConnectionProvider.class); Clock clock = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneId.of("UTC")); + LinkHeaderReader linkHeaderReader = mock(LinkHeaderReader.class); - String expectedAuthenticationToken = "expected authentication token"; - String projectPath = "project path"; - String expectedRepositoryId = "expected repository Id"; - String expectedHtmlUrl = "http://url.for/users/repo"; - - URLConnection installationsUrlConnection = mock(URLConnection.class); - doReturn(new ByteArrayInputStream( - "[{\"repositories_url\": \"repositories_url\", \"access_tokens_url\": \"tokens_url\"}]" - .getBytes(StandardCharsets.UTF_8))).when(installationsUrlConnection).getInputStream(); - - HttpURLConnection accessTokensUrlConnection = mock(HttpURLConnection.class); - doReturn(new ByteArrayInputStream( - ("{\"token\": \"" + expectedAuthenticationToken + "\"}").getBytes(StandardCharsets.UTF_8))) - .when(accessTokensUrlConnection).getInputStream(); - doReturn(accessTokensUrlConnection).when(urlProvider).createUrlConnection("tokens_url"); - - - HttpURLConnection repositoriesUrlConnection = mock(HttpURLConnection.class); - doReturn(new ByteArrayInputStream( - ("{\"repositories\": [{\"node_id\": \"" + expectedRepositoryId + "\", \"full_name\": \"" + projectPath + - "\", \"html_url\": \"" + expectedHtmlUrl + "\", \"name\": \"project\", \"owner\": {\"login\": \"owner_name\"}}]}").getBytes(StandardCharsets.UTF_8))).when(repositoriesUrlConnection).getInputStream(); - doReturn(repositoriesUrlConnection).when(urlProvider).createUrlConnection("repositories_url"); - - doReturn(installationsUrlConnection).when(urlProvider).createUrlConnection(fullUrl); + when(linkHeaderReader.findNextLink(any())).thenAnswer(i -> Optional.ofNullable(i.getArgument(0))); + String apiUrl = "https://api.url/api/v3"; String appId = "appID"; - String apiPrivateKey; - try (InputStream inputStream = getClass().getResourceAsStream("/rsa-private-key.pem")) { + try (InputStream inputStream = Optional.ofNullable(getClass().getResourceAsStream("/rsa-private-key.pem")).orElseThrow()) { apiPrivateKey = IOUtils.toString(inputStream, StandardCharsets.UTF_8); } - - RestApplicationAuthenticationProvider testCase = new RestApplicationAuthenticationProvider(clock, h -> Optional.empty(), urlProvider); - RepositoryAuthenticationToken result = testCase.getInstallationToken(apiUrl, appId, apiPrivateKey, projectPath); - - assertEquals(expectedAuthenticationToken, result.getAuthenticationToken()); - assertEquals(expectedRepositoryId, result.getRepositoryId()); - assertEquals(expectedHtmlUrl, result.getRepositoryUrl()); - - ArgumentCaptor requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(installationsUrlConnection, times(2)) - .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw"), - requestPropertyArgumentCaptor.getAllValues()); - - requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(accessTokensUrlConnection, times(2)) - .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); - verify(accessTokensUrlConnection).setRequestMethod("POST"); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw"), - requestPropertyArgumentCaptor.getAllValues()); - - requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(repositoriesUrlConnection, times(2)) - .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); - verify(repositoriesUrlConnection).setRequestMethod("GET"); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer " + expectedAuthenticationToken), - 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) ); + String projectPath = "path/repo-49.3"; + + List appPageUrlConnections = new ArrayList<>(); + List appTokenConnections = new ArrayList<>(); + List appRepositoryConnections = new ArrayList<>(); + for (int appPage = 0; appPage < 5; appPage++) { + List appPageContents = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + int itemNumber = (appPage * 10) + i; + appPageContents.add(new AppInstallation("http://repository.url/item-" + itemNumber, "http://acccess-token.url/item-" + itemNumber)); + + HttpURLConnection appTokenConnection = mock(HttpURLConnection.class); + when(appTokenConnection.getInputStream()).thenReturn(new ByteArrayInputStream(objectMapper.writeValueAsBytes(new AppToken("token-" + itemNumber)))); + when(urlConnectionProvider.createUrlConnection("http://acccess-token.url/item-" + itemNumber)).thenReturn(appTokenConnection); + appTokenConnections.add(appTokenConnection); + + List repositories = new ArrayList<>(); + for (int x = 0; x < 5; x++) { + repositories.add(new Repository("nodeId", "path/repo-" + itemNumber + "." + x, "url", "repo-" + itemNumber + "." + x, new Owner("login"))); + } + + HttpURLConnection repositoryConnection = mock(HttpURLConnection.class); + when(repositoryConnection.getInputStream()).thenAnswer(invocation -> new ByteArrayInputStream(objectMapper.writeValueAsBytes(new InstallationRepositories(repositories.toArray(new Repository[0]))))); + when(urlConnectionProvider.createUrlConnection("http://repository.url/item-" + itemNumber)).thenReturn(repositoryConnection); + if (i == 1 && appPage == 1) { + when(repositoryConnection.getHeaderField("Link")).thenReturn("http://repository.url/item-" + (itemNumber + 1)); + } + appRepositoryConnections.add(repositoryConnection); + } + HttpURLConnection appPageUrlConnection = mock(HttpURLConnection.class); + when(appPageUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(objectMapper.writeValueAsBytes(appPageContents))); + if (appPage < 4) { + when(appPageUrlConnection.getHeaderField("Link")).thenReturn(apiUrl + "/app/installations?page=" + (appPage + 1)); + } + appPageUrlConnections.add(appPageUrlConnection); + if (appPage == 0) { + when(urlConnectionProvider.createUrlConnection(apiUrl + "/app/installations")).thenReturn(appPageUrlConnection); + } else { + when(urlConnectionProvider.createUrlConnection(apiUrl + "/app/installations?page=" + appPage)).thenReturn(appPageUrlConnection); + } } - 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); - Clock clock = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneId.of("UTC")); - - String expectedAuthenticationToken = "expected authentication token"; - String projectPath = "project path"; - String expectedRepositoryId = "expected repository Id"; - - URLConnection installationsUrlConnection = mock(URLConnection.class); - doReturn(new ByteArrayInputStream( - "[{\"repositories_url\": \"repositories_url\", \"access_tokens_url\": \"tokens_url\"}]" - .getBytes(StandardCharsets.UTF_8))).when(installationsUrlConnection).getInputStream(); + RepositoryAuthenticationToken expected = new RepositoryAuthenticationToken("nodeId", "token-49", "url", "repo-49.3", "login"); - HttpURLConnection accessTokensUrlConnection = mock(HttpURLConnection.class); - doReturn(new ByteArrayInputStream( - ("{\"token\": \"" + expectedAuthenticationToken + "\"}").getBytes(StandardCharsets.UTF_8))) - .when(accessTokensUrlConnection).getInputStream(); - doReturn(accessTokensUrlConnection).when(urlProvider).createUrlConnection("tokens_url"); + RestApplicationAuthenticationProvider restApplicationAuthenticationProvider = new RestApplicationAuthenticationProvider(clock, linkHeaderReader, urlConnectionProvider); + RepositoryAuthenticationToken repositoryAuthenticationToken = restApplicationAuthenticationProvider.getInstallationToken("https://api.url/api/", appId, apiPrivateKey, projectPath); + assertThat(repositoryAuthenticationToken).usingRecursiveComparison().isEqualTo(expected); - for (int i = 0; i < 2; i ++) { - HttpURLConnection repositoriesUrlConnection = mock(HttpURLConnection.class); - doReturn(new ByteArrayInputStream( - ("{\"repositories\": [{\"node_id\": \"" + expectedRepositoryId + (i == 0 ? "a" : "") + "\", \"full_name\": \"" + - projectPath + (i == 0 ? "a" : "") + "\", \"name\": \"name\", \"owner\": {\"login\": \"login\"}}]}").getBytes(StandardCharsets.UTF_8))).when(repositoriesUrlConnection).getInputStream(); - doReturn(i == 0 ? "a" : null).when(repositoriesUrlConnection).getHeaderField("Link"); - doReturn(repositoriesUrlConnection).when(urlProvider).createUrlConnection(i == 0 ? "repositories_url" : "https://dummy.url/path?param=dummy&page=" + (i + 1)); + for (URLConnection installationsUrlConnection : appPageUrlConnections) { + ArgumentCaptor requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); + verify(installationsUrlConnection, times(2)) + .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", + "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw")); } + for (HttpURLConnection accessTokensUrlConnection : appTokenConnections) { + ArgumentCaptor requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); + verify(accessTokensUrlConnection, times(2)) + .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); + verify(accessTokensUrlConnection).setRequestMethod("POST"); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", + "Authorization", "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw")); - String apiUrl = "apiUrl"; - doReturn(installationsUrlConnection).when(urlProvider).createUrlConnection(apiUrl + "/app/installations"); - - String appId = "appID"; - - String apiPrivateKey; - try (InputStream inputStream = getClass().getResourceAsStream("/rsa-private-key.pem")) { - apiPrivateKey = IOUtils.toString(inputStream, StandardCharsets.UTF_8); } - LinkHeaderReader linkHeaderReader = mock(LinkHeaderReader.class); - doReturn(Optional.of("https://dummy.url/path?param=dummy&page=2")).when(linkHeaderReader).findNextLink("a"); - doReturn(Optional.empty()).when(linkHeaderReader).findNextLink(isNull()); - - RestApplicationAuthenticationProvider testCase = new RestApplicationAuthenticationProvider(clock, linkHeaderReader, urlProvider); - RepositoryAuthenticationToken result = testCase.getInstallationToken(apiUrl, appId, apiPrivateKey, projectPath); - - assertEquals(expectedAuthenticationToken, result.getAuthenticationToken()); - assertEquals(expectedRepositoryId, result.getRepositoryId()); + for (int i = 0; i < appRepositoryConnections.size(); i++) { + HttpURLConnection appRepositoryConnection = appRepositoryConnections.get(i); + ArgumentCaptor requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); + if (i == 12) { + verify(appRepositoryConnection, times(4)) + .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", + "Authorization", "Bearer token-11", + "Accept", "application/vnd.github.machine-man-preview+json", + "Authorization", "Bearer token-12")); + } else { + verify(appRepositoryConnection, times(2)) + .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", + "Authorization", "Bearer token-" + i)); + } - ArgumentCaptor requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(installationsUrlConnection, times(2)) - .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw"), - requestPropertyArgumentCaptor.getAllValues()); - - requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(accessTokensUrlConnection, times(2)) - .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); - verify(accessTokensUrlConnection).setRequestMethod("POST"); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw"), - requestPropertyArgumentCaptor.getAllValues()); + } } @Test - public void testExceptionOnNoMatchingToken() throws IOException { + void shouldThrowExceptionIfNotTokensMatch() throws IOException { UrlConnectionProvider urlProvider = mock(UrlConnectionProvider.class); Clock clock = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneId.of("UTC")); @@ -275,7 +193,7 @@ public void testExceptionOnNoMatchingToken() throws IOException { String appId = "appID"; String apiPrivateKey; - try (InputStream inputStream = getClass().getResourceAsStream("/rsa-private-key.pem")) { + try (InputStream inputStream = Optional.ofNullable(getClass().getResourceAsStream("/rsa-private-key.pem")).orElseThrow()) { apiPrivateKey = IOUtils.toString(inputStream, StandardCharsets.UTF_8); } @@ -287,25 +205,22 @@ public void testExceptionOnNoMatchingToken() throws IOException { ArgumentCaptor requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); verify(installationsUrlConnection, times(2)) .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw"), - requestPropertyArgumentCaptor.getAllValues()); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", + "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw")); requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); verify(accessTokensUrlConnection, times(2)) .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); verify(accessTokensUrlConnection).setRequestMethod("POST"); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw"), - requestPropertyArgumentCaptor.getAllValues()); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", + "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjEyMzQ0NiwiZXhwIjoxMjM1NjYsImlzcyI6ImFwcElEIn0.yMvAoUmmAHli-Mc-RidLbqlX2Cvc2RwPBwkgY6n1R2ZkV-IaY8uBO4s7pp0-3hcJvY4F7-UGnAi1dteGOODY8cOmx86DsSASJIHJ3wxaRxyLGOq2Z8A1KSWZj-F8O6wFf5pm2xzumm0gSSwdd3gQR0FiSn2TIHemjyoieNJfzvG2kgtHPBNIVaJcS8LqkVYBlvAujnAt1nQ1hIAbeQJyEmyVyb_NRMPQZZioBraobTlWdPWdnTQoNTWjmjcopIbUFw8s21uhMcDpA_6lS1iAZcoZKcpzMqsItEvQaiwYQWRccfZT69M_zWaVRjw2-eKsTuFXzumVyq3MnAoxy6R2Xw")); requestPropertyArgumentCaptor = ArgumentCaptor.forClass(String.class); verify(repositoriesUrlConnection, times(2)) .setRequestProperty(requestPropertyArgumentCaptor.capture(), requestPropertyArgumentCaptor.capture()); verify(repositoriesUrlConnection).setRequestMethod("GET"); - assertEquals(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", - "Bearer " + expectedAuthenticationToken), - requestPropertyArgumentCaptor.getAllValues()); + assertThat(requestPropertyArgumentCaptor.getAllValues()).isEqualTo(Arrays.asList("Accept", "application/vnd.github.machine-man-preview+json", "Authorization", + "Bearer " + expectedAuthenticationToken)); } }