From 980e86138775014533996d93ec9694b122cdec83 Mon Sep 17 00:00:00 2001 From: Lex Felix Date: Fri, 10 Jul 2020 14:52:09 +0200 Subject: [PATCH] Feature: #135 / #96 comments filter and limiter. implemented a filter system with 3 filters (maxamount,severy,type) added commentfilter to the githubpullrequest decorator fixed the tests I broke with adding the filter added test to verify the filter is called for the Github Graphql wrote test for IssueFilterRunner. Added test for TypeExclusionFilter and SeverityExclusionFilter Added tests for TypeComparator Added tests for SeverityComparator Added default comparators for IssueFilterRunner for real build. --- .../plugin/CommunityBranchPlugin.java | 74 ++++--- .../PullRequestBuildStatusDecorator.java | 6 +- .../PullRequestPostAnalysisTask.java | 75 +++++-- .../BitbucketPullRequestDecorator.java | 6 + .../commentfilter/IssueFilterRunner.java | 60 ++++++ .../commentfilter/SeverityComparator.java | 13 ++ .../SeverityExclusionFilter.java | 36 ++++ .../commentfilter/TypeComparator.java | 12 ++ .../commentfilter/TypeExclusionFilter.java | 37 ++++ .../pullrequest/github/CheckRunProvider.java | 3 +- .../github/GithubPullRequestDecorator.java | 10 +- .../github/v4/GraphqlCheckRunProvider.java | 6 +- .../GitlabServerPullRequestDecorator.java | 12 +- .../plugin/CommunityBranchPluginTest.java | 2 +- .../PullRequestPostAnalysisTaskTest.java | 184 +++++++++++++++++- .../commentfilter/IssueFilterRunnerTest.java | 84 ++++++++ .../commentfilter/SeverityComparatorTest.java | 46 +++++ .../SeverityExclusionFilterTest.java | 35 ++++ .../commentfilter/TypeComparatorTest.java | 45 +++++ .../TypeExclusionFilterTest.java | 37 ++++ .../GithubPullRequestDecoratorTest.java | 14 +- .../v4/GraphqlCheckRunProviderTest.java | 45 +++-- .../GitlabServerPullRequestDecoratorTest.java | 123 +++++++++++- 23 files changed, 885 insertions(+), 80 deletions(-) create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunner.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparator.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilter.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparator.java create mode 100644 src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilter.java create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunnerTest.java create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparatorTest.java create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilterTest.java create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparatorTest.java create mode 100644 src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilterTest.java diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java index b6ed8c3d..3f112c28 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPlugin.java @@ -20,6 +20,8 @@ import com.github.mc1arke.sonarqube.plugin.ce.CommunityBranchEditionProvider; import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestPostAnalysisTask; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.GitlabServerPullRequestDecorator; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchParamsValidator; import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectBranchesLoader; @@ -52,16 +54,21 @@ import org.sonar.api.SonarQubeSide; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.resources.Qualifiers; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; import org.sonar.core.config.PurgeConstants; import org.sonar.core.extension.CoreExtension; +import java.util.Arrays; + /** * @author Michael Clarke */ public class CommunityBranchPlugin implements Plugin, CoreExtension { public static final String IMAGE_URL_BASE = "com.github.mc1arke.sonarqube.plugin.branch.image-url-base"; - + private static final String PULL_REQUEST_CATEGORY_LABEL = "Pull Request Decoration Filters"; + private static final String PULL_REQUEST_DECORATIONS_LABEL = "Filters"; @Override public String getName() { return "Community Branch Plugin"; @@ -74,46 +81,57 @@ public void load(CoreExtension.Context context) { } else if (SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class, - AlmSettingsWs.class, CountBindingAction.class, DeleteAction.class, - DeleteBindingAction.class, ListAction.class, ListDefinitionsAction.class, - GetBindingAction.class, + AlmSettingsWs.class, CountBindingAction.class, DeleteAction.class, + DeleteBindingAction.class, ListAction.class, ListDefinitionsAction.class, + GetBindingAction.class, - CreateGithubAction.class, SetGithubBindingAction.class, UpdateGithubAction.class, + CreateGithubAction.class, SetGithubBindingAction.class, UpdateGithubAction.class, - CreateAzureAction.class, SetAzureBindingAction.class, UpdateAzureAction.class, + CreateAzureAction.class, SetAzureBindingAction.class, UpdateAzureAction.class, - CreateBitbucketAction.class, SetBitbucketBindingAction.class, - UpdateBitbucketAction.class, + CreateBitbucketAction.class, SetBitbucketBindingAction.class, + UpdateBitbucketAction.class, - CreateGitlabAction.class, SetGitlabBindingAction.class, UpdateGitlabAction.class, + CreateGitlabAction.class, SetGitlabBindingAction.class, UpdateGitlabAction.class, /* org.sonar.db.purge.PurgeConfiguration uses the value for the this property if it's configured, so it only needs to be specified here, but doesn't need any additional classes to perform the relevant purge/cleanup */ - PropertyDefinition - .builder(PurgeConstants.DAYS_BEFORE_DELETING_INACTIVE_BRANCHES_AND_PRS) - .name("Number of days before purging inactive short living branches") - .description( - "Short living branches are permanently deleted when there are no analysis for the configured number of days.") - .category(CoreProperties.CATEGORY_HOUSEKEEPING) - .subCategory(CoreProperties.SUBCATEGORY_GENERAL).defaultValue("30") - .type(PropertyType.INTEGER).build() + PropertyDefinition + .builder(PurgeConstants.DAYS_BEFORE_DELETING_INACTIVE_BRANCHES_AND_PRS) + .name("Number of days before purging inactive short living branches") + .description( + "Short living branches are permanently deleted when there are no analysis for the configured number of days.") + .category(CoreProperties.CATEGORY_HOUSEKEEPING) + .subCategory(CoreProperties.SUBCATEGORY_GENERAL).defaultValue("30") + .type(PropertyType.INTEGER).build() - ); + ); } if (SonarQubeSide.COMPUTE_ENGINE == context.getRuntime().getSonarQubeSide() || - SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { + SonarQubeSide.SERVER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(PropertyDefinition.builder(IMAGE_URL_BASE) - .category(CoreProperties.CATEGORY_GENERAL) - .subCategory(CoreProperties.SUBCATEGORY_GENERAL) - .onQualifiers(Qualifiers.APP) - .name("Images base URL") - .description("Base URL used to load the images for the PR comments (please use this only if images are not displayed properly).") - .type(PropertyType.STRING) - .build()); + .category(CoreProperties.CATEGORY_GENERAL) + .subCategory(CoreProperties.SUBCATEGORY_GENERAL) + .onQualifiers(Qualifiers.APP) + .name("Images base URL") + .description("Base URL used to load the images for the PR comments (please use this only if images are not displayed properly).") + .type(PropertyType.STRING) + .build(), + PropertyDefinition.builder(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_TYPE_EXCLUSION).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(PULL_REQUEST_DECORATIONS_LABEL) + .onQualifiers(Qualifiers.PROJECT).name("RuleType Exclusions").description("Comma-separated list of ruletypes you want to exclude, possible values: CODE_SMELL, BUG, VULNERABILITY, SECURITY_HOTSPOT") + .type(PropertyType.STRING).options(RuleType.BUG.name(), RuleType.CODE_SMELL.name(), RuleType.VULNERABILITY.name(),RuleType.SECURITY_HOTSPOT.name()).build(), + PropertyDefinition.builder(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_SEVERITY_EXCLUSION).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(PULL_REQUEST_DECORATIONS_LABEL) + .onQualifiers(Qualifiers.PROJECT).name("Severity Exclusions").description("Comma-separated list of severity levels you want to exclude, possible values: INFO, MINOR, MAJOR, CRITICAL, BLOCKER") + .type(PropertyType.STRING).options(Severity.ALL).build(), + PropertyDefinition.builder(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_MAXAMOUNT).category(PULL_REQUEST_CATEGORY_LABEL).subCategory(PULL_REQUEST_DECORATIONS_LABEL) + .onQualifiers(Qualifiers.PROJECT).name("Max amount").description("Max amount of comments to be added to the pull request, must be > 0") + .type(PropertyType.INTEGER).build() + + ); } } @@ -122,8 +140,8 @@ public void load(CoreExtension.Context context) { public void define(Plugin.Context context) { if (SonarQubeSide.SCANNER == context.getRuntime().getSonarQubeSide()) { context.addExtensions(CommunityProjectBranchesLoader.class, CommunityProjectPullRequestsLoader.class, - CommunityBranchConfigurationLoader.class, CommunityBranchParamsValidator.class, - ScannerPullRequestPropertySensor.class); + CommunityBranchConfigurationLoader.class, CommunityBranchParamsValidator.class, + ScannerPullRequestPropertySensor.class); } } } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java index 915e4696..8748f01d 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestBuildStatusDecorator.java @@ -18,6 +18,7 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; @@ -25,7 +26,10 @@ public interface PullRequestBuildStatusDecorator { DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto); + ProjectAlmSettingDto projectAlmSettingDto); + + DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, + ProjectAlmSettingDto projectAlmSettingDto, IssueFilterRunner issueFilterRunner); ALM alm(); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java index f73a0061..6942c5f6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTask.java @@ -18,10 +18,11 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; -import org.sonar.api.ce.posttask.Analysis; -import org.sonar.api.ce.posttask.Branch; -import org.sonar.api.ce.posttask.PostProjectAnalysisTask; -import org.sonar.api.ce.posttask.QualityGate; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.SeverityExclusionFilter; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.TypeExclusionFilter; +import org.checkerframework.checker.nullness.Opt; +import org.sonar.api.ce.posttask.*; import org.sonar.api.config.Configuration; import org.sonar.api.platform.Server; import org.sonar.api.utils.log.Logger; @@ -36,14 +37,20 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto; import org.sonar.db.component.BranchDao; import org.sonar.db.component.BranchDto; +import org.sonar.db.property.PropertyDto; import org.sonar.db.protobuf.DbProjectBranches; +import org.sonar.server.setting.ws.Setting; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Stream; public class PullRequestPostAnalysisTask implements PostProjectAnalysisTask { private static final Logger LOGGER = Loggers.get(PullRequestPostAnalysisTask.class); + public static final String PULLREQUEST_FILTER_SEVERITY_EXCLUSION = "sonar.pullrequest.comment.filter.severity.exclusions"; + public static final String PULLREQUEST_FILTER_TYPE_EXCLUSION = "sonar.pullrequest.comment.filter.type.exclusions"; + public static final String PULLREQUEST_FILTER_MAXAMOUNT = "sonar.pullrequest.comment.filter.maxamount"; private final List pullRequestDecorators; private final Server server; @@ -78,6 +85,7 @@ public String getDescription() { @Override public void finished(Context context) { ProjectAnalysis projectAnalysis = context.getProjectAnalysis(); + LOGGER.debug("found " + pullRequestDecorators.size() + " pull request decorators"); Optional optionalPullRequest = projectAnalysis.getBranch().filter(branch -> Branch.Type.PULL_REQUEST == branch.getType()); @@ -94,11 +102,14 @@ public void finished(Context context) { ProjectAlmSettingDto projectAlmSettingDto; Optional optionalAlmSettingDto; + List projectProperties; try (DbSession dbSession = dbClient.openSession(false)) { Optional optionalProjectAlmSettingDto = dbClient.projectAlmSettingDao().selectByProject(dbSession, projectAnalysis.getProject().getUuid()); + projectProperties = dbClient.propertiesDao().selectProjectProperties(dbSession, projectAnalysis.getProject().getKey()); + if (!optionalProjectAlmSettingDto.isPresent()) { LOGGER.debug("No ALM has been set on the current project"); return; @@ -145,23 +156,63 @@ public void finished(Context context) { return; } + String commitId = revision.get(); AnalysisDetails analysisDetails = new AnalysisDetails(new AnalysisDetails.BranchDetails(optionalBranchName.get(), commitId), - postAnalysisIssueVisitor, qualityGate, - new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, - treeRootHolder), analysis, - projectAnalysis.getProject(), configuration, server.getPublicRootUrl(), - projectAnalysis.getScannerContext()); + postAnalysisIssueVisitor, qualityGate, + new AnalysisDetails.MeasuresHolder(metricRepository, measureRepository, + treeRootHolder), analysis, + projectAnalysis.getProject(), configuration, server.getPublicRootUrl(), + projectAnalysis.getScannerContext()); PullRequestBuildStatusDecorator pullRequestDecorator = optionalPullRequestDecorator.get(); LOGGER.info("using pull request decorator " + pullRequestDecorator.alm().getId()); - DecorationResult decorationResult = pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); + + Optional optionalIssueFilterRunner = getIssueFilterList(projectProperties); + + DecorationResult decorationResult; + if (optionalIssueFilterRunner.isPresent()) + decorationResult = pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, optionalIssueFilterRunner.get()); + else + decorationResult = pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); decorationResult.getPullRequestUrl().ifPresent(pullRequestUrl -> persistPullRequestUrl(pullRequestUrl, projectAnalysis, optionalBranchName.get())); } + private Optional getIssueFilterList(List projectProperties) { + List> filterList = new ArrayList<>(); + + + Optional optionalSeverityExclusion = projectProperties.stream().filter(propertyDto -> propertyDto.getKey().equals(PULLREQUEST_FILTER_SEVERITY_EXCLUSION)) + .map(PropertyDto::getValue) + .filter(Objects::nonNull) + .findAny(); + Optional optionalTypeExclusion = projectProperties.stream().filter(propertyDto -> propertyDto.getKey().equals(PULLREQUEST_FILTER_TYPE_EXCLUSION)) + .map(PropertyDto::getValue) + .filter(Objects::nonNull) + .findAny(); + Optional optionalMaxAmount = projectProperties.stream().filter(propertyDto -> propertyDto.getKey().equals(PULLREQUEST_FILTER_MAXAMOUNT)) + .map(PropertyDto::getValue) + .filter(Objects::nonNull) + .map(Integer::parseInt) + .findAny(); + + optionalSeverityExclusion = Optional.ofNullable(optionalSeverityExclusion.orElseGet(() -> configuration.get(PULLREQUEST_FILTER_SEVERITY_EXCLUSION).orElse(null))); + optionalTypeExclusion = Optional.ofNullable(optionalTypeExclusion.orElseGet(() -> configuration.get(PULLREQUEST_FILTER_TYPE_EXCLUSION).orElse(null))); + optionalMaxAmount = Optional.ofNullable(optionalMaxAmount.orElseGet(() -> configuration.getInt(PULLREQUEST_FILTER_MAXAMOUNT).orElse(null))); + + optionalSeverityExclusion.ifPresent(severityString -> filterList.add(new SeverityExclusionFilter(severityString))); + optionalTypeExclusion.ifPresent(typeString -> filterList.add(new TypeExclusionFilter(typeString))); + + if (filterList.isEmpty() && !optionalMaxAmount.isPresent()) { + return Optional.empty(); + } else { + return Optional.of(new IssueFilterRunner(filterList, optionalMaxAmount.orElse(null))); + } + } + private static Optional findCurrentPullRequestStatusDecorator( AlmSettingDto almSetting, List pullRequestDecorators) { diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java index 043e6178..73840b27 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/bitbucket/BitbucketPullRequestDecorator.java @@ -30,6 +30,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.DataValue; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.bitbucket.client.model.ReportData; import com.google.common.annotations.VisibleForTesting; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.issue.Issue; import org.sonar.api.measures.CoreMetrics; @@ -70,6 +71,11 @@ public class BitbucketPullRequestDecorator implements PullRequestBuildStatusDeco @Override public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { + return decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, null); + } + + @Override + public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto, IssueFilterRunner issueFilterRunner) { String project = projectAlmSettingDto.getAlmRepo(); String repo = projectAlmSettingDto.getAlmSlug(); String url = almSettingDto.getUrl(); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunner.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunner.java new file mode 100644 index 00000000..cd892a38 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunner.java @@ -0,0 +1,60 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; + +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class IssueFilterRunner { + private final List> filters; + private Integer maxAmountOfIssues; + private final SeverityComparator severityComparator; + private final TypeComparator typeComparator; + + public IssueFilterRunner(List> filters, Integer maxAmountOfIssues) { + this.filters = filters; + this.maxAmountOfIssues = maxAmountOfIssues; + this.severityComparator = new SeverityComparator(); + this.typeComparator = new TypeComparator(); + } + + public IssueFilterRunner( + List> filters, + SeverityComparator severityComparator, + TypeComparator typeComparator) { + this.filters = filters; + this.severityComparator = severityComparator; + this.typeComparator = typeComparator; + } + + public IssueFilterRunner( + List> filters, Integer maxAmountOfIssues, + SeverityComparator severityComparator, + TypeComparator typeComparator) { + this(filters, severityComparator, typeComparator); + this.maxAmountOfIssues = maxAmountOfIssues; + } + + public List filterIssues(List issues) { + Stream stream = issues.stream() + .filter(filters.stream() + .reduce(issue -> true, + Predicate::and)) + .sorted(severityComparator.thenComparing(typeComparator)); + + if (maxAmountOfIssues != null && maxAmountOfIssues > 0) stream = stream.limit(maxAmountOfIssues); + + return Collections.unmodifiableList(stream.collect(Collectors.toList())); + } + + public List> getFilters() { + return filters; + } + + public Integer getMaxAmountOfIssues() { + return maxAmountOfIssues; + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparator.java new file mode 100644 index 00000000..e6323f4a --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparator.java @@ -0,0 +1,13 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.sonar.api.rule.Severity; + +import java.util.Comparator; + +public class SeverityComparator implements Comparator { + @Override + public int compare(PostAnalysisIssueVisitor.ComponentIssue o1, PostAnalysisIssueVisitor.ComponentIssue o2) { + return Severity.ALL.indexOf(o2.getIssue().severity()) - Severity.ALL.indexOf(o1.getIssue().severity()); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilter.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilter.java new file mode 100644 index 00000000..33dc303b --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilter.java @@ -0,0 +1,36 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.google.common.base.Preconditions; +import org.sonar.api.rule.Severity; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class SeverityExclusionFilter implements Predicate { + + private final List exclusions; + + public SeverityExclusionFilter(String severityString) { + this.exclusions = parseString(severityString); + } + + private List parseString(String severityString) { + List severityStringList = Arrays.asList(severityString.split(",")); + + return Collections.unmodifiableList(severityStringList.stream() + .map(String::trim) + .map(String::toUpperCase) + .filter(Severity.ALL::contains) + .collect(Collectors.toList())); + } + + @Override + public boolean test(PostAnalysisIssueVisitor.ComponentIssue componentIssue) { + return !exclusions.contains(componentIssue.getIssue().severity()); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparator.java new file mode 100644 index 00000000..cc9527ae --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparator.java @@ -0,0 +1,12 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; + +import java.util.Comparator; + +public class TypeComparator implements Comparator { + @Override + public int compare(PostAnalysisIssueVisitor.ComponentIssue o1, PostAnalysisIssueVisitor.ComponentIssue o2) { + return o2.getIssue().type().getDbConstant() - o1.getIssue().type().getDbConstant(); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilter.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilter.java new file mode 100644 index 00000000..202bcd91 --- /dev/null +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilter.java @@ -0,0 +1,37 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class TypeExclusionFilter implements Predicate { + + private final List exclusions; + + public TypeExclusionFilter(String typeExclusionString) { + this.exclusions = parseString(typeExclusionString); + } + + private List parseString(String typeExclusionString) { + List typeExclusionStringList = Arrays.asList(typeExclusionString.split(",")); + + return Collections.unmodifiableList(typeExclusionStringList.stream() + .map(String::trim) + .map(String::toUpperCase) + .filter(string -> + RuleType.names().contains(string)) + .map(RuleType::valueOf) + .collect(Collectors.toList())); + } + + @Override + public boolean test(PostAnalysisIssueVisitor.ComponentIssue componentIssue) { + return !exclusions.contains(componentIssue.getIssue().type()); + } +} diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java index 900c13ba..81918dae 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/CheckRunProvider.java @@ -20,6 +20,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; @@ -28,5 +29,5 @@ public interface CheckRunProvider { DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto) throws IOException, GeneralSecurityException; + ProjectAlmSettingDto projectAlmSettingDto, IssueFilterRunner issueFilterRunner) throws IOException, GeneralSecurityException; } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java index 3db92bd6..f6dfcdd6 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecorator.java @@ -21,6 +21,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import org.sonar.db.alm.setting.ALM; import org.sonar.db.alm.setting.AlmSettingDto; import org.sonar.db.alm.setting.ProjectAlmSettingDto; @@ -33,11 +34,16 @@ public GithubPullRequestDecorator(CheckRunProvider checkRunProvider) { this.checkRunProvider = checkRunProvider; } + @Override + public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { + return decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, null); + } + @Override public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto) { + ProjectAlmSettingDto projectAlmSettingDto, IssueFilterRunner issueFilterRunner) { try { - return checkRunProvider.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); + return checkRunProvider.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto, issueFilterRunner); } catch (Exception ex) { throw new IllegalStateException("Could not decorate Pull Request on Github", ex); } diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java index 42e0c268..544214d4 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProvider.java @@ -21,6 +21,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.CheckRunProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubApplicationAuthenticationProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.RepositoryAuthenticationToken; @@ -96,7 +97,7 @@ public GraphqlCheckRunProvider(Clock clock, @Override public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto) throws IOException, GeneralSecurityException { + ProjectAlmSettingDto projectAlmSettingDto, IssueFilterRunner issueFilterRunner) throws IOException, GeneralSecurityException { String apiUrl = Optional.ofNullable(almSettingDto.getUrl()).orElseThrow(() -> new IllegalArgumentException("No URL has been set for Github connections")); String apiPrivateKey = Optional.ofNullable(almSettingDto.getPrivateKey()).orElseThrow(() -> new IllegalArgumentException("No private key has been set for Github connections")); String projectPath = Optional.ofNullable(projectAlmSettingDto.getAlmRepo()).orElseThrow(() -> new IllegalArgumentException("No repository name has been set for Github connections")); @@ -108,8 +109,9 @@ public DecorationResult createCheckRun(AnalysisDetails analysisDetails, AlmSetti headers.put("Authorization", "Bearer " + repositoryAuthenticationToken.getAuthenticationToken()); headers.put("Accept", "application/vnd.github.antiope-preview+json"); - List issues = analysisDetails.getPostAnalysisIssueVisitor().getIssues(); + if(issueFilterRunner != null) + issues = issueFilterRunner.filterIssues(issues); List> annotations = createAnnotations(issues); diff --git a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java index 7864f135..944d5e10 100644 --- a/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java +++ b/src/main/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecorator.java @@ -25,6 +25,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PullRequestBuildStatusDecorator; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Commit; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.Discussion; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.gitlab.response.MergeRequest; @@ -96,9 +97,14 @@ public GitlabServerPullRequestDecorator(Server server, ScmInfoRepository scmInfo this.scmInfoRepository = scmInfoRepository; } + @Override + public DecorationResult decorateQualityGateStatus(AnalysisDetails analysisDetails, AlmSettingDto almSettingDto, ProjectAlmSettingDto projectAlmSettingDto) { + return decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, null); + } + @Override public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, AlmSettingDto almSettingDto, - ProjectAlmSettingDto projectAlmSettingDto) { + ProjectAlmSettingDto projectAlmSettingDto, IssueFilterRunner issueFilterRunner) { LOGGER.info("starting to analyze with " + analysis.toString()); String revision = analysis.getCommitSha(); @@ -164,6 +170,10 @@ public DecorationResult decorateQualityGateStatus(AnalysisDetails analysis, AlmS List openIssues = analysis.getPostAnalysisIssueVisitor().getIssues().stream().filter(i -> OPEN_ISSUE_STATUSES.contains(i.getIssue().getStatus())).collect(Collectors.toList()); + if(issueFilterRunner != null) + openIssues = issueFilterRunner.filterIssues(openIssues); + + String summaryComment = analysis.createAnalysisSummary(new MarkdownFormatterFactory()); List summaryContentParams = Collections .singletonList(new BasicNameValuePair("body", summaryComment)); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java index ad56f8cc..4b55ff1f 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/CommunityBranchPluginTest.java @@ -119,7 +119,7 @@ public void testServerSideLoad() { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); verify(context, times(2)).addExtensions(argumentCaptor.capture(), argumentCaptor.capture()); - assertEquals(23, argumentCaptor.getAllValues().size()); + assertEquals(26, argumentCaptor.getAllValues().size()); assertEquals(Arrays.asList(CommunityBranchFeatureExtension.class, CommunityBranchSupportDelegate.class), argumentCaptor.getAllValues().subList(0, 2)); diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java index afa13c7c..4316bdf2 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/PullRequestPostAnalysisTaskTest.java @@ -18,6 +18,7 @@ */ package com.github.mc1arke.sonarqube.plugin.ce.pullrequest; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -41,6 +42,8 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto; import org.sonar.db.component.BranchDao; import org.sonar.db.component.BranchDto; +import org.sonar.db.property.PropertiesDao; +import org.sonar.db.property.PropertyDto; import org.sonar.db.protobuf.DbProjectBranches; import java.util.ArrayList; @@ -91,8 +94,7 @@ public void init() { doReturn(projectAnalysis).when(context).getProjectAnalysis(); doReturn(project).when(projectAnalysis).getProject(); doReturn("uuid").when(project).getUuid(); - - + doReturn("PRJ").when(project).getKey(); } @Test @@ -138,6 +140,11 @@ public void testFinishedNoProviderSet() { doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + testCase.finished(context); verify(projectAnalysis, never()).getAnalysis(); @@ -172,6 +179,11 @@ public void testFinishedNoProviderMatchingName() { when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + testCase.finished(context); verify(decorator1).alm(); @@ -209,11 +221,15 @@ public void testFinishedNoAnalysis() { ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); testCase.finished(context); verify(projectAnalysis).getAnalysis(); - verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any()); + verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any(), any()); } @@ -250,11 +266,16 @@ public void testFinishedAnalysisWithNoRevision() { when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + testCase.finished(context); verify(projectAnalysis).getAnalysis(); verify(projectAnalysis, never()).getQualityGate(); - verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any()); + verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any(), any()); } @Test @@ -293,11 +314,16 @@ public void testFinishedAnalysisWithNoQualityGate() { doReturn(ALM.GITHUB).when(decorator2).alm(); pullRequestBuildStatusDecorators.add(decorator2); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + testCase.finished(context); verify(projectAnalysis).getAnalysis(); verify(projectAnalysis).getQualityGate(); - verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any()); + verify(decorator2, never()).decorateQualityGateStatus(any(), any(), any(), any()); } @Test @@ -336,6 +362,11 @@ public void testFinishedAnalysisDecorationRequest() { ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + DbSession dbSession = mock(DbSession.class); doReturn(dbSession).when(dbClient).openSession(anyBoolean()); @@ -411,6 +442,11 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkSaved() { doReturn(projectAlmSettingDao).when(dbClient).projectAlmSettingDao(); doReturn(almSettingDao).when(dbClient).almSettingDao(); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + testCase.finished(context); ArgumentCaptor analysisDetailsArgumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); @@ -487,6 +523,11 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkNotSavedIfBranch doReturn(Optional.empty()).when(branchDao).selectByPullRequestKey(any(), any(), any()); doReturn(DbProjectBranches.PullRequestData.newBuilder().build()).when(branchDto).getPullRequestData(); + List projectProperties = new ArrayList<>(); + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + testCase.finished(context); ArgumentCaptor analysisDetailsArgumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); @@ -522,4 +563,137 @@ public void testFinishedAnalysisDecorationRequestPullRequestLinkNotSavedIfBranch public void testCorrectDescriptionReturnedForTask() { assertThat(testCase.getDescription()).isEqualTo("Pull Request Decoration"); } + + @Test + public void testIssueFilterRunnerIsFilledFromConfiguration(){ + doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); + doReturn(Optional.of("pull-request")).when(branch).getName(); + + Analysis analysis = mock(Analysis.class); + doReturn(Optional.of("revision")).when(analysis).getRevision(); + doReturn(Optional.of(analysis)).when(projectAnalysis).getAnalysis(); + + QualityGate qualityGate = mock(QualityGate.class); + doReturn(qualityGate).when(projectAnalysis).getQualityGate(); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITLAB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + + PullRequestBuildStatusDecorator decorator1 = mock(PullRequestBuildStatusDecorator.class); + doReturn(ALM.GITLAB).when(decorator1).alm(); + doReturn(DecorationResult.builder().build()).when(decorator1).decorateQualityGateStatus(any(), any(), any(),any()); + pullRequestBuildStatusDecorators.add(decorator1); + + PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); + doReturn(ALM.GITHUB).when(decorator2).alm(); + pullRequestBuildStatusDecorators.add(decorator2); + + List projectProperties = new ArrayList<>(); + PropertyDto severityExclusionProperty = mock(PropertyDto.class); + when(severityExclusionProperty.getKey()).thenReturn(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_SEVERITY_EXCLUSION); + when(severityExclusionProperty.getValue()).thenReturn("INFO,MAJOR"); + PropertyDto typeExclusionProperty = mock(PropertyDto.class); + when(typeExclusionProperty.getKey()).thenReturn(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_TYPE_EXCLUSION); + when(typeExclusionProperty.getValue()).thenReturn("CODE_SMELL"); + PropertyDto maxAmountOfIssuesProperty = mock(PropertyDto.class); + when(maxAmountOfIssuesProperty.getKey()).thenReturn(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_MAXAMOUNT); + when(maxAmountOfIssuesProperty.getValue()).thenReturn("10"); + projectProperties.add(severityExclusionProperty); + projectProperties.add(typeExclusionProperty); + projectProperties.add(maxAmountOfIssuesProperty); + + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + + testCase.finished(context); + + ArgumentCaptor filterRunnerArgumentCaptor = ArgumentCaptor.forClass(IssueFilterRunner.class); + + verify(decorator1).decorateQualityGateStatus(any(), any(), any(), filterRunnerArgumentCaptor.capture()); + IssueFilterRunner generatedIssueFilterRunner = filterRunnerArgumentCaptor.getValue(); + assertThat(generatedIssueFilterRunner.getFilters().size()).isEqualTo(2); + assertThat(generatedIssueFilterRunner.getMaxAmountOfIssues()).isEqualTo(10); + } + @Test + public void testIssueFilterRunnerIsFilledFromProjectSettings(){ + doReturn(Branch.Type.PULL_REQUEST).when(branch).getType(); + doReturn(Optional.of("pull-request")).when(branch).getName(); + + Analysis analysis = mock(Analysis.class); + doReturn(Optional.of("revision")).when(analysis).getRevision(); + doReturn(Optional.of(analysis)).when(projectAnalysis).getAnalysis(); + + QualityGate qualityGate = mock(QualityGate.class); + doReturn(qualityGate).when(projectAnalysis).getQualityGate(); + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + when(projectAlmSettingDto.getAlmSlug()).thenReturn("dummy/repo"); + when(projectAlmSettingDto.getAlmSettingUuid()).thenReturn("almUuid"); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getUrl()).thenReturn("http://host.name"); + when(almSettingDto.getAppId()).thenReturn("app id"); + when(almSettingDto.getPrivateKey()).thenReturn("private key"); + when(almSettingDto.getAlm()).thenReturn(ALM.GITLAB); + when(dbClient.openSession(anyBoolean())).thenReturn(mock(DbSession.class)); + AlmSettingDao almSettingDao = mock(AlmSettingDao.class); + when(almSettingDao.selectByUuid(any(), any())).thenReturn(Optional.of(almSettingDto)); + when(dbClient.almSettingDao()).thenReturn(almSettingDao); + ProjectAlmSettingDao projectAlmSettingDao = mock(ProjectAlmSettingDao.class); + when(projectAlmSettingDao.selectByProject(any(), anyString())).thenReturn(Optional.of(projectAlmSettingDto)); + when(dbClient.projectAlmSettingDao()).thenReturn(projectAlmSettingDao); + + ScannerContext scannerContext = mock(ScannerContext.class); + doReturn(scannerContext).when(projectAnalysis).getScannerContext(); + + PullRequestBuildStatusDecorator decorator1 = mock(PullRequestBuildStatusDecorator.class); + doReturn(ALM.GITLAB).when(decorator1).alm(); + doReturn(DecorationResult.builder().build()).when(decorator1).decorateQualityGateStatus(any(), any(), any(),any()); + pullRequestBuildStatusDecorators.add(decorator1); + + PullRequestBuildStatusDecorator decorator2 = mock(PullRequestBuildStatusDecorator.class); + doReturn(ALM.GITHUB).when(decorator2).alm(); + pullRequestBuildStatusDecorators.add(decorator2); + + when(configuration.getInt(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_MAXAMOUNT)).thenReturn(Optional.of(5)); + + List projectProperties = new ArrayList<>(); + PropertyDto severityExclusionProperty = mock(PropertyDto.class); + when(severityExclusionProperty.getKey()).thenReturn(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_SEVERITY_EXCLUSION); + when(severityExclusionProperty.getValue()).thenReturn("INFO,MAJOR"); + PropertyDto typeExclusionProperty = mock(PropertyDto.class); + when(typeExclusionProperty.getKey()).thenReturn(PullRequestPostAnalysisTask.PULLREQUEST_FILTER_TYPE_EXCLUSION); + when(typeExclusionProperty.getValue()).thenReturn("CODE_SMELL"); + projectProperties.add(severityExclusionProperty); + projectProperties.add(typeExclusionProperty); + + PropertiesDao propertiesDao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(propertiesDao); + when(propertiesDao.selectProjectProperties(any(), any())).thenReturn(projectProperties); + + testCase.finished(context); + + ArgumentCaptor filterRunnerArgumentCaptor = ArgumentCaptor.forClass(IssueFilterRunner.class); + + verify(decorator1).decorateQualityGateStatus(any(), any(), any(), filterRunnerArgumentCaptor.capture()); + IssueFilterRunner generatedIssueFilterRunner = filterRunnerArgumentCaptor.getValue(); + assertThat(generatedIssueFilterRunner.getFilters().size()).isEqualTo(2); + assertThat(generatedIssueFilterRunner.getMaxAmountOfIssues()).isEqualTo(5); + } } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunnerTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunnerTest.java new file mode 100644 index 00000000..dbb13f66 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/IssueFilterRunnerTest.java @@ -0,0 +1,84 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class IssueFilterRunnerTest { + Predicate filter1; + Predicate filter2; + List> filterList; + SeverityComparator severityComparator; + TypeComparator typeComparator; + + + @Before + public void setup() { + filter1 = mock(SeverityExclusionFilter.class); + when(filter1.and(any())).thenCallRealMethod(); + when(filter1.test(any())).thenReturn(true); + filter2 = mock(TypeExclusionFilter.class); + when(filter2.test(any())).thenReturn(true); + filterList = Arrays.asList(filter1, filter2); + + severityComparator = mock(SeverityComparator.class); + when(severityComparator.compare(any(), any())).thenReturn(0); + when(severityComparator.thenComparing(any(TypeComparator.class))).thenReturn(severityComparator); + typeComparator = mock(TypeComparator.class); + when(typeComparator.compare(any(), any())).thenReturn(0); + } + + + @Test + public void testFilterIssuesWithoutMaxAmountOfIssues() { + IssueFilterRunner issueFilterRunner = new IssueFilterRunner(filterList, severityComparator, typeComparator); + List componentIssues = Arrays.asList( + mock(PostAnalysisIssueVisitor.ComponentIssue.class), + mock(PostAnalysisIssueVisitor.ComponentIssue.class)); + + List filteredComponentIssues = issueFilterRunner.filterIssues( + componentIssues); + + verify(filter1, times(2)).test(any()); + verify(filter2, times(2)).test(any()); + assertEquals(2, filteredComponentIssues.size()); + } + + @Test + public void testFilterIssuesWithMaxAmountOfIssuesOfZero() { + IssueFilterRunner issueFilterRunner = new IssueFilterRunner(filterList, 0, severityComparator, typeComparator); + List componentIssues = Arrays.asList( + mock(PostAnalysisIssueVisitor.ComponentIssue.class), + mock(PostAnalysisIssueVisitor.ComponentIssue.class)); + + List filteredComponentIssues = issueFilterRunner.filterIssues( + componentIssues); + + verify(filter1, times(2)).test(any()); + verify(filter2, times(2)).test(any()); + assertEquals(2, filteredComponentIssues.size()); + } + + @Test + public void testFilterIssuesWithMaxAmountOfIssuesOfOne() { + IssueFilterRunner issueFilterRunner = new IssueFilterRunner(filterList, 1, severityComparator, typeComparator); + List componentIssues = Arrays.asList( + mock(PostAnalysisIssueVisitor.ComponentIssue.class), + mock(PostAnalysisIssueVisitor.ComponentIssue.class)); + + List filteredComponentIssues = issueFilterRunner.filterIssues( + componentIssues); + + verify(filter1, times(2)).test(any()); + verify(filter2, times(2)).test(any()); + assertEquals(1, filteredComponentIssues.size()); + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparatorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparatorTest.java new file mode 100644 index 00000000..4b20f22a --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityComparatorTest.java @@ -0,0 +1,46 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.junit.Test; +import org.sonar.api.rules.RuleType; +import org.sonar.core.issue.DefaultIssue; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SeverityComparatorTest { + + @Test + public void whenFirstValueLowerThenDifferencePositive(){ + DefaultIssue issue1 = mock(DefaultIssue.class); + when(issue1.severity()).thenReturn("INFO"); + PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue1.getIssue()).thenReturn(issue1); + + DefaultIssue issue2 = mock(DefaultIssue.class); + when(issue2.severity()).thenReturn("MINOR"); + PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue2.getIssue()).thenReturn(issue2); + + SeverityComparator comparator = new SeverityComparator(); + assertEquals(1,comparator.compare(componentIssue1,componentIssue2)); + } + + @Test + public void whenFirstValueHigherThenDifferenceNegative(){ + DefaultIssue issue1 = mock(DefaultIssue.class); + when(issue1.severity()).thenReturn("MINOR"); + PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue1.getIssue()).thenReturn(issue1); + + DefaultIssue issue2 = mock(DefaultIssue.class); + when(issue2.severity()).thenReturn("INFO"); + PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue2.getIssue()).thenReturn(issue2); + + SeverityComparator comparator = new SeverityComparator(); + assertEquals(-1,comparator.compare(componentIssue1,componentIssue2)); + } + +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilterTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilterTest.java new file mode 100644 index 00000000..8e033757 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/SeverityExclusionFilterTest.java @@ -0,0 +1,35 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.junit.Test; +import org.sonar.core.issue.DefaultIssue; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SeverityExclusionFilterTest { + + @Test + public void whenInExcludedReturnFalse(){ + DefaultIssue issue = mock(DefaultIssue.class); + when(issue.severity()).thenReturn("MAJOR"); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue.getIssue()).thenReturn(issue); + + SeverityExclusionFilter filter = new SeverityExclusionFilter("MAJOR"); + assertFalse(filter.test(componentIssue)); + } + + @Test + public void whenNotInExcludedReturnTrue(){ + DefaultIssue issue = mock(DefaultIssue.class); + when(issue.severity()).thenReturn(""); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue.getIssue()).thenReturn(issue); + + SeverityExclusionFilter filter = new SeverityExclusionFilter("MAJOR"); + assertTrue(filter.test(componentIssue)); + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparatorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparatorTest.java new file mode 100644 index 00000000..7d2bd7c0 --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeComparatorTest.java @@ -0,0 +1,45 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.junit.Test; +import org.sonar.api.rules.RuleType; +import org.sonar.core.issue.DefaultIssue; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TypeComparatorTest { + + @Test + public void whenFirstValueLowerThenDifferencePositive(){ + DefaultIssue issue1 = mock(DefaultIssue.class); + when(issue1.type()).thenReturn(RuleType.CODE_SMELL); + PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue1.getIssue()).thenReturn(issue1); + + DefaultIssue issue2 = mock(DefaultIssue.class); + when(issue2.type()).thenReturn(RuleType.BUG); + PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue2.getIssue()).thenReturn(issue2); + + TypeComparator comparator = new TypeComparator(); + assertEquals(1,comparator.compare(componentIssue1,componentIssue2)); + } + + @Test + public void whenFirstValueHigherThenDifferenceNegative(){ + DefaultIssue issue1 = mock(DefaultIssue.class); + when(issue1.type()).thenReturn(RuleType.BUG); + PostAnalysisIssueVisitor.ComponentIssue componentIssue1 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue1.getIssue()).thenReturn(issue1); + + DefaultIssue issue2 = mock(DefaultIssue.class); + when(issue2.type()).thenReturn(RuleType.CODE_SMELL); + PostAnalysisIssueVisitor.ComponentIssue componentIssue2 = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue2.getIssue()).thenReturn(issue2); + + TypeComparator comparator = new TypeComparator(); + assertEquals(-1,comparator.compare(componentIssue1,componentIssue2)); + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilterTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilterTest.java new file mode 100644 index 00000000..1c7b890e --- /dev/null +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/commentfilter/TypeExclusionFilterTest.java @@ -0,0 +1,37 @@ +package com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter; + +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import org.junit.Test; +import org.sonar.api.rules.RuleType; +import org.sonar.core.issue.DefaultIssue; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TypeExclusionFilterTest { + + @Test + public void whenInExcludedReturnFalse(){ + DefaultIssue issue = mock(DefaultIssue.class); + when(issue.type()).thenReturn(RuleType.CODE_SMELL); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue.getIssue()).thenReturn(issue); + + TypeExclusionFilter filter = new TypeExclusionFilter("CODE_SMELL"); + assertFalse(filter.test(componentIssue)); + } + + @Test + public void whenNotInExcludedReturnTrue(){ + DefaultIssue issue = mock(DefaultIssue.class); + when(issue.type()).thenReturn(RuleType.BUG); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + when(componentIssue.getIssue()).thenReturn(issue); + + TypeExclusionFilter filter = new TypeExclusionFilter("CODE_SMELL"); + boolean result = filter.test(componentIssue); + assertTrue(result); + } +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java index 5e10b221..835e89b8 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/GithubPullRequestDecoratorTest.java @@ -20,6 +20,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.DecorationResult; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.db.alm.setting.ALM; @@ -46,6 +47,7 @@ public class GithubPullRequestDecoratorTest { private GithubPullRequestDecorator testCase = new GithubPullRequestDecorator(checkRunProvider); private ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); private AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + private IssueFilterRunner issueFilterRunner = mock(IssueFilterRunner.class); @Test @@ -56,9 +58,9 @@ public void testName() { @Test public void testDecorateQualityGatePropagateException() throws IOException, GeneralSecurityException { Exception dummyException = new IOException("Dummy Exception"); - doThrow(dummyException).when(checkRunProvider).createCheckRun(any(), any(), any()); + doThrow(dummyException).when(checkRunProvider).createCheckRun(any(), any(), any(),any()); - assertThatThrownBy(() -> testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, null)) .hasMessage("Could not decorate Pull Request on Github") .isExactlyInstanceOf(IllegalStateException.class).hasCause(dummyException); } @@ -66,12 +68,12 @@ public void testDecorateQualityGatePropagateException() throws IOException, Gene @Test public void testDecorateQualityGateReturnValue() throws IOException, GeneralSecurityException { DecorationResult expectedResult = DecorationResult.builder().build(); - doReturn(expectedResult).when(checkRunProvider).createCheckRun(any(), any(), any()); - DecorationResult decorationResult = testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); + doReturn(expectedResult).when(checkRunProvider).createCheckRun(any(), any(), any(), any()); + DecorationResult decorationResult = testCase.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, issueFilterRunner); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(AnalysisDetails.class); - verify(checkRunProvider).createCheckRun(argumentCaptor.capture(), eq(almSettingDto), eq(projectAlmSettingDto)); + verify(checkRunProvider).createCheckRun(argumentCaptor.capture(), eq(almSettingDto), eq(projectAlmSettingDto), eq(issueFilterRunner)); assertEquals(analysisDetails, argumentCaptor.getValue()); assertThat(decorationResult).isSameAs(expectedResult); } -} \ No newline at end of file +} diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java index ccf1b907..f4e9dfd2 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/github/v4/GraphqlCheckRunProviderTest.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.GithubApplicationAuthenticationProvider; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.RepositoryAuthenticationToken; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.github.v4.model.CheckAnnotationLevel; @@ -123,7 +124,7 @@ public void createCheckRunExceptionOnErrorResponse() throws IOException, General GraphqlCheckRunProvider testCase = new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto, null)) .hasMessage( "An error was returned in the response from the Github API:" + System.lineSeparator() + "- Error{message='example message', locations=[]}").isExactlyInstanceOf(IllegalStateException.class); @@ -176,47 +177,53 @@ public void createCheckRunExceptionOnInvalidIssueSeverity() throws IOException, GraphqlCheckRunProvider testCase = new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); - assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,null)) .hasMessage("Unknown severity value: dummy") .isExactlyInstanceOf(IllegalArgumentException.class); } @Test public void createCheckRunHappyPathOkStatus() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain", "http://api.target.domain/graphql"); + createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain", "http://api.target.domain/graphql",null); } @Test public void createCheckRunHappyPathOkStatusTrailingSlash() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/", "http://api.target.domain/graphql"); + createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/", "http://api.target.domain/graphql",null); } @Test public void createCheckRunHappyPathOkStatusApiPath() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api", "http://api.target.domain/api/graphql"); + createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api", "http://api.target.domain/api/graphql",null); } @Test public void createCheckRunHappyPathOkStatusApiPathTrailingSlash() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/", "http://api.target.domain/api/graphql"); + createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/", "http://api.target.domain/api/graphql",null); } @Test public void createCheckRunHappyPathOkStatusV3Path() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/v3", "http://api.target.domain/api/graphql"); + createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/v3", "http://api.target.domain/api/graphql",null); } @Test public void createCheckRunHappyPathOkStatusV3PathTrailingSlash() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/v3/", "http://api.target.domain/api/graphql"); + createCheckRunHappyPath(QualityGate.Status.OK, "http://api.target.domain/api/v3/", "http://api.target.domain/api/graphql",null); } @Test public void createCheckRunHappyPathErrorStatus() throws IOException, GeneralSecurityException { - createCheckRunHappyPath(QualityGate.Status.ERROR, "http://abc.de/", "http://abc.de/graphql"); + createCheckRunHappyPath(QualityGate.Status.ERROR, "http://abc.de/", "http://abc.de/graphql",null); } - private void createCheckRunHappyPath(QualityGate.Status status, String basePath, String fullPath) throws IOException, GeneralSecurityException { + @Test + public void checkIfFilterIsCalled() throws IOException, GeneralSecurityException { + IssueFilterRunner issueFilterRunner = mock(IssueFilterRunner.class); + createCheckRunHappyPath(QualityGate.Status.ERROR, "http://abc.de/", "http://abc.de/graphql",issueFilterRunner); + } + + private void createCheckRunHappyPath(QualityGate.Status status, String basePath, String fullPath, IssueFilterRunner issueFilterRunner) throws IOException, GeneralSecurityException { String[] messageInput = { "issue 1", "issue 2", @@ -350,6 +357,8 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, PostAnalysisIssueVisitor postAnalysisIssueVisitor = mock(PostAnalysisIssueVisitor.class); when(postAnalysisIssueVisitor.getIssues()).thenReturn(issueList); + if(issueFilterRunner != null) when(issueFilterRunner.filterIssues(issueList)).thenReturn(issueList); + when(analysisDetails.getQualityGateStatus()).thenReturn(status); when(analysisDetails.createAnalysisSummary(any())).thenReturn("dummy summary"); when(analysisDetails.getCommitSha()).thenReturn("commit SHA"); @@ -418,7 +427,10 @@ private void createCheckRunHappyPath(QualityGate.Status status, String basePath, GraphqlCheckRunProvider testCase = new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); - testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); + testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,issueFilterRunner); + + if(issueFilterRunner != null) verify(issueFilterRunner, times(1)).filterIssues(issueList); + assertEquals(1, requestBuilders.size()); @@ -576,7 +588,7 @@ public void checkExcessIssuesCorrectlyReported() throws IOException, GeneralSecu when(almSettingDto.getPrivateKey()).thenReturn("private key"); GraphqlCheckRunProvider testCase = new GraphqlCheckRunProvider(graphqlProvider, clock, githubApplicationAuthenticationProvider, server); - testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto); + testCase.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,null); ArgumentCaptor> classArgumentCaptor = ArgumentCaptor.forClass(Class.class); verify(graphQLTemplate, times(3)).mutate(any(GraphQLRequestEntity.class), classArgumentCaptor.capture()); @@ -604,7 +616,7 @@ public void checkExceptionThrownOnMissingUrl() { ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); GraphqlCheckRunProvider underTest = new GraphqlCheckRunProvider(mock(Clock.class), mock(GithubApplicationAuthenticationProvider.class), mock(Server.class)); - assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("No URL has been set for Github connections"); } @@ -617,7 +629,7 @@ public void checkExceptionThrownOnMissingPrivateKey() { ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); GraphqlCheckRunProvider underTest = new GraphqlCheckRunProvider(mock(Clock.class), mock(GithubApplicationAuthenticationProvider.class), mock(Server.class)); - assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("No private key has been set for Github connections"); } @@ -631,7 +643,7 @@ public void checkExceptionThrownOnMissingRepoPath() { ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); GraphqlCheckRunProvider underTest = new GraphqlCheckRunProvider(mock(Clock.class), mock(GithubApplicationAuthenticationProvider.class), mock(Server.class)); - assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("No repository name has been set for Github connections"); } @@ -646,9 +658,8 @@ public void checkExceptionThrownOnMissingAppId() { when(projectAlmSettingDto.getAlmRepo()).thenReturn("alm/repo"); GraphqlCheckRunProvider underTest = new GraphqlCheckRunProvider(mock(Clock.class), mock(GithubApplicationAuthenticationProvider.class), mock(Server.class)); - assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto)) + assertThatThrownBy(() -> underTest.createCheckRun(analysisDetails, almSettingDto, projectAlmSettingDto,null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("No App ID has been set for Github connections"); } - } diff --git a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java index bc67b7f5..dbf088c9 100644 --- a/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java +++ b/src/test/java/com/github/mc1arke/sonarqube/plugin/ce/pullrequest/gitlab/GitlabServerPullRequestDecoratorTest.java @@ -20,6 +20,7 @@ import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.AnalysisDetails; import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.PostAnalysisIssueVisitor; +import com.github.mc1arke.sonarqube.plugin.ce.pullrequest.commentfilter.IssueFilterRunner; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Rule; import org.junit.Test; @@ -27,6 +28,7 @@ import org.sonar.api.issue.Issue; import org.sonar.api.platform.Server; import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.issue.filter.IssueFilter; import org.sonar.ce.task.projectanalysis.scm.Changeset; import org.sonar.ce.task.projectanalysis.scm.ScmInfo; import org.sonar.ce.task.projectanalysis.scm.ScmInfoRepository; @@ -50,8 +52,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -97,8 +98,8 @@ public void decorateQualityGateStatus() { when(componentIssue.getComponent()).thenReturn(component); when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); - when(analysisDetails.createAnalysisSummary(Mockito.any())).thenReturn("summary"); - when(analysisDetails.createAnalysisIssueSummary(Mockito.any(), Mockito.any())).thenReturn("issue"); + when(analysisDetails.createAnalysisSummary(any())).thenReturn("summary"); + when(analysisDetails.createAnalysisIssueSummary(any(), any())).thenReturn("issue"); when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); @@ -172,6 +173,120 @@ public void decorateQualityGateStatus() { pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto); } + @Test + public void decorateQualityGateStatusWithIssueFilterRunner() { + String user = "sonar_user"; + String repositorySlug = "repo/slug"; + String commitSHA = "commitSHA"; + String branchName = "1"; + String projectKey = "projectKey"; + String sonarRootUrl = "http://sonar:9000/sonar"; + String discussionId = "6a9c1750b37d513a43987b574953fceb50b03ce7"; + String noteId = "1126"; + String filePath = "/path/to/file"; + int lineNumber = 5; + + ProjectAlmSettingDto projectAlmSettingDto = mock(ProjectAlmSettingDto.class); + AlmSettingDto almSettingDto = mock(AlmSettingDto.class); + when(almSettingDto.getPersonalAccessToken()).thenReturn("token"); + + AnalysisDetails analysisDetails = mock(AnalysisDetails.class); + when(analysisDetails.getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_INSTANCE_URL))) + .thenReturn(Optional.of(wireMockRule.baseUrl()+"/api/v4")); + when(analysisDetails + .getScannerProperty(eq(GitlabServerPullRequestDecorator.PULLREQUEST_GITLAB_PROJECT_ID))) + .thenReturn(Optional.of(repositorySlug)); + when(analysisDetails.getAnalysisProjectKey()).thenReturn(projectKey); + when(analysisDetails.getBranchName()).thenReturn(branchName); + when(analysisDetails.getCommitSha()).thenReturn(commitSHA); + when(analysisDetails.getNewCoverage()).thenReturn(Optional.of(BigDecimal.TEN)); + PostAnalysisIssueVisitor issueVisitor = mock(PostAnalysisIssueVisitor.class); + PostAnalysisIssueVisitor.ComponentIssue componentIssue = mock(PostAnalysisIssueVisitor.ComponentIssue.class); + DefaultIssue defaultIssue = mock(DefaultIssue.class); + when(defaultIssue.getStatus()).thenReturn(Issue.STATUS_OPEN); + when(defaultIssue.getLine()).thenReturn(lineNumber); + when(componentIssue.getIssue()).thenReturn(defaultIssue); + Component component = mock(Component.class); + when(componentIssue.getComponent()).thenReturn(component); + when(issueVisitor.getIssues()).thenReturn(Collections.singletonList(componentIssue)); + when(analysisDetails.getPostAnalysisIssueVisitor()).thenReturn(issueVisitor); + when(analysisDetails.createAnalysisSummary(any())).thenReturn("summary"); + when(analysisDetails.createAnalysisIssueSummary(any(), any())).thenReturn("issue"); + when(analysisDetails.getSCMPathForIssue(componentIssue)).thenReturn(Optional.of(filePath)); + + ScmInfoRepository scmInfoRepository = mock(ScmInfoRepository.class); + ScmInfo scmInfo = mock(ScmInfo.class); + when(scmInfo.hasChangesetForLine(anyInt())).thenReturn(true); + when(scmInfo.getChangesetForLine(anyInt())).thenReturn(Changeset.newChangesetBuilder().setDate(0L).setRevision(commitSHA).build()); + when(scmInfoRepository.getScmInfo(component)).thenReturn(Optional.of(scmInfo)); + wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/user")).withHeader("PRIVATE-TOKEN", equalTo("token")).willReturn(okJson("{\n" + + " \"id\": 1,\n" + + " \"username\": \"" + user + "\"}"))); + + wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName)).willReturn(okJson("{\n" + + " \"id\": 15235,\n" + + " \"iid\": " + branchName + ",\n" + + " \"diff_refs\": {\n" + + " \"base_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\",\n" + + " \"head_sha\":\"" + commitSHA + "\",\n" + + " \"start_sha\":\"d6a420d043dfe85e7c240fd136fc6e197998b10a\"}\n" + + "}"))); + + wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/commits")).willReturn(okJson("[\n" + + " {\n" + + " \"id\": \"" + commitSHA + "\"\n" + + " }]"))); + + wireMockRule.stubFor(get(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")).willReturn(okJson("[\n" + + " {\n" + + " \"id\": \"" + discussionId + "\",\n" + + " \"individual_note\": false,\n" + + " \"notes\": [\n" + + " {\n" + + " \"id\": " + noteId + ",\n" + + " \"type\": \"DiscussionNote\",\n" + + " \"body\": \"discussion text\",\n" + + " \"attachment\": null,\n" + + " \"author\": {\n" + + " \"id\": 1,\n" + + " \"username\": \"" + user + "\"\n" + + " }}]}]"))); + + wireMockRule.stubFor(delete(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions/" + discussionId + "/notes/" + noteId)).willReturn(noContent())); + + wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/statuses/" + commitSHA)) + .withQueryParam("name", equalTo("SonarQube")) + .withQueryParam("state", equalTo("failed")) + .withQueryParam("target_url", equalTo(sonarRootUrl + "/dashboard?id=" + projectKey + "&pullRequest=" + branchName)) + .withQueryParam("coverage", equalTo("10")) + .willReturn(created())); + + wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")) + .withRequestBody(equalTo("body=summary")) + .willReturn(created())); + + wireMockRule.stubFor(post(urlPathEqualTo("/api/v4/projects/" + urlEncode(repositorySlug) + "/merge_requests/" + branchName + "/discussions")) + .withRequestBody(equalTo("body=issue&" + + urlEncode("position[base_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + + urlEncode("position[start_sha]") + "=d6a420d043dfe85e7c240fd136fc6e197998b10a&" + + urlEncode("position[head_sha]") + "=" + commitSHA + "&" + + urlEncode("position[old_path]") + "=" + urlEncode(filePath) + "&" + + urlEncode("position[new_path]") + "=" + urlEncode(filePath) + "&" + + urlEncode("position[new_line]") + "=" + lineNumber + "&" + + urlEncode("position[position_type]") + "=text")) + .willReturn(created())); + + Server server = mock(Server.class); + when(server.getPublicRootUrl()).thenReturn(sonarRootUrl); + GitlabServerPullRequestDecorator pullRequestDecorator = + new GitlabServerPullRequestDecorator(server, scmInfoRepository); + + IssueFilterRunner mockFilterRunner= mock(IssueFilterRunner.class); + when(mockFilterRunner.filterIssues(any())).thenReturn(Collections.singletonList(componentIssue)); + + pullRequestDecorator.decorateQualityGateStatus(analysisDetails, almSettingDto, projectAlmSettingDto, mockFilterRunner); + } + private static String urlEncode(String value) { try { return URLEncoder.encode(value, StandardCharsets.UTF_8.name());