From 32cec354bfc868d591e6356ee85728c5ecba51d1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?L=C3=A9o=20Geoffroy?= Date: Thu, 10 Oct 2024 16:03:21 +0200 Subject: [PATCH] SONAR-23250 Update activate_rule endpoint to support impacts --- .../server/qualityprofile/RuleActivation.java | 6 + .../ws/ActivateRuleActionIT.java | 107 +++++++++++++++++- .../qualityprofile/ws/ActivateRuleAction.java | 44 ++++++- .../QualityProfileWsParameters.java | 2 + 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java index 5cf81f7a547..a155d284fb9 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/qualityprofile/RuleActivation.java @@ -68,6 +68,12 @@ public class RuleActivation { return new RuleActivation(ruleUuid, false, severity, prioritizedRule, parameters, Map.of()); } + public static RuleActivation create(String ruleUuid, @Nullable String severity, Map impactSeverities, + @Nullable Boolean prioritizedRule, + @Nullable Map parameters) { + return new RuleActivation(ruleUuid, false, severity, prioritizedRule, parameters, impactSeverities); + } + public static RuleActivation create(String ruleUuid, @Nullable String severity, @Nullable Map parameters) { return create(ruleUuid, severity, null, parameters); } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRuleActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRuleActionIT.java index 72e33b8b962..e13a7d15b66 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRuleActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ActivateRuleActionIT.java @@ -21,17 +21,21 @@ package org.sonar.server.qualityprofile.ws; import java.net.HttpURLConnection; import java.util.Collection; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentCaptor; import org.mockito.MockitoAnnotations; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.server.ws.WebService; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import org.sonar.db.issue.ImpactDto; import org.sonar.db.permission.GlobalPermission; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.db.rule.RuleDto; @@ -55,9 +59,11 @@ import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_IMPACTS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_KEY; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_PRIORITIZED_RULE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_RULE; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_SEVERITY; class ActivateRuleActionIT { @@ -83,7 +89,7 @@ class ActivateRuleActionIT { WebService.Action definition = ws.getDef(); assertThat(definition).isNotNull(); assertThat(definition.isPost()).isTrue(); - assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("severity", "prioritizedRule", "key", + assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("severity", "impacts", "prioritizedRule", "key", "reset", "rule", "params"); } @@ -144,10 +150,69 @@ class ActivateRuleActionIT { } @Test - void activate_rule() { + void handle_whenBothSeverityAndImpactsAreSent_shouldFail() { userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_PROFILES); QProfileDto qualityProfile = db.qualityProfiles().insert(); RuleDto rule = db.rules().insert(RuleTesting.randomRuleKey()); + + TestRequest request = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RULE, rule.getKey().toString()) + .setParam(PARAM_KEY, qualityProfile.getKee()) + .setParam(PARAM_SEVERITY, "BLOCKER") + .setParam(PARAM_IMPACTS, "MAINTAINABILITY=BLOCKER"); + + assertThatThrownBy(() -> request.execute()) + .isInstanceOf(BadRequestException.class) + .hasMessage("'severity' and 'impacts' parameters can't be provided both at the same time"); + } + + @Test + void handle_whenImpactsAreMalformed_shouldFail() { + userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_PROFILES); + QProfileDto qualityProfile = db.qualityProfiles().insert(); + RuleDto rule = db.rules().insert(RuleTesting.randomRuleKey()); + + TestRequest request = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RULE, rule.getKey().toString()) + .setParam(PARAM_KEY, qualityProfile.getKee()) + .setParam(PARAM_IMPACTS, "MAINTAINABILITY=UNKNOWN_SEVERITY"); + + assertThatThrownBy(() -> request.execute()) + .isInstanceOf(BadRequestException.class) + .hasMessage("Unexpected value for parameter 'impacts': MAINTAINABILITY=UNKNOWN_SEVERITY"); + } + + @Test + void handle_whenImpactsAreProvidedAndDoesntMatchRuleImpacts_shouldFail() { + userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_PROFILES); + QProfileDto qualityProfile = db.qualityProfiles().insert(); + RuleDto rule = db.rules().insert(r -> r.setRuleKey(RuleTesting.randomRuleKey()).setSeverity(Severity.MAJOR) + .replaceAllDefaultImpacts(List.of( + newImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM)))); + + TestRequest request = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RULE, rule.getKey().toString()) + .setParam(PARAM_KEY, qualityProfile.getKee()) + .setParam(PARAM_IMPACTS, "MAINTAINABILITY=BLOCKER;SECURITY=MEDIUM"); + + assertThatThrownBy(() -> request.execute()) + .isInstanceOf(BadRequestException.class) + .hasMessage("Only impacts defined on the rule can be overridden. (MAINTAINABILITY)"); + } + + @Test + void activate_rule() { + userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_PROFILES); + QProfileDto qualityProfile = db.qualityProfiles().insert(); + RuleDto rule = db.rules().insert(r -> r.setRuleKey(RuleTesting.randomRuleKey()).setSeverity(Severity.MAJOR) + .replaceAllDefaultImpacts(List.of( + newImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM), + newImpact(SoftwareQuality.SECURITY, org.sonar.api.issue.impact.Severity.LOW), + newImpact(SoftwareQuality.RELIABILITY, org.sonar.api.issue.impact.Severity.INFO)))); + TestRequest request = ws.newRequest() .setMethod("POST") .setParam(PARAM_RULE, rule.getKey().toString()) @@ -168,10 +233,48 @@ class ActivateRuleActionIT { RuleActivation activation = activations.iterator().next(); assertThat(activation.getRuleUuid()).isEqualTo(rule.getUuid()); assertThat(activation.getSeverity()).isEqualTo(Severity.BLOCKER); + assertThat(activation.getImpactSeverities()).isEmpty(); assertThat(activation.isPrioritizedRule()).isTrue(); assertThat(activation.isReset()).isFalse(); } + @Test + void handle_whenImpactsAreProvided_shouldOverrideImpacts() { + userSession.logIn().addPermission(GlobalPermission.ADMINISTER_QUALITY_PROFILES); + QProfileDto qualityProfile = db.qualityProfiles().insert(); + RuleDto rule = db.rules().insert(r -> r.setRuleKey(RuleTesting.randomRuleKey()).setSeverity(Severity.MAJOR) + .replaceAllDefaultImpacts(List.of( + newImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.MEDIUM), + newImpact(SoftwareQuality.SECURITY, org.sonar.api.issue.impact.Severity.LOW), + newImpact(SoftwareQuality.RELIABILITY, org.sonar.api.issue.impact.Severity.INFO)))); + + TestRequest request = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RULE, rule.getKey().toString()) + .setParam(PARAM_KEY, qualityProfile.getKee()) + .setParam(PARAM_IMPACTS, "MAINTAINABILITY=BLOCKER;SECURITY=MEDIUM"); + + TestResponse response = request.execute(); + + assertThat(response.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT); + verify(qProfileRules).activateAndCommit(any(DbSession.class), any(QProfileDto.class), ruleActivationCaptor.capture()); + + Collection activations = ruleActivationCaptor.getValue(); + assertThat(activations).hasSize(1); + + RuleActivation activation = activations.iterator().next(); + assertThat(activation.getRuleUuid()).isEqualTo(rule.getUuid()); + assertThat(activation.getSeverity()).isNull(); + assertThat(activation.getImpactSeverities()).containsExactlyInAnyOrderEntriesOf( + Map.of(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.BLOCKER, SoftwareQuality.SECURITY, org.sonar.api.issue.impact.Severity.MEDIUM)); + assertThat(activation.isPrioritizedRule()).isNull(); + assertThat(activation.isReset()).isFalse(); + } + + private static ImpactDto newImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity) { + return new ImpactDto().setSoftwareQuality(softwareQuality).setSeverity(severity); + } + @Test void as_qprofile_editor() { UserDto user = db.users().insertUser(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRuleAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRuleAction.java index 93b149595cb..39dee38f0d5 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRuleAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ActivateRuleAction.java @@ -19,7 +19,11 @@ */ package org.sonar.server.qualityprofile.ws; +import java.util.EnumMap; import java.util.Map; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.server.ws.Change; @@ -31,6 +35,7 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.db.rule.RuleDto; +import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.qualityprofile.QProfileRules; import org.sonar.server.qualityprofile.RuleActivation; import org.sonar.server.user.UserSession; @@ -40,6 +45,7 @@ import static java.lang.String.format; import static java.util.Collections.singletonList; import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_ACTIVATE_RULE; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_IMPACTS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_KEY; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_PARAMS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_PRIORITIZED_RULE; @@ -71,6 +77,7 @@ public class ActivateRuleAction implements QProfileWsAction { "
  • Edit right on the specified quality profile
  • " + "") .setChangelog( + new Change("10.8", format("Add new parameter '%s'", PARAM_IMPACTS)), new Change("10.6", format("Add parameter '%s'.", PARAM_PRIORITIZED_RULE)), new Change("10.2", format("Parameter '%s' is now deprecated.", PARAM_SEVERITY))) .setHandler(this) @@ -88,10 +95,14 @@ public class ActivateRuleAction implements QProfileWsAction { .setExampleValue("java:AvoidCycles"); activate.createParam(PARAM_SEVERITY) - .setDescription(format("Severity. Ignored if parameter %s is true.", PARAM_RESET)) + .setDescription(format("Severity. Cannot be used as the same time as '%s'.Ignored if parameter %s is true.", PARAM_IMPACTS, PARAM_RESET)) .setDeprecatedSince("10.2") .setPossibleValues(Severity.ALL); + activate.createParam(PARAM_IMPACTS) + .setDescription(format("Override of impact severities for the rule. Cannot be used as the same time as '%s'. Ignored if parameter %s is true.", PARAM_SEVERITY, PARAM_RESET)) + .setExampleValue("impacts=MAINTAINABILITY=HIGH;SECURITY=MEDIUM"); + activate.createParam(PARAM_PARAMS) .setDescription(format("Parameters as semi-colon list of key=value. Ignored if parameter %s is true.", PARAM_RESET)) .setExampleValue("params=key1=v1;key2=v2"); @@ -129,13 +140,42 @@ public class ActivateRuleAction implements QProfileWsAction { return RuleActivation.createReset(ruleDto.getUuid()); } String severity = request.param(PARAM_SEVERITY); + String impactsAsString = request.param(PARAM_IMPACTS); + + if (impactsAsString != null && severity != null) { + throw BadRequestException.create(format("'%s' and '%s' parameters can't be provided both at the same time", PARAM_SEVERITY, PARAM_IMPACTS)); + } + + Map impacts = new EnumMap<>(SoftwareQuality.class); + if (impactsAsString != null) { + impacts = getImpacts(impactsAsString, ruleDto); + } + Boolean prioritizedRule = request.paramAsBoolean(PARAM_PRIORITIZED_RULE); Map params = null; String paramsAsString = request.param(PARAM_PARAMS); if (paramsAsString != null) { params = KeyValueFormat.parse(paramsAsString); } - return RuleActivation.create(ruleDto.getUuid(), severity, prioritizedRule, params); + return RuleActivation.create(ruleDto.getUuid(), severity, impacts, prioritizedRule, params); + } + + @NotNull + private static Map getImpacts(String impactsAsString, RuleDto ruleDto) { + Map result; + try { + result = KeyValueFormat.parse(impactsAsString) + .entrySet() + .stream().collect(Collectors.toMap(e -> SoftwareQuality.valueOf(e.getKey()), e -> org.sonar.api.issue.impact.Severity.valueOf(e.getValue()))); + } catch (Exception e) { + throw BadRequestException.create(format("Unexpected value for parameter '%s': %s", PARAM_IMPACTS, impactsAsString)); + } + if (!ruleDto.getDefaultImpactsMap().keySet().containsAll(result.keySet())) { + throw BadRequestException.create( + format("Only impacts defined on the rule can be overridden. (%s)", ruleDto.getDefaultImpactsMap().keySet().stream().map(Enum::name).collect(Collectors.joining(",")))); + } + return result; + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfileWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfileWsParameters.java index ae9a36ba880..4d39e64bdea 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfileWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/qualityprofile/QualityProfileWsParameters.java @@ -27,6 +27,7 @@ public class QualityProfileWsParameters { String PARAM_BACKUP = "backup"; } + public static final String ACTION_ACTIVATE_RULE = "activate_rule"; public static final String ACTION_ACTIVATE_RULES = "activate_rules"; public static final String ACTION_ADD_PROJECT = "add_project"; @@ -70,6 +71,7 @@ public class QualityProfileWsParameters { public static final String PARAM_TO = "to"; public static final String PARAM_TO_NAME = "toName"; public static final String PARAM_PRIORITIZED_RULE = "prioritizedRule"; + public static final String PARAM_IMPACTS = "impacts"; private QualityProfileWsParameters() { // Only static stuff -- 2.39.5