From 54c600a0fddace72aadafb4c3a3484b51405628d Mon Sep 17 00:00:00 2001 From: Antoine Vinot Date: Wed, 6 Jul 2022 11:36:11 +0200 Subject: [PATCH] SONAR-16614 Modify issues search API to include ruleDescriptionContextKey field --- .../server/issue/ws/AddCommentAction.java | 1 + .../sonar/server/issue/ws/AssignAction.java | 1 + .../server/issue/ws/DeleteCommentAction.java | 1 + .../server/issue/ws/DoTransitionAction.java | 1 + .../server/issue/ws/EditCommentAction.java | 1 + .../sonar/server/issue/ws/SearchAction.java | 5 +- .../issue/ws/SearchAdditionalField.java | 3 +- .../server/issue/ws/SearchResponseFormat.java | 78 +++-- .../server/issue/ws/SetSeverityAction.java | 1 + .../sonar/server/issue/ws/SetTagsAction.java | 1 + .../sonar/server/issue/ws/SetTypeAction.java | 1 + .../server/issue/ws/add_comment-example.json | 3 +- .../sonar/server/issue/ws/assign-example.json | 3 +- .../issue/ws/delete_comment-example.json | 3 +- .../issue/ws/do_transition-example.json | 3 +- .../server/issue/ws/edit_comment-example.json | 3 +- .../sonar/server/issue/ws/search-example.json | 3 +- .../server/issue/ws/set_severity-example.json | 3 +- .../server/issue/ws/set_tags-example.json | 3 +- .../server/issue/ws/set_type-example.json | 3 +- .../server/issue/ws/SearchActionTest.java | 52 +++ ...archResponseFormatFormatOperationTest.java | 306 ++++++++++++++++++ sonar-ws/src/main/protobuf/ws-issues.proto | 1 + 23 files changed, 434 insertions(+), 46 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java index 9eb76207056..474894f396d 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java @@ -71,6 +71,7 @@ public class AddCommentAction implements IssuesWsAction { "Requires authentication and the following permission: 'Browse' on the project of the specified issue.") .setSince("3.6") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.3", "the response returns the issue with all its details"), new Change("6.5", "the database ids of the components are removed from the response"), new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead.")) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java index 37c7dec4cb3..701f2cc80c0 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java @@ -74,6 +74,7 @@ public class AssignAction implements IssuesWsAction { .setDescription("Assign/Unassign an issue. Requires authentication and Browse permission on project") .setSince("3.6") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.5", "the database ids of the components are removed from the response"), new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead.")) .setHandler(this) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java index 7089228fa13..ba76cbf2475 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java @@ -60,6 +60,7 @@ public class DeleteCommentAction implements IssuesWsAction { "Requires authentication and the following permission: 'Browse' on the project of the specified issue.") .setSince("3.6") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead."), new Change("6.5", "the database ids of the components are removed from the response"), new Change("6.3", "the response returns the issue with all its details"), diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java index 2776f28ca5e..05e70e96e5c 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java @@ -81,6 +81,7 @@ public class DoTransitionAction implements IssuesWsAction { "The transitions involving security hotspots require the permission 'Administer Security Hotspot'.") .setSince("3.6") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("8.1", format("transitions '%s' and '%s' are no more supported", SET_AS_IN_REVIEW, OPEN_AS_VULNERABILITY)), new Change("7.8", format("added '%s', %s, %s and %s transitions for security hotspots ", SET_AS_IN_REVIEW, RESOLVE_AS_REVIEWED, OPEN_AS_VULNERABILITY, RESET_AS_TO_REVIEW)), new Change("7.3", "added transitions for security hotspots"), diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/EditCommentAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/EditCommentAction.java index 676e6a9795d..a3d3f51c230 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/EditCommentAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/EditCommentAction.java @@ -66,6 +66,7 @@ public class EditCommentAction implements IssuesWsAction { "Requires authentication and the following permission: 'Browse' on the project of the specified issue.") .setSince("3.6") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.3", "the response returns the issue with all its details"), new Change("6.3", format("the 'key' parameter has been renamed %s", PARAM_COMMENT)), new Change("6.5", "the database ids of the components are removed from the response"), diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java index 12be75097fb..fe773f380d0 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java @@ -189,7 +189,8 @@ public class SearchAction implements IssuesWsAction { + "
When issue indexation is in progress returns 503 service unavailable HTTP code.") .setSince("3.6") .setChangelog( - + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), + new Change("9.6", "New possible value for 'additionalFields' parameter: 'ruleDescriptionContextKey'"), new Change("9.6", "Facet 'moduleUuids' is dropped."), new Change("9.4", format("Parameter '%s' is deprecated, please use '%s' instead", PARAM_SINCE_LEAK_PERIOD, PARAM_IN_NEW_CODE_PERIOD)), new Change("9.2", "Response field 'quickFixAvailable' added"), @@ -394,7 +395,7 @@ public class SearchAction implements IssuesWsAction { .filter(FACETS_REQUIRING_PROJECT::contains) .collect(toSet()); checkArgument(facetsRequiringProjectParameter.isEmpty() || - (!query.projectUuids().isEmpty()), "Facet(s) '%s' require to also filter by project", + (!query.projectUuids().isEmpty()), "Facet(s) '%s' require to also filter by project", String.join(",", facetsRequiringProjectParameter)); // execute request diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAdditionalField.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAdditionalField.java index 5620f49d573..17b609c3484 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAdditionalField.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAdditionalField.java @@ -40,7 +40,8 @@ public enum SearchAdditionalField { LANGUAGES("languages"), RULES("rules"), TRANSITIONS("transitions"), - USERS("users"); + USERS("users"), + RULE_DESCRIPTION_CONTEXT_KEY("ruleDescriptionContextKey"); public static final String ALL_ALIAS = "_all"; static final EnumSet ALL_ADDITIONAL_FIELDS = EnumSet.allOf(SearchAdditionalField.class); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index 4698db673c5..40ff06f7f74 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -25,10 +25,10 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; -import org.sonar.api.resources.Qualifiers; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.DateUtils; @@ -67,10 +67,17 @@ import static java.lang.String.format; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; +import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE; import static org.sonar.api.rule.RuleKey.EXTERNAL_RULE_REPO_PREFIX; import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; import static org.sonar.server.issue.index.IssueIndex.FACET_ASSIGNED_TO_ME; import static org.sonar.server.issue.index.IssueIndex.FACET_PROJECTS; +import static org.sonar.server.issue.ws.SearchAdditionalField.ACTIONS; +import static org.sonar.server.issue.ws.SearchAdditionalField.ALL_ADDITIONAL_FIELDS; +import static org.sonar.server.issue.ws.SearchAdditionalField.COMMENTS; +import static org.sonar.server.issue.ws.SearchAdditionalField.RULE_DESCRIPTION_CONTEXT_KEY; +import static org.sonar.server.issue.ws.SearchAdditionalField.TRANSITIONS; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES; @@ -93,7 +100,7 @@ public class SearchResponseFormat { formatPaging(paging, response); ofNullable(data.getEffortTotal()).ifPresent(response::setEffortTotal); - response.addAllIssues(formatIssues(fields, data)); + response.addAllIssues(createIssues(fields, data)); response.addAllComponents(formatComponents(data)); formatFacets(data, facets, response); if (fields.contains(SearchAdditionalField.RULES)) { @@ -112,13 +119,8 @@ public class SearchResponseFormat { Operation.Builder response = Operation.newBuilder(); if (data.getIssues().size() == 1) { - Issue.Builder issueBuilder = Issue.newBuilder(); IssueDto dto = data.getIssues().get(0); - formatIssue(issueBuilder, dto, data); - formatIssueActions(data, issueBuilder, dto); - formatIssueTransitions(data, issueBuilder, dto); - formatIssueComments(data, issueBuilder, dto); - response.setIssue(issueBuilder.build()); + response.setIssue(createIssue(ALL_ADDITIONAL_FIELDS, data, dto)); } response.addAllComponents(formatComponents(data)); response.addAllRules(formatRules(data).getRulesList()); @@ -140,27 +142,20 @@ public class SearchResponseFormat { .setTotal(paging.total()); } - private List formatIssues(Set fields, SearchResponseData data) { - List result = new ArrayList<>(); + private List createIssues(Collection fields, SearchResponseData data) { + return data.getIssues().stream() + .map(dto -> createIssue(fields, data, dto)) + .collect(toList()); + } + + private Issue createIssue(Collection fields, SearchResponseData data, IssueDto dto) { Issue.Builder issueBuilder = Issue.newBuilder(); - data.getIssues().forEach(dto -> { - issueBuilder.clear(); - formatIssue(issueBuilder, dto, data); - if (fields.contains(SearchAdditionalField.ACTIONS)) { - formatIssueActions(data, issueBuilder, dto); - } - if (fields.contains(SearchAdditionalField.TRANSITIONS)) { - formatIssueTransitions(data, issueBuilder, dto); - } - if (fields.contains(SearchAdditionalField.COMMENTS)) { - formatIssueComments(data, issueBuilder, dto); - } - result.add(issueBuilder.build()); - }); - return result; + addMandatoryFieldsToIssueBuilder(issueBuilder, dto, data); + addAdditionalFieldsToIssueBuilder(fields, data, dto, issueBuilder); + return issueBuilder.build(); } - private void formatIssue(Issue.Builder issueBuilder, IssueDto dto, SearchResponseData data) { + private void addMandatoryFieldsToIssueBuilder(Issue.Builder issueBuilder, IssueDto dto, SearchResponseData data) { issueBuilder.setKey(dto.getKey()); issueBuilder.setType(Common.RuleType.forNumber(dto.getType())); @@ -199,10 +194,25 @@ public class SearchResponseFormat { ofNullable(dto.getIssueUpdateDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setUpdateDate); ofNullable(dto.getIssueCloseDate()).map(DateUtils::formatDateTime).ifPresent(issueBuilder::setCloseDate); - ofNullable(dto.isQuickFixAvailable()) + Optional.of(dto.isQuickFixAvailable()) .ifPresentOrElse(issueBuilder::setQuickFixAvailable, () -> issueBuilder.setQuickFixAvailable(false)); - issueBuilder.setScope(Qualifiers.UNIT_TEST_FILE.equals(component.qualifier()) ? IssueScope.TEST.name() : IssueScope.MAIN.name()); + issueBuilder.setScope(UNIT_TEST_FILE.equals(component.qualifier()) ? IssueScope.TEST.name() : IssueScope.MAIN.name()); + } + + private static void addAdditionalFieldsToIssueBuilder(Collection fields, SearchResponseData data, IssueDto dto, Issue.Builder issueBuilder) { + if (fields.contains(ACTIONS)) { + issueBuilder.setActions(createIssueActions(data, dto)); + } + if (fields.contains(TRANSITIONS)) { + issueBuilder.setTransitions(createIssueTransition(data, dto)); + } + if (fields.contains(COMMENTS)) { + issueBuilder.setComments(createIssueComments(data, dto)); + } + if (fields.contains(RULE_DESCRIPTION_CONTEXT_KEY)) { + dto.getOptionalRuleDescriptionContextKey().ifPresent(issueBuilder::setRuleDescriptionContextKey); + } } private static String engineNameFrom(RuleKey ruleKey) { @@ -225,7 +235,7 @@ public class SearchResponseFormat { } } - private static void formatIssueTransitions(SearchResponseData data, Issue.Builder wsIssue, IssueDto dto) { + private static Transitions createIssueTransition(SearchResponseData data, IssueDto dto) { Transitions.Builder wsTransitions = Transitions.newBuilder(); List transitions = data.getTransitionsForIssueKey(dto.getKey()); if (transitions != null) { @@ -233,19 +243,19 @@ public class SearchResponseFormat { wsTransitions.addTransitions(transition.key()); } } - wsIssue.setTransitions(wsTransitions); + return wsTransitions.build(); } - private static void formatIssueActions(SearchResponseData data, Issue.Builder wsIssue, IssueDto dto) { + private static Actions createIssueActions(SearchResponseData data, IssueDto dto) { Actions.Builder wsActions = Actions.newBuilder(); List actions = data.getActionsForIssueKey(dto.getKey()); if (actions != null) { wsActions.addAllActions(actions); } - wsIssue.setActions(wsActions); + return wsActions.build(); } - private static void formatIssueComments(SearchResponseData data, Issue.Builder wsIssue, IssueDto dto) { + private static Comments createIssueComments(SearchResponseData data, IssueDto dto) { Comments.Builder wsComments = Comments.newBuilder(); List comments = data.getCommentsForIssueKey(dto.getKey()); if (comments != null) { @@ -266,7 +276,7 @@ public class SearchResponseFormat { wsComments.addComments(wsComment); } } - wsIssue.setComments(wsComments); + return wsComments.build(); } private Common.Rules.Builder formatRules(SearchResponseData data) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java index 0150ad31962..564736a0d48 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java @@ -78,6 +78,7 @@ public class SetSeverityAction implements IssuesWsAction { "") .setSince("3.6") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.5", "the database ids of the components are removed from the response"), new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead.")) .setHandler(this) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java index f6f363bec9b..d7f8218b4b3 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java @@ -73,6 +73,7 @@ public class SetTagsAction implements IssuesWsAction { .setDescription("Set tags on an issue.
" + "Requires authentication and Browse permission on project") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.5", "the database ids of the components are removed from the response"), new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead."), new Change("6.4", "response contains issue information instead of list of tags")) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java index aee2c8a7fb4..30cd890e1be 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java @@ -82,6 +82,7 @@ public class SetTypeAction implements IssuesWsAction { "") .setSince("5.5") .setChangelog( + new Change("9.6", "Response field 'ruleDescriptionContextKey' added"), new Change("6.5", "the database ids of the components are removed from the response"), new Change("6.5", "the response field components.uuid is deprecated. Use components.key instead.")) .setHandler(this) diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/add_comment-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/add_comment-example.json index 6f52df0df9d..cbfd2a75f83 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/add_comment-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/add_comment-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/assign-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/assign-example.json index 16269e44266..50f89081597 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/assign-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/assign-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/delete_comment-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/delete_comment-example.json index 16269e44266..50f89081597 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/delete_comment-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/delete_comment-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/do_transition-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/do_transition-example.json index 16269e44266..50f89081597 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/do_transition-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/do_transition-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/edit_comment-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/edit_comment-example.json index 16269e44266..50f89081597 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/edit_comment-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/edit_comment-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json index 4c8efbada3e..ba19cc0b7df 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json @@ -77,7 +77,8 @@ ] } ], - "quickFixAvailable": false + "quickFixAvailable": false, + "ruleDescriptionContextKey": "spring" } ], "components": [ diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_severity-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_severity-example.json index 16269e44266..50f89081597 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_severity-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_severity-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_tags-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_tags-example.json index 1ccdedc6d71..58865766e66 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_tags-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_tags-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_type-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_type-example.json index 16269e44266..50f89081597 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_type-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_type-example.json @@ -47,7 +47,8 @@ ], "creationDate": "2016-11-25T13:50:24+0100", "updateDate": "2017-01-09T13:51:12+0100", - "type": "CODE_SMELL" + "type": "CODE_SMELL", + "ruleDescriptionContextKey": "spring" }, "components": [ { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java index 03ecfc9eaea..0d1d86a81e2 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java @@ -84,6 +84,7 @@ import org.sonarqube.ws.Issues.SearchWsResponse; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static org.apache.commons.lang.StringUtils.EMPTY; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.groups.Tuple.tuple; @@ -1489,6 +1490,57 @@ public class SearchActionTest { .hasMessage("If both provided, the following parameters sinceLeakPeriod and inNewCodePeriod must match."); } + @Test + public void search_when_additional_field_set_return_context_key() { + insertIssues(issue -> issue.setRuleDescriptionContextKey("spring")); + indexPermissionsAndIssues(); + + SearchWsResponse response = ws.newRequest() + .setParam("additionalFields", "ruleDescriptionContextKey") + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getIssuesList()).isNotEmpty() + .extracting(Issue::getRuleDescriptionContextKey).containsExactly("spring"); + } + + @Test + public void search_when_no_additional_field_return_empty_context_key() { + insertIssues(issue -> issue.setRuleDescriptionContextKey("spring")); + indexPermissionsAndIssues(); + + SearchWsResponse response = ws.newRequest() + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getIssuesList()).isNotEmpty() + .extracting(Issue::getRuleDescriptionContextKey).containsExactly(EMPTY); + } + + @Test + public void search_when_additional_field_but_no_context_key_return_empty_context_key() { + insertIssues(issue -> issue.setRuleDescriptionContextKey(null)); + indexPermissionsAndIssues(); + + SearchWsResponse response = ws.newRequest() + .setParam("additionalFields", "ruleDescriptionContextKey") + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getIssuesList()).isNotEmpty() + .extracting(Issue::getRuleDescriptionContextKey).containsExactly(EMPTY); + } + + @Test + public void search_when_additional_field_set_to_all_return_context_key() { + insertIssues(issue -> issue.setRuleDescriptionContextKey("spring")); + indexPermissionsAndIssues(); + + SearchWsResponse response = ws.newRequest() + .setParam("additionalFields", "_all") + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getIssuesList()).isNotEmpty() + .extracting(Issue::getRuleDescriptionContextKey).containsExactly("spring"); + } + private RuleDto newIssueRule() { RuleDto rule = newRule(XOO_X1, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc")) .setLanguage("xoo") diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java new file mode 100644 index 00000000000..c5f5878b552 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java @@ -0,0 +1,306 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.issue.ws; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.sonar.api.resources.Languages; +import org.sonar.api.utils.Duration; +import org.sonar.api.utils.Durations; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueChangeDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.rule.RuleDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.issue.TextRangeResponseFormatter; +import org.sonar.server.issue.workflow.Transition; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Issues.Issue; +import org.sonarqube.ws.Issues.Operation; + +import static java.lang.System.currentTimeMillis; +import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE; +import static org.sonar.api.rule.RuleKey.EXTERNAL_RULE_REPO_PREFIX; +import static org.sonar.api.rules.RuleType.CODE_SMELL; +import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.db.component.ComponentDto.BRANCH_KEY_SEPARATOR; +import static org.sonar.db.component.ComponentDto.PULL_REQUEST_SEPARATOR; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.db.issue.IssueTesting.newIssue; +import static org.sonar.db.issue.IssueTesting.newIssuechangeDto; +import static org.sonar.db.rule.RuleTesting.newRule; +import static org.sonar.db.user.UserTesting.newUserDto; +import static org.sonar.server.issue.index.IssueScope.MAIN; +import static org.sonar.server.issue.index.IssueScope.TEST; + +@RunWith(MockitoJUnitRunner.class) +public class SearchResponseFormatFormatOperationTest { + + private SearchResponseFormat searchResponseFormat; + + private final Durations durations = new Durations(); + @Mock + private Languages languages; + @Mock + private TextRangeResponseFormatter textRangeResponseFormatter; + @Mock + private UserResponseFormatter userResponseFormatter; + @Mock + private Common.User user; + + private SearchResponseData searchResponseData; + private IssueDto issueDto; + private ComponentDto componentDto; + private UserDto userDto; + + + @Before + public void setUp() { + searchResponseFormat = new SearchResponseFormat(durations, languages, textRangeResponseFormatter, userResponseFormatter); + searchResponseData = newSearchResponseData(); + issueDto = searchResponseData.getIssues().get(0); + componentDto = searchResponseData.getComponents().iterator().next(); + userDto = searchResponseData.getUsers().get(0); + when(userResponseFormatter.formatUser(any(Common.User.Builder.class), eq(userDto))).thenReturn(user); + } + + @Test + public void formatOperation_should_add_components_to_response() { + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getComponentsList()).hasSize(1); + assertThat(result.getComponentsList().get(0).getKey()).isEqualTo(issueDto.getComponentKey()); + } + + @Test + public void formatOperation_should_add_rules_to_response() { + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getRulesList()).hasSize(1); + assertThat(result.getRulesList().get(0).getKey()).isEqualTo(issueDto.getRuleKey().toString()); + } + + @Test + public void formatOperation_should_add_users_to_response() { + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getUsersList()).hasSize(1); + assertThat(result.getUsers(0)).isSameAs(user); + } + + @Test + public void formatOperation_should_add_issue_to_response() { + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertIssueEqualsIssueDto(result.getIssue(), issueDto); + } + + private void assertIssueEqualsIssueDto(Issue issue, IssueDto issueDto) { + assertThat(issue.getKey()).isEqualTo(issueDto.getKey()); + assertThat(issue.getType().getNumber()).isEqualTo(issueDto.getType()); + assertThat(issue.getComponent()).isEqualTo(issueDto.getComponentKey()); + assertThat(issue.getRule()).isEqualTo(issueDto.getRuleKey().toString()); + assertThat(issue.getSeverity()).hasToString(issueDto.getSeverity()); + assertThat(issue.getAssignee()).isEqualTo(userDto.getLogin()); + assertThat(issue.getResolution()).isEqualTo(issueDto.getResolution()); + assertThat(issue.getStatus()).isEqualTo(issueDto.getStatus()); + assertThat(issue.getMessage()).isEqualTo(issueDto.getMessage()); + assertThat(new ArrayList<>(issue.getTagsList())).containsExactlyInAnyOrderElementsOf(issueDto.getTags()); + assertThat(issue.getLine()).isEqualTo(issueDto.getLine()); + assertThat(issue.getHash()).isEqualTo(issueDto.getChecksum()); + assertThat(issue.getAuthor()).isEqualTo(issueDto.getAuthorLogin()); + assertThat(issue.getCreationDate()).isEqualTo(formatDateTime(issueDto.getIssueCreationDate())); + assertThat(issue.getUpdateDate()).isEqualTo(formatDateTime(issueDto.getIssueUpdateDate())); + assertThat(issue.getCloseDate()).isEqualTo(formatDateTime(issueDto.getIssueCloseDate())); + assertThat(issue.getQuickFixAvailable()).isEqualTo(issueDto.isQuickFixAvailable()); + assertThat(issue.getRuleDescriptionContextKey()).isEqualTo(issueDto.getOptionalRuleDescriptionContextKey().orElse(null)); + } + + @Test + public void formatOperation_should_not_add_issue_when_several_issue() { + searchResponseData = new SearchResponseData(List.of(createIssue(), createIssue())); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue()).isEqualTo(Issue.getDefaultInstance()); + } + + private static IssueDto createIssue() { + RuleDto ruleDto = newRule(); + String projectUuid = "project_uuid_" + randomAlphanumeric(5); + ComponentDto projectDto = newPrivateProjectDto(); + projectDto.setProjectUuid(projectUuid); + return newIssue(ruleDto, projectUuid, "project_key_" + randomAlphanumeric(5), projectDto); + } + + @Test + public void formatOperation_should_add_branch_on_issue() { + componentDto.setDbKey(randomAlphanumeric(5) + BRANCH_KEY_SEPARATOR + randomAlphanumeric(5)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getBranch()).isEqualTo(componentDto.getBranch()); + } + + @Test + public void formatOperation_should_add_pullrequest_on_issue() { + componentDto.setDbKey(randomAlphanumeric(5) + PULL_REQUEST_SEPARATOR + randomAlphanumeric(5)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getPullRequest()).isEqualTo(componentDto.getPullRequest()); + } + + @Test + public void formatOperation_should_add_project_on_issue() { + issueDto.setProjectUuid(componentDto.uuid()); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getProject()).isEqualTo(componentDto.getKey()); + } + + @Test + public void formatOperation_should_add_external_rule_engine_on_issue() { + issueDto.setExternal(true); + String expected = randomAlphanumeric(5); + issueDto.setRuleKey(EXTERNAL_RULE_REPO_PREFIX + expected, randomAlphanumeric(5)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getExternalRuleEngine()).isEqualTo(expected); + } + + @Test + public void formatOperation_should_add_effort_and_debt_on_issue() { + long effort = 60L; + issueDto.setEffort(effort); + String expected = durations.encode(Duration.create(effort)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getEffort()).isEqualTo(expected); + assertThat(result.getIssue().getDebt()).isEqualTo(expected); + } + + @Test + public void formatOperation_should_add_scope_test_on_issue_when_unit_test_file() { + componentDto.setQualifier(UNIT_TEST_FILE); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getScope()).isEqualTo(TEST.name()); + } + + @Test + public void formatOperation_should_add_scope_main_on_issue_when_not_unit_test_file() { + componentDto.setQualifier(randomAlphanumeric(5)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getScope()).isEqualTo(MAIN.name()); + } + + @Test + public void formatOperation_should_add_actions_on_issues() { + Set expectedActions = Set.of("actionA", "actionB"); + searchResponseData.addActions(issueDto.getKey(), expectedActions); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getActions().getActionsList()).containsExactlyInAnyOrderElementsOf(expectedActions); + } + + @Test + public void formatOperation_should_add_transitions_on_issues() { + Set expectedTransitions = Set.of("transitionone", "transitiontwo"); + searchResponseData.addTransitions(issueDto.getKey(), createFakeTransitions(expectedTransitions)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getTransitions().getTransitionsList()).containsExactlyInAnyOrderElementsOf(expectedTransitions); + } + + private static List createFakeTransitions(Collection transitions) { + return transitions.stream() + .map(transition -> Transition.builder(transition).from("OPEN").to("RESOLVED").build()) + .collect(toList()); + } + + @Test + public void formatOperation_should_add_comments_on_issues() { + IssueChangeDto issueChangeDto = newIssuechangeDto(issueDto); + searchResponseData.setComments(List.of(issueChangeDto)); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().getComments().getCommentsList()).hasSize(1).extracting(Common.Comment::getKey).containsExactly(issueChangeDto.getKey()); + } + + @Test + public void formatOperation_should_not_set_severity_for_security_hotspot_issue() { + issueDto.setType(SECURITY_HOTSPOT); + + Operation result = searchResponseFormat.formatOperation(searchResponseData); + + assertThat(result.getIssue().hasSeverity()).isFalse(); + } + + private static SearchResponseData newSearchResponseData() { + RuleDto ruleDto = newRule(); + + String projectUuid = "project_uuid_" + randomAlphanumeric(5); + ComponentDto projectDto = newPrivateProjectDto(); + projectDto.setProjectUuid(projectUuid); + + UserDto userDto = newUserDto(); + + IssueDto issueDto = newIssue(ruleDto, projectUuid, "project_key_" + randomAlphanumeric(5), projectDto) + .setType(CODE_SMELL) + .setRuleDescriptionContextKey("context_key_" + randomAlphanumeric(5)) + .setAssigneeUuid(userDto.getUuid()) + .setResolution("resolution_" + randomAlphanumeric(5)) + .setIssueCreationDate(new Date(currentTimeMillis() - 2_000)) + .setIssueUpdateDate(new Date(currentTimeMillis() - 1_000)) + .setIssueCloseDate(new Date(currentTimeMillis())); + + SearchResponseData searchResponseData = new SearchResponseData(issueDto); + searchResponseData.addComponents(List.of(projectDto)); + searchResponseData.addRules(List.of(ruleDto)); + searchResponseData.addUsers(List.of(userDto)); + return searchResponseData; + } + +} diff --git a/sonar-ws/src/main/protobuf/ws-issues.proto b/sonar-ws/src/main/protobuf/ws-issues.proto index 0b7b09fa598..be5c462de27 100644 --- a/sonar-ws/src/main/protobuf/ws-issues.proto +++ b/sonar-ws/src/main/protobuf/ws-issues.proto @@ -158,6 +158,7 @@ message Issue { optional string scope = 35; optional bool quickFixAvailable = 36; + optional string ruleDescriptionContextKey = 37; } message Transitions { -- 2.39.5