]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16614 Modify issues search API to include ruleDescriptionContextKey field
authorAntoine Vinot <antoine.vinot@sonarsource.com>
Wed, 6 Jul 2022 09:36:11 +0000 (11:36 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 8 Jul 2022 20:02:48 +0000 (20:02 +0000)
23 files changed:
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/EditCommentAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAdditionalField.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/add_comment-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/assign-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/delete_comment-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/do_transition-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/edit_comment-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/search-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_severity-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_tags-example.json
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/issue/ws/set_type-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchResponseFormatFormatOperationTest.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-issues.proto

index 9eb76207056ed18ce6950460503948b131cac9af..474894f396dd5b6aebacc057ccc6a86a9efadbb1 100644 (file)
@@ -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."))
index 37c7dec4cb37f801049c5971a4b86d13fb4990c6..701f2cc80c0b98fed547c0fd8268af321de54745 100644 (file)
@@ -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)
index 7089228fa13fa60b6e56e2fa5ba5876b43835df8..ba76cbf24758631c449447ccf1c543caf6a1f5b2 100644 (file)
@@ -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"),
index 2776f28ca5e5c8331d06c45ea1db9c285e0e197f..05e70e96e5c140eaa81b74aee28c3072d4aff7e6 100644 (file)
@@ -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"),
index 676e6a9795dcc231d3831957d0c8308264b292e9..a3d3f51c2306170a4fc6e09af9c9b6ae5b512573 100644 (file)
@@ -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"),
index 12be75097fb7a6e92a4d73ffe0b12f4222d891dd..fe773f380d0dc0fe1bac5725f3ce6074b5b8c16d 100644 (file)
@@ -189,7 +189,8 @@ public class SearchAction implements IssuesWsAction {
         + "<br/>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
index 5620f49d5736d5c9359f5c2133bce0fea3485f39..17b609c3484707eeec19d6672e5ea4e9131fb2a4 100644 (file)
@@ -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<SearchAdditionalField> ALL_ADDITIONAL_FIELDS = EnumSet.allOf(SearchAdditionalField.class);
index 4698db673c5d939e784a6691148a3c791d4e376a..40ff06f7f74508ba0e360184c3087d3e1ec32a52 100644 (file)
@@ -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<Issues.Issue> formatIssues(Set<SearchAdditionalField> fields, SearchResponseData data) {
-    List<Issues.Issue> result = new ArrayList<>();
+  private List<Issues.Issue> createIssues(Collection<SearchAdditionalField> fields, SearchResponseData data) {
+    return data.getIssues().stream()
+      .map(dto -> createIssue(fields, data, dto))
+      .collect(toList());
+  }
+
+  private Issue createIssue(Collection<SearchAdditionalField> 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<SearchAdditionalField> 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<Transition> 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<String> 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<IssueChangeDto> 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) {
index 0150ad3196257120da8dddaa3f569f4a37241320..564736a0d48ec5785f01253bb604a8799a59b6d3 100644 (file)
@@ -78,6 +78,7 @@ public class SetSeverityAction implements IssuesWsAction {
         "</ul>")
       .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)
index f6f363bec9b897f042627f2d4202af4217932c7a..d7f8218b4b3310656068f06d57ec951acebb84ec 100644 (file)
@@ -73,6 +73,7 @@ public class SetTagsAction implements IssuesWsAction {
       .setDescription("Set tags on an issue. <br/>" +
         "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"))
index aee2c8a7fb446996b24954cae37b654843f50760..30cd890e1be4951dbdf5d33a2e2ea0a2eb2714d6 100644 (file)
@@ -82,6 +82,7 @@ public class SetTypeAction implements IssuesWsAction {
         "</ul>")
       .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)
index 6f52df0df9db51ac9fa1d2c10c313ce6390def5f..cbfd2a75f839cee97cdb9dd1bf2487698485bd86 100644 (file)
@@ -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": [
     {
index 16269e44266c083e924e4fc7605db52c714c3135..50f89081597bb6bb5c179c430ee25553384cfded 100644 (file)
@@ -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": [
     {
index 16269e44266c083e924e4fc7605db52c714c3135..50f89081597bb6bb5c179c430ee25553384cfded 100644 (file)
@@ -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": [
     {
index 16269e44266c083e924e4fc7605db52c714c3135..50f89081597bb6bb5c179c430ee25553384cfded 100644 (file)
@@ -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": [
     {
index 16269e44266c083e924e4fc7605db52c714c3135..50f89081597bb6bb5c179c430ee25553384cfded 100644 (file)
@@ -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": [
     {
index 4c8efbada3ebb9d3d195d85e6eb8472a05a33f34..ba19cc0b7df483966b76dc441079f6f97af75160 100644 (file)
@@ -77,7 +77,8 @@
           ]
         }
       ],
-      "quickFixAvailable": false
+      "quickFixAvailable": false,
+      "ruleDescriptionContextKey": "spring"
     }
   ],
   "components": [
index 16269e44266c083e924e4fc7605db52c714c3135..50f89081597bb6bb5c179c430ee25553384cfded 100644 (file)
@@ -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": [
     {
index 1ccdedc6d71907193b4a04952fb22356c07c4eba..58865766e66d87175b840927b6e8a47c26ed7ea8 100644 (file)
@@ -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": [
     {
index 16269e44266c083e924e4fc7605db52c714c3135..50f89081597bb6bb5c179c430ee25553384cfded 100644 (file)
@@ -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": [
     {
index 03ecfc9eaeaf11c601273010b101104cffd6860c..0d1d86a81e2416a7411838e5ac82ab3006b585d0 100644 (file)
@@ -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 (file)
index 0000000..c5f5878
--- /dev/null
@@ -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<String> 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<String> 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<Transition> createFakeTransitions(Collection<String> 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;
+  }
+
+}
index 0b7b09fa59810bad017356419da7b17ea8d4103b..be5c462de278f84895ee42b0928de4d47adf1f26 100644 (file)
@@ -158,6 +158,7 @@ message Issue {
   optional string scope = 35;
 
   optional bool quickFixAvailable = 36;
+  optional string ruleDescriptionContextKey = 37;
 }
 
 message Transitions {