From 66d786a3382970c1f9364f3c0e6319a06aa2a9e4 Mon Sep 17 00:00:00 2001 From: C4tWithShell Date: Mon, 11 Dec 2023 21:05:57 +0300 Subject: [PATCH] Test --- gradle.properties | 2 +- ...RestApplicationAuthenticationProvider.java | 97 +++--- .../ce/CommunityBranchLoaderDelegate.java | 2 +- .../plugin/ce/pullrequest/markup/Link.java | 9 +- .../pullrequest/report/AnalysisSummary.java | 70 ++++- .../pullrequest/report/ReportGenerator.java | 25 +- ...ApplicationAuthenticationProviderTest.java | 285 ++++++------------ .../ce/CommunityBranchLoaderDelegateTest.java | 4 +- .../ce/pullrequest/markup/LinkTest.java | 68 +++-- .../report/AnalysisSummaryTest.java | 15 +- .../report/ReportGeneratorTest.java | 5 + 11 files changed, 313 insertions(+), 269 deletions(-) diff --git a/gradle.properties b/gradle.properties index c7ca05f..500d9df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.16.2 +version=1.16.1 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 0c2e0f1..a606fec 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/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java index 133fcb6..3ee9d24 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegate.java @@ -119,7 +119,7 @@ private static Branch createBranch(DbClient dbClient, String branchName, String private static Optional findBranchByUuid(String projectUuid, DbClient dbClient) { try (DbSession dbSession = dbClient.openSession(false)) { - return dbClient.branchDao().selectByUuid(dbSession, projectUuid); + return dbClient.branchDao().selectMainBranchByProjectUuid(dbSession, projectUuid); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java index dafd108..aaf321b 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/Link.java @@ -18,14 +18,17 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; +import java.util.Set; + public final class Link extends Node { + private static final Set> SUPPORTED_CHILDREN = Set.of(Text.class, Image.class); private final String url; public Link(String url, Node... children) { super(children); - this.url=url; - } + this.url = url; + } public String getUrl() { return url; @@ -33,6 +36,6 @@ public String getUrl() { @Override boolean isValidChild(Node child) { - return child instanceof Text; + return child != null && SUPPORTED_CHILDREN.contains(child.getClass()); } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java index a95f913..a3468b2 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummary.java @@ -47,22 +47,27 @@ public final class AnalysisSummary { private final BigDecimal newCoverage; private final BigDecimal coverage; + private final String coverageUrl; private final String coverageImageUrl; private final BigDecimal newDuplications; private final BigDecimal duplications; + private final String duplicationsUrl; private final String duplicationsImageUrl; private final long totalIssueCount; private final long bugCount; + private final String bugUrl; private final String bugImageUrl; private final long securityHotspotCount; private final long vulnerabilityCount; + private final String vulnerabilityUrl; private final String vulnerabilityImageUrl; private final long codeSmellCount; + private final String codeSmellUrl; private final String codeSmellImageUrl; private AnalysisSummary(Builder builder) { @@ -74,17 +79,22 @@ private AnalysisSummary(Builder builder) { this.dashboardUrl = builder.dashboardUrl; this.newCoverage = builder.newCoverage; this.coverage = builder.coverage; + this.coverageUrl = builder.coverageUrl; this.coverageImageUrl = builder.coverageImageUrl; this.newDuplications = builder.newDuplications; this.duplications = builder.duplications; + this.duplicationsUrl = builder.duplicationsUrl; this.duplicationsImageUrl = builder.duplicationsImageUrl; this.totalIssueCount = builder.totalIssueCount; this.bugCount = builder.bugCount; + this.bugUrl = builder.bugUrl; this.bugImageUrl = builder.bugImageUrl; this.securityHotspotCount = builder.securityHotspotCount; this.vulnerabilityCount = builder.vulnerabilityCount; + this.vulnerabilityUrl = builder.vulnerabilityUrl; this.vulnerabilityImageUrl = builder.vulnerabilityImageUrl; this.codeSmellCount = builder.codeSmellCount; + this.codeSmellUrl = builder.codeSmellUrl; this.codeSmellImageUrl = builder.codeSmellImageUrl; } @@ -120,6 +130,10 @@ public BigDecimal getCoverage() { return coverage; } + public String getCoverageUrl() { + return coverageUrl; + } + public String getCoverageImageUrl() { return coverageImageUrl; } @@ -132,6 +146,10 @@ public BigDecimal getDuplications() { return duplications; } + public String getDuplicationsUrl() { + return duplicationsUrl; + } + public String getDuplicationsImageUrl() { return duplicationsImageUrl; } @@ -144,6 +162,10 @@ public long getBugCount() { return bugCount; } + public String getBugUrl() { + return bugUrl; + } + public String getBugImageUrl() { return bugImageUrl; } @@ -156,6 +178,10 @@ public long getVulnerabilityCount() { return vulnerabilityCount; } + public String getVulnerabilityUrl() { + return vulnerabilityUrl; + } + public String getVulnerabilityImageUrl() { return vulnerabilityImageUrl; } @@ -164,6 +190,10 @@ public long getCodeSmellCount() { return codeSmellCount; } + public String getCodeSmellUrl() { + return codeSmellUrl; + } + public String getCodeSmellImageUrl() { return codeSmellImageUrl; } @@ -185,26 +215,26 @@ public String format(FormatterFactory formatterFactory) { new Heading(2, new Text(pluralOf(getTotalIssueCount(), "Issue", "Issues"))), new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, - new ListItem(new Image("Bug", getBugImageUrl()), + new ListItem(new Link(getBugUrl(), new Image("Bug", getBugImageUrl())), new Text(" "), new Text(pluralOf(getBugCount(), "Bug", "Bugs"))), - new ListItem(new Image("Vulnerability", getVulnerabilityImageUrl()), + new ListItem(new Link(getVulnerabilityUrl(), new Image("Vulnerability", getVulnerabilityImageUrl())), new Text(" "), new Text(pluralOf(getVulnerabilityCount() + getSecurityHotspotCount(), "Vulnerability", "Vulnerabilities"))), - new ListItem(new Image("Code Smell", getCodeSmellImageUrl()), + new ListItem(new Link(getCodeSmellUrl(), new Image("Code Smell", getCodeSmellImageUrl())), new Text(" "), new Text(pluralOf(getCodeSmellCount(), "Code Smell", "Code Smells")))), new Heading(2, new Text("Coverage and Duplications")), new com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List( com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup.List.Style.BULLET, - new ListItem(new Image("Coverage", getCoverageImageUrl()), + new ListItem(new Link(getCoverageUrl(), new Image("Coverage", getCoverageImageUrl())), new Text(" "), new Text( Optional.ofNullable(getNewCoverage()) .map(decimalFormat::format) .map(i -> i + "% Coverage") .orElse("No coverage information") + " (" + decimalFormat.format(Optional.ofNullable(getCoverage()).orElse(BigDecimal.valueOf(0))) + "% Estimated after merge)")), - new ListItem(new Image("Duplications", getDuplicationsImageUrl()), + new ListItem(new Link(getDuplicationsUrl(), new Image("Duplications", getDuplicationsImageUrl())), new Text(" "), new Text(Optional.ofNullable(getNewDuplications()) .map(decimalFormat::format) @@ -236,22 +266,27 @@ public static class Builder { private BigDecimal newCoverage; private BigDecimal coverage; + private String coverageUrl; private String coverageImageUrl; private BigDecimal newDuplications; private BigDecimal duplications; + private String duplicationsUrl; private String duplicationsImageUrl; private long totalIssueCount; private long bugCount; + private String bugUrl; private String bugImageUrl; private long securityHotspotCount; private long vulnerabilityCount; + private String vulnerabilityUrl; private String vulnerabilityImageUrl; private long codeSmellCount; + private String codeSmellUrl; private String codeSmellImageUrl; private Builder() { @@ -298,6 +333,11 @@ public Builder withCoverage(BigDecimal coverage) { return this; } + public Builder withCoverageUrl(String coverageUrl) { + this.coverageUrl = coverageUrl; + return this; + } + public Builder withCoverageImageUrl(String coverageImageUrl) { this.coverageImageUrl = coverageImageUrl; return this; @@ -313,6 +353,11 @@ public Builder withDuplications(BigDecimal duplications) { return this; } + public Builder withDuplicationsUrl(String duplicationsUrl) { + this.duplicationsUrl = duplicationsUrl; + return this; + } + public Builder withDuplicationsImageUrl(String duplicationsImageUrl) { this.duplicationsImageUrl = duplicationsImageUrl; return this; @@ -328,6 +373,11 @@ public Builder withBugCount(long bugCount) { return this; } + public Builder withBugUrl(String bugUrl) { + this.bugUrl = bugUrl; + return this; + } + public Builder withBugImageUrl(String bugImageUrl) { this.bugImageUrl = bugImageUrl; return this; @@ -343,6 +393,11 @@ public Builder withVulnerabilityCount(long vulnerabilityCount) { return this; } + public Builder withVulnerabilityUrl(String vulnerabilityUrl) { + this.vulnerabilityUrl = vulnerabilityUrl; + return this; + } + public Builder withVulnerabilityImageUrl(String vulnerabilityImageUrl) { this.vulnerabilityImageUrl = vulnerabilityImageUrl; return this; @@ -353,6 +408,11 @@ public Builder withCodeSmellCount(long codeSmellCount) { return this; } + public Builder withCodeSmellUrl(String codeSmellUrl) { + this.codeSmellUrl = codeSmellUrl; + return this; + } + public Builder withCodeSmellImageUrl(String codeSmellImageUrl) { this.codeSmellImageUrl = codeSmellImageUrl; return this; diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java index 36067ab..c1237ef 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGenerator.java @@ -130,14 +130,18 @@ public AnalysisSummary createAnalysisSummary(AnalysisDetails analysisDetails) { .withProjectKey(analysisDetails.getAnalysisProjectKey()) .withSummaryImageUrl(baseImageUrl + "/common/icon.png") .withBugCount(issueCounts.get(RuleType.BUG)) + .withBugUrl(getIssuesUrlForRuleType(analysisDetails, RuleType.BUG)) .withBugImageUrl(baseImageUrl + "/common/bug.svg?sanitize=true") .withCodeSmellCount(issueCounts.get(RuleType.CODE_SMELL)) + .withCodeSmellUrl(getIssuesUrlForRuleType(analysisDetails, RuleType.CODE_SMELL)) .withCodeSmellImageUrl(baseImageUrl + "/common/code_smell.svg?sanitize=true") .withCoverage(coverage) .withNewCoverage(newCoverage) + .withCoverageUrl(getComponentMeasuresUrlForCodeMetrics(analysisDetails, CoreMetrics.NEW_COVERAGE_KEY)) .withCoverageImageUrl(createCoverageImage(newCoverage, baseImageUrl)) .withDashboardUrl(getDashboardUrl(analysisDetails)) .withDuplications(duplications) + .withDuplicationsUrl(getComponentMeasuresUrlForCodeMetrics(analysisDetails, CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY)) .withDuplicationsImageUrl(createDuplicateImage(newDuplications, baseImageUrl)) .withNewDuplications(newDuplications) .withFailedQualityGateConditions(failedConditions.stream() @@ -148,12 +152,31 @@ public AnalysisSummary createAnalysisSummary(AnalysisDetails analysisDetails) { ? baseImageUrl + "/checks/QualityGateBadge/passed.svg?sanitize=true" : baseImageUrl + "/checks/QualityGateBadge/failed.svg?sanitize=true") .withTotalIssueCount(issueTotal) - .withVulnerabilityCount(issueCounts.get(RuleType.VULNERABILITY)) .withSecurityHotspotCount(issueCounts.get(RuleType.SECURITY_HOTSPOT)) + .withVulnerabilityCount(issueCounts.get(RuleType.VULNERABILITY)) + .withVulnerabilityUrl(getIssuesUrlForRuleType(analysisDetails, RuleType.VULNERABILITY)) .withVulnerabilityImageUrl(baseImageUrl + "/common/vulnerability.svg?sanitize=true") .build(); } + private String getIssuesUrlForRuleType(AnalysisDetails analysisDetails, RuleType ruleType) { + // https://my-server:port/project/issues?pullRequest=341&resolved=false&types=BUG&inNewCodePeriod=true&id=some-key + return server.getPublicRootUrl() + + "/project/issues?pullRequest=" + analysisDetails.getPullRequestId() + + "&resolved=false&types=" + ruleType.name() + + "&inNewCodePeriod=true" + + "&id=" + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8); + } + + private String getComponentMeasuresUrlForCodeMetrics(AnalysisDetails analysisDetails, String codeMetricsKey) { + // https://my-server:port/component_measures?id=some-key&metric=new_coverage&pullRequest=341&view=list + return server.getPublicRootUrl() + + "/component_measures?id=" + URLEncoder.encode(analysisDetails.getAnalysisProjectKey(), StandardCharsets.UTF_8) + + "&metric=" + codeMetricsKey + + "&pullRequest=" + analysisDetails.getPullRequestId() + + "&view=list"; + } + private String getBaseImageUrl() { return configuration.get(CommunityBranchPlugin.IMAGE_URL_BASE) .orElse(server.getPublicRootUrl() + "/static/communityBranchPlugin") 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 1c6d58e..e22f4dd 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)); } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java index 91e61ad..94e301e 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/CommunityBranchLoaderDelegateTest.java @@ -91,7 +91,7 @@ public void testNoBranchDetailsExistingBranchMatch() { when(branchDto.isMain()).thenReturn(false); BranchDao branchDao = mock(BranchDao.class); - when(branchDao.selectByUuid(any(), any())).thenReturn(Optional.of(branchDto)); + when(branchDao.selectMainBranchByProjectUuid(any(), any())).thenReturn(Optional.of(branchDto)); ScannerReport.Metadata metadata = ScannerReport.Metadata.getDefaultInstance(); when(dbClient.branchDao()).thenReturn(branchDao); @@ -118,7 +118,7 @@ public void testNoBranchDetailsExistingBranchMatch() { verify(dbClient).openSession(anyBoolean()); verifyNoMoreInteractions(dbClient); - verify(branchDao).selectByUuid(any(), any()); + verify(branchDao).selectMainBranchByProjectUuid(any(), any()); verifyNoMoreInteractions(branchDao); } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java index 8b5382f..819da23 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/markup/LinkTest.java @@ -18,28 +18,62 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.markup; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.params.provider.Arguments.arguments; -public class LinkTest { +class LinkTest { - @Test - public void correctParametersReturned() { - Link image = new Link("url", new Text("Text")); - assertThat(image).extracting(Link::getUrl).isEqualTo("url"); - } + private final String testUrl = "url"; + private final Link link = new Link(testUrl, new Text("Text")); - @Test - public void testIsValidChildInvalidChild() { - assertFalse(new Link("url", new Text("Text")).isValidChild(new Paragraph())); - } + @Nested + class Constructor { + @Test + void shouldCorrectlyAssignParameters() { + // given link under test - @Test - public void testIsValidChildValidChildText() { - assertTrue(new Link("url", new Text("Text")).isValidChild(new Text(""))); + // when + String url = link.getUrl(); + + // then + assertThat(url).isEqualTo(testUrl); + } } -} \ No newline at end of file + @Nested + @TestInstance(PER_CLASS) + class IsValid { + + @MethodSource("childNodes") + @ParameterizedTest(name = "child type: {0} => {1}") + void shouldReturnTrueForSupportedChildren(Node child, boolean expectedResult) { + // given link under test and parameters + + // when + boolean validChild = link.isValidChild(child); + + // then + assertThat(validChild).isEqualTo(expectedResult); + } + + private Stream childNodes() { + return Stream.of( + arguments(named(Text.class.getSimpleName(), new Text("")), true), + arguments(named(Image.class.getSimpleName(), new Image("alt", "source")), true), + arguments(named(Paragraph.class.getSimpleName(), new Paragraph()), false), + arguments(named("null", null), false) + ); + } + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java index 552bef1..d8bb0d3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/AnalysisSummaryTest.java @@ -48,13 +48,17 @@ void testCreateAnalysisSummary() { .withSummaryImageUrl("summaryImageUrl") .withProjectKey("projectKey") .withBugCount(911) + .withBugUrl("bugUrl") .withBugImageUrl("bugImageUrl") .withCodeSmellCount(1) .withCoverage(BigDecimal.valueOf(303)) + .withCodeSmellUrl("codeSmellUrl") .withCodeSmellImageUrl("codeSmellImageUrl") + .withCoverageUrl("codeCoverageUrl") .withCoverageImageUrl("codeCoverageImageUrl") .withDashboardUrl("dashboardUrl") .withDuplications(BigDecimal.valueOf(66)) + .withDuplicationsUrl("duplicationsUrl") .withDuplicationsImageUrl("duplicationsImageUrl") .withFailedQualityGateConditions(java.util.List.of("issuea", "issueb", "issuec")) .withNewCoverage(BigDecimal.valueOf(99)) @@ -63,6 +67,7 @@ void testCreateAnalysisSummary() { .withStatusImageUrl("statusImageUrl") .withTotalIssueCount(666) .withVulnerabilityCount(96) + .withVulnerabilityUrl("vulnerabilityUrl") .withVulnerabilityImageUrl("vulnerabilityImageUrl") .build(); @@ -85,25 +90,25 @@ void testCreateAnalysisSummary() { new Heading(2, new Text("666 Issues")), new List(List.Style.BULLET, new ListItem( - new Image("Bug","bugImageUrl"), + new Link("bugUrl", new Image("Bug","bugImageUrl")), new Text(" "), new Text("911 Bugs")), new ListItem( - new Image("Vulnerability","vulnerabilityImageUrl"), + new Link("vulnerabilityUrl", new Image("Vulnerability","vulnerabilityImageUrl")), new Text(" "), new Text("165 Vulnerabilities")), new ListItem( - new Image("Code Smell", "codeSmellImageUrl"), + new Link("codeSmellUrl", new Image("Code Smell", "codeSmellImageUrl")), new Text(" "), new Text("1 Code Smell"))), new Heading(2, new Text("Coverage and Duplications")), new List(List.Style.BULLET, new ListItem( - new Image("Coverage", "codeCoverageImageUrl"), + new Link("codeCoverageUrl", new Image("Coverage", "codeCoverageImageUrl")), new Text(" "), new Text("99.00% Coverage (303.00% Estimated after merge)")), new ListItem( - new Image("Duplications", "duplicationsImageUrl"), + new Link("duplicationsUrl", new Image("Duplications", "duplicationsImageUrl")), new Text(" "), new Text("199.00% Duplicated Code (66.00% Estimated after merge)"))), new Paragraph(new Text("**Project ID:** projectKey")), diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java index 2fcaca3..3ee31f3 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/report/ReportGeneratorTest.java @@ -181,20 +181,25 @@ void shouldProduceCorrectAnalysisSummary(String coverage, String coverageImage, AnalysisSummary expected = AnalysisSummary.builder() .withBugCount(2) + .withBugUrl("http://localhost:9000/project/issues?pullRequest=5&resolved=false&types=BUG&inNewCodePeriod=true&id=projectKey") .withBugImageUrl("http://localhost:9000/static/communityBranchPlugin/common/bug.svg?sanitize=true") .withCoverage(null == coverage ? null : new BigDecimal(coverage)) + .withCoverageUrl("http://localhost:9000/component_measures?id=projectKey&metric=new_coverage&pullRequest=5&view=list") .withCoverageImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/CoverageChart/" + coverageImage) .withNewCoverage(null == coverage ? null : new BigDecimal(coverage)) .withDuplications(null == duplications ? null : new BigDecimal(duplications).setScale(1, RoundingMode.CEILING)) + .withDuplicationsUrl("http://localhost:9000/component_measures?id=projectKey&metric=new_duplicated_lines_density&pullRequest=5&view=list") .withDuplicationsImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/Duplications/" + duplicationsImage) .withNewDuplications(null == duplications ? null : new BigDecimal(duplications)) .withCodeSmellCount(1) + .withCodeSmellUrl("http://localhost:9000/project/issues?pullRequest=5&resolved=false&types=CODE_SMELL&inNewCodePeriod=true&id=projectKey") .withCodeSmellImageUrl("http://localhost:9000/static/communityBranchPlugin/common/code_smell.svg?sanitize=true") .withDashboardUrl("http://localhost:9000/dashboard?id=projectKey&pullRequest=5") .withProjectKey("projectKey") .withSummaryImageUrl("http://localhost:9000/static/communityBranchPlugin/common/icon.png") .withSecurityHotspotCount(1) .withVulnerabilityCount(1) + .withVulnerabilityUrl("http://localhost:9000/project/issues?pullRequest=5&resolved=false&types=VULNERABILITY&inNewCodePeriod=true&id=projectKey") .withVulnerabilityImageUrl("http://localhost:9000/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true") .withStatusDescription("Failed") .withStatusImageUrl("http://localhost:9000/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true")