From cbe3a6997e7955f03f885b0622fa9c7fa0295463 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Mon, 9 Dec 2019 16:31:59 +0100 Subject: [PATCH] SONAR-12720 add WS api/hotspots/add_comment --- .../server/hotspot/ws/AddCommentAction.java | 88 ++++++ .../server/hotspot/ws/ChangeStatusAction.java | 37 +-- .../server/hotspot/ws/HotspotWsSupport.java | 72 +++++ .../server/hotspot/ws/HotspotsWsModule.java | 2 + .../sonar/server/hotspot/ws/ShowAction.java | 27 +- .../hotspot/ws/AddCommentActionTest.java | 264 ++++++++++++++++++ .../hotspot/ws/ChangeStatusActionTest.java | 3 +- .../hotspot/ws/HotspotsWsModuleTest.java | 2 +- .../server/hotspot/ws/ShowActionTest.java | 3 +- 9 files changed, 448 insertions(+), 50 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsSupport.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AddCommentActionTest.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 index 00000000000..eaf476c1e3e --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java @@ -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(); + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java index 1b80cb9eea0..ffb8048b85a 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java @@ -19,31 +19,23 @@ */ 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 index 00000000000..c4e1b2f7709 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotWsSupport.java @@ -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()); + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java index 555ed0eec62..5d4450c4486 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java @@ -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); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 610556f0483..d2d5085764a 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java @@ -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 index 00000000000..7f8d2eb8207 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/AddCommentActionTest.java @@ -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 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 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 populate) { + RuleDefinitionDto ruleDefinition = RuleTesting.newRule() + .setType(ruleType); + populate.accept(ruleDefinition); + dbTester.rules().insert(ruleDefinition); + return ruleDefinition; + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ChangeStatusActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ChangeStatusActionTest.java index fa9d57c9e90..a6192e51885 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ChangeStatusActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ChangeStatusActionTest.java @@ -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 diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java index dea04b193f0..b7bf86a0a18 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java @@ -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); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java index 062b6e19c32..1ae8411818a 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java @@ -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 -- 2.39.5