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 b6ed8c3d2..3f112c28f 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 915e4696c..8748f01d3 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 f73a00613..6942c5f62 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 043e61789..73840b27d 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 000000000..cd892a387 --- /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 000000000..e6323f4a8 --- /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 000000000..33dc303b1 --- /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 000000000..cc9527ae8 --- /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 000000000..202bcd914 --- /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 900c13ba1..81918daef 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 3db92bd63..f6dfcdd60 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 42e0c2688..544214d42 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 7864f1353..944d5e105 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 ad56f8cc4..4b55ff1f0 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 afa13c7c3..4316bdf24 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 000000000..dbb13f66c --- /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 000000000..4b20f22a7 --- /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 000000000..8e0337571 --- /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 000000000..7d2bd7c0c --- /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 000000000..1c7b890e7 --- /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 5e10b2215..835e89b8f 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 ccf1b9075..f4e9dfd22 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 bc67b7f56..dbf088c97 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());