]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12720 add WS api/hotspots/add_comment
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 9 Dec 2019 15:31:59 +0000 (16:31 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:28 +0000 (20:46 +0100)
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsSupport.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AddCommentActionTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ChangeStatusActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java

diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java
new file mode 100644 (file)
index 0000000..eaf476c
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.server.issue.IssueFieldsSetter;
+import org.sonar.server.issue.ws.IssueUpdater;
+
+public class AddCommentAction implements HotspotsWsAction {
+  private static final String PARAM_HOTSPOT_KEY = "hotspot";
+  private static final String PARAM_COMMENT = "comment";
+
+  private final DbClient dbClient;
+  private final HotspotWsSupport hotspotWsSupport;
+  private final IssueFieldsSetter issueFieldsSetter;
+  private final IssueUpdater issueUpdater;
+
+  public AddCommentAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport,
+    IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater) {
+    this.dbClient = dbClient;
+    this.hotspotWsSupport = hotspotWsSupport;
+    this.issueFieldsSetter = issueFieldsSetter;
+    this.issueUpdater = issueUpdater;
+  }
+
+  @Override
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller
+      .createAction("add_comment")
+      .setHandler(this)
+      .setPost(true)
+      .setDescription("Add a comment to a Security Hotpot.")
+      .setSince("8.1")
+      .setInternal(true);
+
+    action.createParam(PARAM_HOTSPOT_KEY)
+      .setDescription("Key of the Security Hotspot")
+      .setExampleValue(Uuids.UUID_EXAMPLE_03)
+      .setRequired(true);
+    action.createParam(PARAM_COMMENT)
+      .setDescription("Comment text.")
+      .setExampleValue("This is safe because user input is validated by the calling code")
+      .setRequired(true);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    hotspotWsSupport.checkLoggedIn();
+
+    String hotspotKey = request.mandatoryParam(PARAM_HOTSPOT_KEY);
+    String comment = request.mandatoryParam(PARAM_COMMENT);
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      IssueDto hotspot = hotspotWsSupport.loadHotspot(dbSession, hotspotKey);
+      hotspotWsSupport.loadAndCheckProject(dbSession, hotspot);
+
+      DefaultIssue defaultIssue = hotspot.toDefaultIssue();
+      IssueChangeContext context = hotspotWsSupport.newIssueChangeContext();
+      issueFieldsSetter.addComment(defaultIssue, comment, context);
+      issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, false);
+      response.noContent();
+    }
+  }
+}
index 1b80cb9eea0a0bfd54f4371bfa3e655a33083ee4..ffb8048b85a8af0a6714c9aad0904683ca0ceb54 100644 (file)
  */
 package org.sonar.server.hotspot.ws;
 
-import java.util.Date;
 import java.util.Objects;
 import javax.annotation.Nullable;
 import org.sonar.api.issue.DefaultTransitions;
-import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.utils.System2;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.core.util.Uuids;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
-import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
-import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.TransitionService;
 import org.sonar.server.issue.ws.IssueUpdater;
-import org.sonar.server.user.UserSession;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static java.lang.String.format;
 import static org.apache.commons.lang.StringUtils.trimToNull;
 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
 import static org.sonar.api.issue.Issue.SECURITY_HOTSPOT_RESOLUTIONS;
@@ -58,19 +50,17 @@ public class ChangeStatusAction implements HotspotsWsAction {
   private static final String PARAM_COMMENT = "comment";
 
   private final DbClient dbClient;
-  private final UserSession userSession;
+  private final HotspotWsSupport hotspotWsSupport;
   private final TransitionService transitionService;
   private final IssueFieldsSetter issueFieldsSetter;
-  private final System2 system2;
   private final IssueUpdater issueUpdater;
 
-  public ChangeStatusAction(DbClient dbClient, UserSession userSession, TransitionService transitionService,
-    IssueFieldsSetter issueFieldsSetter, System2 system2, IssueUpdater issueUpdater) {
+  public ChangeStatusAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, TransitionService transitionService,
+    IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater) {
     this.dbClient = dbClient;
-    this.userSession = userSession;
+    this.hotspotWsSupport = hotspotWsSupport;
     this.transitionService = transitionService;
     this.issueFieldsSetter = issueFieldsSetter;
-    this.system2 = system2;
     this.issueUpdater = issueUpdater;
   }
 
@@ -102,16 +92,14 @@ public class ChangeStatusAction implements HotspotsWsAction {
 
   @Override
   public void handle(Request request, Response response) throws Exception {
-    userSession.checkLoggedIn();
+    hotspotWsSupport.checkLoggedIn();
 
     String hotspotKey = request.mandatoryParam(PARAM_HOTSPOT_KEY);
     String newStatus = request.mandatoryParam(PARAM_STATUS);
     String newResolution = resolutionParam(request, newStatus);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      IssueDto hotspot = dbClient.issueDao().selectByKey(dbSession, hotspotKey)
-        .filter(t -> t.getType() == RuleType.SECURITY_HOTSPOT.getDbConstant())
-        .orElseThrow(() -> new NotFoundException(format("Hotspot '%s' does not exist", hotspotKey)));
-      loadAndCheckProject(dbSession, hotspot);
+      IssueDto hotspot = hotspotWsSupport.loadHotspot(dbSession, hotspotKey);
+      hotspotWsSupport.loadAndCheckProject(dbSession, hotspot);
 
       if (needStatusUpdate(hotspot, newStatus, newResolution)) {
         String transitionKey = toTransitionKey(newStatus, newResolution);
@@ -132,15 +120,6 @@ public class ChangeStatusAction implements HotspotsWsAction {
     return resolution;
   }
 
-  private void loadAndCheckProject(DbSession dbSession, IssueDto hotspot) {
-    String projectUuid = hotspot.getProjectUuid();
-    checkArgument(projectUuid != null, "Hotspot '%s' has no project", hotspot.getKee());
-
-    ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, projectUuid)
-      .orElseThrow(() -> new NotFoundException(format("Project with uuid '%s' does not exist", projectUuid)));
-    userSession.checkComponentPermission(UserRole.USER, project);
-  }
-
   private static boolean needStatusUpdate(IssueDto hotspot, String newStatus, String newResolution) {
     return !(hotspot.getStatus().equals(newStatus) && Objects.equals(hotspot.getResolution(), newResolution));
   }
@@ -157,7 +136,7 @@ public class ChangeStatusAction implements HotspotsWsAction {
 
   private void doTransition(DbSession session, IssueDto issueDto, String transitionKey, @Nullable String comment) {
     DefaultIssue defaultIssue = issueDto.toDefaultIssue();
-    IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid());
+    IssueChangeContext context = hotspotWsSupport.newIssueChangeContext();
     transitionService.checkTransitionPermission(transitionKey, defaultIssue);
     if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
       if (comment != null) {
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsSupport.java
new file mode 100644 (file)
index 0000000..c4e1b2f
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import java.util.Date;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+
+public class HotspotWsSupport {
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final System2 system2;
+
+  public HotspotWsSupport(DbClient dbClient, UserSession userSession, System2 system2) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.system2 = system2;
+  }
+
+  String checkLoggedIn() {
+    return userSession.checkLoggedIn().getUuid();
+  }
+
+  IssueDto loadHotspot(DbSession dbSession, String hotspotKey) {
+    return dbClient.issueDao().selectByKey(dbSession, hotspotKey)
+      .filter(t -> t.getType() == RuleType.SECURITY_HOTSPOT.getDbConstant())
+      .orElseThrow(() -> new NotFoundException(format("Hotspot '%s' does not exist", hotspotKey)));
+  }
+
+  ComponentDto loadAndCheckProject(DbSession dbSession, IssueDto hotspot) {
+    String projectUuid = hotspot.getProjectUuid();
+    checkArgument(projectUuid != null, "Hotspot '%s' has no project", hotspot.getKee());
+
+    ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, projectUuid)
+      .orElseThrow(() -> new NotFoundException(format("Project with uuid '%s' does not exist", projectUuid)));
+    userSession.checkComponentPermission(UserRole.USER, project);
+
+    return project;
+  }
+
+  IssueChangeContext newIssueChangeContext() {
+    return IssueChangeContext.createUser(new Date(system2.now()), checkLoggedIn());
+  }
+}
index 555ed0eec62389f0501531a27625b434833afac7..5d4450c4486b91d4615ad0331dec665cdf26d514 100644 (file)
@@ -26,9 +26,11 @@ public class HotspotsWsModule extends Module {
   protected void configureModule() {
     add(
       HotspotWsResponseFormatter.class,
+      HotspotWsSupport.class,
       SearchAction.class,
       ShowAction.class,
       ChangeStatusAction.class,
+      AddCommentAction.class,
       HotspotsWs.class);
   }
 }
index 610556f0483598dd1e1a91d688aecde502b41b36..d2d5085764ab3119bb82f2e50635bab71be8e61b 100644 (file)
@@ -26,11 +26,9 @@ import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.util.Uuids;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -44,7 +42,6 @@ import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext;
 import org.sonar.server.issue.TextRangeResponseFormatter;
 import org.sonar.server.issue.ws.UserResponseFormatter;
 import org.sonar.server.security.SecurityStandards;
-import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Hotspots;
 import org.sonarqube.ws.Hotspots.ShowWsResponse;
@@ -61,17 +58,17 @@ public class ShowAction implements HotspotsWsAction {
   private static final String PARAM_HOTSPOT_KEY = "hotspot";
 
   private final DbClient dbClient;
-  private final UserSession userSession;
+  private final HotspotWsSupport hotspotWsSupport;
   private final HotspotWsResponseFormatter responseFormatter;
   private final TextRangeResponseFormatter textRangeFormatter;
   private final UserResponseFormatter userFormatter;
   private final IssueChangelog issueChangelog;
 
-  public ShowAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter,
-    TextRangeResponseFormatter textRangeFormatter, UserResponseFormatter userFormatter,
-    IssueChangelog issueChangelog) {
+  public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport,
+    HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter,
+    UserResponseFormatter userFormatter, IssueChangelog issueChangelog) {
     this.dbClient = dbClient;
-    this.userSession = userSession;
+    this.hotspotWsSupport = hotspotWsSupport;
     this.responseFormatter = responseFormatter;
     this.textRangeFormatter = textRangeFormatter;
     this.userFormatter = userFormatter;
@@ -99,9 +96,7 @@ public class ShowAction implements HotspotsWsAction {
   public void handle(Request request, Response response) throws Exception {
     String hotspotKey = request.mandatoryParam(PARAM_HOTSPOT_KEY);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      IssueDto hotspot = dbClient.issueDao().selectByKey(dbSession, hotspotKey)
-        .filter(t -> t.getType() == RuleType.SECURITY_HOTSPOT.getDbConstant())
-        .orElseThrow(() -> new NotFoundException(format("Hotspot '%s' does not exist", hotspotKey)));
+      IssueDto hotspot = hotspotWsSupport.loadHotspot(dbSession, hotspotKey);
 
       Components components = loadComponents(dbSession, hotspot);
       Users users = loadUsers(dbSession, hotspot);
@@ -195,16 +190,12 @@ public class ShowAction implements HotspotsWsAction {
   }
 
   private Components loadComponents(DbSession dbSession, IssueDto hotspot) {
-    String projectUuid = hotspot.getProjectUuid();
     String componentUuid = hotspot.getComponentUuid();
-    checkArgument(projectUuid != null, "Hotspot '%s' has no project", hotspot.getKee());
-    checkArgument(componentUuid != null, "Hotspot '%s' has no component", hotspot.getKee());
 
-    ComponentDto project = dbClient.componentDao().selectByUuid(dbSession, projectUuid)
-      .orElseThrow(() -> new NotFoundException(format("Project with uuid '%s' does not exist", projectUuid)));
-    userSession.checkComponentPermission(UserRole.USER, project);
+    ComponentDto project = hotspotWsSupport.loadAndCheckProject(dbSession, hotspot);
 
-    boolean hotspotOnProject = Objects.equals(projectUuid, componentUuid);
+    checkArgument(componentUuid != null, "Hotspot '%s' has no component", hotspot.getKee());
+    boolean hotspotOnProject = Objects.equals(project.uuid(), componentUuid);
     ComponentDto component = hotspotOnProject ? project
       : dbClient.componentDao().selectByUuid(dbSession, componentUuid)
         .orElseThrow(() -> new NotFoundException(format("Component with uuid '%s' does not exist", componentUuid)));
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AddCommentActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AddCommentActionTest.java
new file mode 100644 (file)
index 0000000..7f8d2eb
--- /dev/null
@@ -0,0 +1,264 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.hotspot.ws;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueTesting;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.issue.IssueFieldsSetter;
+import org.sonar.server.issue.ws.IssueUpdater;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.issue.Issue.RESOLUTION_SAFE;
+import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
+import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+@RunWith(DataProviderRunner.class)
+public class AddCommentActionTest {
+  private static final Random RANDOM = new Random();
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+  @Rule
+  public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+  private DbClient dbClient = dbTester.getDbClient();
+  private IssueUpdater issueUpdater = mock(IssueUpdater.class);
+  private System2 system2 = mock(System2.class);
+  private IssueFieldsSetter issueFieldsSetter = mock(IssueFieldsSetter.class);
+  private HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, system2);
+  private AddCommentAction underTest = new AddCommentAction(dbClient, hotspotWsSupport, issueFieldsSetter, issueUpdater);
+  private WsActionTester actionTester = new WsActionTester(underTest);
+
+  @Test
+  public void ws_is_internal() {
+    assertThat(actionTester.getDef().isInternal()).isTrue();
+  }
+
+  @Test
+  public void fails_with_UnauthorizedException_if_user_is_anonymous() {
+    userSessionRule.anonymous();
+
+    TestRequest request = actionTester.newRequest();
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(UnauthorizedException.class)
+      .hasMessage("Authentication is required");
+  }
+
+  @Test
+  public void fails_with_IAE_if_parameter_hotspot_is_missing() {
+    userSessionRule.logIn();
+    TestRequest request = actionTester.newRequest();
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("The 'hotspot' parameter is missing");
+  }
+
+  @Test
+  public void fails_with_IAE_if_parameter_comment_is_missing() {
+    String key = randomAlphabetic(12);
+    userSessionRule.logIn();
+    TestRequest request = actionTester.newRequest()
+      .setParam("hotspot", key);
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("The 'comment' parameter is missing");
+  }
+
+  @Test
+  public void fails_with_NotFoundException_if_hotspot_does_not_exist() {
+    String key = randomAlphabetic(12);
+    userSessionRule.logIn();
+    TestRequest request = actionTester.newRequest()
+      .setParam("hotspot", key)
+      .setParam("comment", randomAlphabetic(10));
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("Hotspot '%s' does not exist", key);
+  }
+
+  @Test
+  @UseDataProvider("ruleTypesByHotspot")
+  public void fails_with_NotFoundException_if_issue_is_not_a_hotspot(RuleType ruleType) {
+    ComponentDto project = dbTester.components().insertPublicProject();
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule = newRule(ruleType);
+    IssueDto notAHotspot = dbTester.issues().insertIssue(IssueTesting.newIssue(rule, project, file).setType(ruleType));
+    userSessionRule.logIn();
+    TestRequest request = newRequest(notAHotspot, randomAlphabetic(12));
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("Hotspot '%s' does not exist", notAHotspot.getKey());
+  }
+
+  @DataProvider
+  public static Object[][] ruleTypesByHotspot() {
+    return Arrays.stream(RuleType.values())
+      .filter(t -> t != RuleType.SECURITY_HOTSPOT)
+      .map(t -> new Object[] {t})
+      .toArray(Object[][]::new);
+  }
+
+  @Test
+  public void fails_with_ForbiddenException_if_project_is_private_and_not_allowed() {
+    ComponentDto project = dbTester.components().insertPrivateProject();
+    userSessionRule.logIn().registerComponents(project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+    IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+    String comment = randomAlphabetic(12);
+    TestRequest request = newRequest(hotspot, comment);
+
+    assertThatThrownBy(request::execute)
+      .isInstanceOf(ForbiddenException.class)
+      .hasMessage("Insufficient privileges");
+  }
+
+  @Test
+  public void succeeds_on_public_project() {
+    ComponentDto project = dbTester.components().insertPublicProject();
+    userSessionRule.logIn().registerComponents(project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+    IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+    String comment = randomAlphabetic(12);
+
+    newRequest(hotspot, comment).execute().assertNoContent();
+  }
+
+  @Test
+  public void succeeds_on_private_project_with_permission() {
+    ComponentDto project = dbTester.components().insertPrivateProject();
+    userSessionRule.logIn().registerComponents(project).addProjectPermission(UserRole.USER, project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+    IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule));
+    String comment = randomAlphabetic(12);
+
+    newRequest(hotspot, comment).execute().assertNoContent();
+  }
+
+  @Test
+  @UseDataProvider("validStatusAndResolutions")
+  public void persists_comment_if_hotspot_status_changes_and_transition_done(String currentStatus, @Nullable String currentResolution) {
+    long now = RANDOM.nextInt(232_323);
+    when(system2.now()).thenReturn(now);
+    ComponentDto project = dbTester.components().insertPublicProject();
+    userSessionRule.logIn().registerComponents(project);
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+    RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT);
+    IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule).setStatus(currentStatus).setResolution(currentResolution));
+    String comment = randomAlphabetic(12);
+
+    newRequest(hotspot, comment).execute().assertNoContent();
+
+    IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(now), userSessionRule.getUuid());
+    ArgumentCaptor<DefaultIssue> defaultIssueCaptor = ArgumentCaptor.forClass(DefaultIssue.class);
+    verify(issueFieldsSetter).addComment(defaultIssueCaptor.capture(), eq(comment), eq(issueChangeContext));
+    verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
+      any(DbSession.class),
+      defaultIssueCaptor.capture(),
+      eq(issueChangeContext),
+      eq(false));
+
+    // because it is mutated by FieldSetter and IssueUpdater, the same object must be passed to all methods
+    List<DefaultIssue> capturedDefaultIssues = defaultIssueCaptor.getAllValues();
+    assertThat(capturedDefaultIssues).hasSize(2);
+    assertThat(capturedDefaultIssues.get(0))
+      .isSameAs(capturedDefaultIssues.get(1));
+  }
+
+  @DataProvider
+  public static Object[][] validStatusAndResolutions() {
+    return new Object[][] {
+      {STATUS_TO_REVIEW, null},
+      {STATUS_REVIEWED, RESOLUTION_FIXED},
+      {STATUS_REVIEWED, RESOLUTION_SAFE}
+    };
+  }
+
+  private static IssueDto newHotspot(ComponentDto project, ComponentDto file, RuleDefinitionDto rule) {
+    return IssueTesting.newIssue(rule, project, file).setType(SECURITY_HOTSPOT);
+  }
+
+  private TestRequest newRequest(IssueDto hotspot, String comment) {
+    return actionTester.newRequest()
+      .setParam("hotspot", hotspot.getKey())
+      .setParam("comment", comment);
+  }
+
+  private RuleDefinitionDto newRule(RuleType ruleType) {
+    return newRule(ruleType, t -> {
+    });
+  }
+
+  private RuleDefinitionDto newRule(RuleType ruleType, Consumer<RuleDefinitionDto> populate) {
+    RuleDefinitionDto ruleDefinition = RuleTesting.newRule()
+      .setType(ruleType);
+    populate.accept(ruleDefinition);
+    dbTester.rules().insert(ruleDefinition);
+    return ruleDefinition;
+  }
+
+}
index fa9d57c9e90f3c6f856e696274e0d52224713426..a6192e51885e23c953d05beadb415ea7f1838314 100644 (file)
@@ -90,7 +90,8 @@ public class ChangeStatusActionTest {
   private IssueUpdater issueUpdater = mock(IssueUpdater.class);
   private System2 system2 = mock(System2.class);
   private IssueFieldsSetter issueFieldsSetter = mock(IssueFieldsSetter.class);
-  private ChangeStatusAction underTest = new ChangeStatusAction(dbClient, userSessionRule, transitionService, issueFieldsSetter, system2, issueUpdater);
+  private HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, system2);
+  private ChangeStatusAction underTest = new ChangeStatusAction(dbClient, hotspotWsSupport, transitionService, issueFieldsSetter, issueUpdater);
   private WsActionTester actionTester = new WsActionTester(underTest);
 
   @Test
index dea04b193f072f83731f349366882ab93c734f8e..b7bf86a0a18911b0ec458a70cd8a16450aee95b7 100644 (file)
@@ -30,7 +30,7 @@ public class HotspotsWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new HotspotsWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 5);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 7);
   }
 
 }
index 062b6e19c32c36cad5d9c3f8b264d7146d0625ff..1ae8411818a40994a07442970ec814a29babebeb 100644 (file)
@@ -101,10 +101,11 @@ public class ShowActionTest {
   private AvatarResolver avatarResolver = new AvatarResolverImpl();
   private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
   private IssueChangelog issueChangelog = Mockito.mock(IssueChangelog.class);
+  private HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE);
   private UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl());
   private TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter();
 
-  private ShowAction underTest = new ShowAction(dbClient, userSessionRule, responseFormatter, textRangeFormatter, userFormatter, issueChangelog);
+  private ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangelog);
   private WsActionTester actionTester = new WsActionTester(underTest);
 
   @Test