@@ -1,3 +1,22 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.db.issue; | |||
import org.junit.Rule; | |||
@@ -45,4 +64,44 @@ public class AnticipatedTransitionDaoIT { | |||
var anticipatedTransitionDtosDeleted = underTest.selectByProjectUuid(db.getSession(), projectUuid); | |||
assertThat(anticipatedTransitionDtosDeleted).isEmpty(); | |||
} | |||
@Test | |||
public void deleteByProjectAndUser_shouldDeleteAllRelatedRecords() { | |||
// given | |||
final String projectUuid1 = "project1"; | |||
final String projectUuid2 = "project2"; | |||
String userUuid1 = "user1"; | |||
String userUuid2 = "user2"; | |||
generateAndInsertAnticipatedTransition("uuid1", projectUuid1, userUuid1); // should be deleted | |||
generateAndInsertAnticipatedTransition("uuid2", projectUuid1, userUuid1); // should be deleted | |||
generateAndInsertAnticipatedTransition("uuid3", projectUuid2, userUuid1); | |||
generateAndInsertAnticipatedTransition("uuid4", projectUuid1, userUuid2); | |||
generateAndInsertAnticipatedTransition("uuid5", projectUuid2, userUuid2); | |||
generateAndInsertAnticipatedTransition("uuid6", projectUuid1, userUuid1); // should be deleted | |||
// when | |||
underTest.deleteByProjectAndUser(db.getSession(), projectUuid1, userUuid1); | |||
// then | |||
assertThat(underTest.selectByProjectUuid(db.getSession(), projectUuid1)).hasSize(1); | |||
assertThat(underTest.selectByProjectUuid(db.getSession(), projectUuid2)).hasSize(2); | |||
} | |||
private void generateAndInsertAnticipatedTransition(String uuid, String projectUuid1, String userUuid1) { | |||
AnticipatedTransitionDto transition = new AnticipatedTransitionDto( | |||
uuid, | |||
projectUuid1, | |||
userUuid1, | |||
"transition", | |||
"status", | |||
"comment", | |||
1, | |||
"message", | |||
"lineHash", | |||
"ruleKey"); | |||
// insert one | |||
underTest.insert(db.getSession(), transition); | |||
} | |||
} |
@@ -1,3 +1,22 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.db.issue; | |||
import java.util.List; | |||
@@ -14,6 +33,10 @@ public class AnticipatedTransitionDao implements Dao { | |||
mapper(session).delete(uuid); | |||
} | |||
public void deleteByProjectAndUser(DbSession dbSession, String projectUuid, String userUuid) { | |||
mapper(dbSession).deleteByProjectAndUser(projectUuid, userUuid); | |||
} | |||
public List<AnticipatedTransitionDto> selectByProjectUuid(DbSession session, String projectUuid) { | |||
return mapper(session).selectByProjectUuid(projectUuid); | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.db.issue; | |||
import javax.annotation.Nullable; | |||
import org.sonar.core.issue.AnticipatedTransition; | |||
public class AnticipatedTransitionDto { | |||
private String uuid; | |||
@@ -138,4 +139,18 @@ public class AnticipatedTransitionDto { | |||
public void setRuleKey(String ruleKey) { | |||
this.ruleKey = ruleKey; | |||
} | |||
public static AnticipatedTransitionDto toDto(AnticipatedTransition anticipatedTransition, String uuid, String projectUuid) { | |||
return new AnticipatedTransitionDto( | |||
uuid, | |||
projectUuid, | |||
anticipatedTransition.getUserUuid(), | |||
anticipatedTransition.getTransition(), | |||
anticipatedTransition.getStatus(), | |||
anticipatedTransition.getComment(), | |||
anticipatedTransition.getLine(), | |||
anticipatedTransition.getMessage(), | |||
anticipatedTransition.getLineHash(), | |||
anticipatedTransition.getRuleKey().toString()); | |||
} | |||
} |
@@ -27,5 +27,7 @@ public interface AnticipatedTransitionMapper { | |||
void delete(@Param("uuid") String uuid); | |||
void deleteByProjectAndUser(@Param("projectUuid") String projectUuid, @Param("userUuid") String userUuid); | |||
List<AnticipatedTransitionDto> selectByProjectUuid(@Param("projectUuid") String projectUuid); | |||
} |
@@ -30,6 +30,10 @@ | |||
delete from anticipated_transitions where uuid=#{uuid} | |||
</delete> | |||
<delete id="deleteByProjectAndUser" parameterType="string"> | |||
delete from anticipated_transitions where project_uuid=#{projectUuid,jdbcType=VARCHAR} and user_uuid=#{userUuid,jdbcType=VARCHAR} | |||
</delete> | |||
<select id="selectByProjectUuid" parameterType="string" resultType="AnticipatedTransition"> | |||
select | |||
<include refid="anticipatedTransitionsColumns"/> |
@@ -0,0 +1,240 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.io.IOException; | |||
import java.nio.file.Files; | |||
import java.nio.file.Path; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.resources.ResourceTypeTree; | |||
import org.sonar.api.resources.ResourceTypes; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.core.component.DefaultResourceTypes; | |||
import org.sonar.core.util.SequenceUuidFactory; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.issue.AnticipatedTransitionDao; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.TestResponse; | |||
import org.sonar.server.ws.WsActionTester; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.sonar.api.web.UserRole.CODEVIEWER; | |||
import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
import static org.sonar.db.component.ProjectTesting.newPrivateProjectDto; | |||
public class AnticipatedTransitionsActionIT { | |||
private static final String PROJECT_UUID = "123"; | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private final ComponentFinder componentFinder = new ComponentFinder(db.getDbClient(), new ResourceTypes(new ResourceTypeTree[]{DefaultResourceTypes.get()})); | |||
private final AnticipatedTransitionsActionValidator validator = new AnticipatedTransitionsActionValidator(db.getDbClient(), componentFinder, userSession); | |||
private final UuidFactory uuidFactory = new SequenceUuidFactory(); | |||
private final AnticipatedTransitionDao anticipatedTransitionDao = db.getDbClient().anticipatedTransitionDao(); | |||
private final AnticipatedTransitionParser anticipatedTransitionParser = new AnticipatedTransitionParser(); | |||
private final AnticipatedTransitionHandler handler = new AnticipatedTransitionHandler(anticipatedTransitionParser, anticipatedTransitionDao, uuidFactory, db.getDbClient()); | |||
private final WsActionTester ws = new WsActionTester(new AnticipatedTransitionsAction(validator, handler)); | |||
@Test | |||
public void definition() { | |||
var definition = ws.getDef(); | |||
assertThat(definition.key()).isEqualTo("anticipated_transitions"); | |||
assertThat(definition.description()).isEqualTo(""" | |||
Receive a list of anticipated transitions that can be applied to not yet discovered issues on a specific project.<br> | |||
Requires the following permission: 'Administer' on the specified project.<br><br> | |||
Upon successful execution, the HTTP status code returned is 202 (Accepted).<br><br> | |||
Request example: | |||
<pre><code> | |||
[ | |||
{ | |||
"ruleKey": "squid:S0001", | |||
"issueMessage": "issueMessage1", | |||
"filePath": "filePath1", | |||
"line": 1, | |||
"lineHash": "lineHash1", | |||
"transition": "transition1", | |||
"comment": "comment1" | |||
}, | |||
{ | |||
"ruleKey": "squid:S0002", | |||
"issueMessage": "issueMessage2", | |||
"filePath": "filePath2", | |||
"line": 2, | |||
"lineHash": "lineHash2", | |||
"transition": "transition2", | |||
"comment": "comment2" | |||
} | |||
]"""); | |||
assertThat(definition.isPost()).isTrue(); | |||
assertThat(definition.isInternal()).isTrue(); | |||
assertThat(definition.params()).extracting(WebService.Param::key, WebService.Param::isRequired, WebService.Param::description, WebService.Param::since).containsExactlyInAnyOrder( | |||
tuple("projectKey", true, "The key of the project", "10.2") | |||
); | |||
} | |||
@Test | |||
public void givenRequestWithTransitions_whenHandle_thenAllTransitionsAreSaved() throws IOException { | |||
// given | |||
ProjectDto projectDto = mockProjectDto(); | |||
mockUser(projectDto, ISSUE_ADMIN); | |||
String requestBody = readTestResourceFile("request-with-transitions.json"); | |||
// when | |||
TestResponse response = getTestRequest(projectDto, requestBody).execute(); | |||
// then | |||
assertThat(anticipatedTransitionDao.selectByProjectUuid(db.getSession(), projectDto.getUuid())).hasSize(2); | |||
assertThat(response.getStatus()).isEqualTo(202); | |||
} | |||
@Test | |||
public void givenRequestWithNoTransitions_whenHandle_thenNoTransitionsAreSaved() { | |||
// given | |||
ProjectDto projectDto = mockProjectDto(); | |||
mockUser(projectDto, ISSUE_ADMIN); | |||
String requestBody = "[]"; | |||
// when | |||
TestResponse response = getTestRequest(projectDto, requestBody).execute(); | |||
// then | |||
assertThat(anticipatedTransitionDao.selectByProjectUuid(db.getSession(), projectDto.getUuid())).isEmpty(); | |||
assertThat(response.getStatus()).isEqualTo(202); | |||
} | |||
@Test | |||
public void givenRequestWithInvalidBody_whenHandle_thenExceptionIsThrown() { | |||
// given | |||
ProjectDto projectDto = mockProjectDto(); | |||
mockUser(projectDto, ISSUE_ADMIN); | |||
String requestBody = "invalidJson"; | |||
// when then | |||
TestRequest request = getTestRequest(projectDto, requestBody); | |||
assertThatThrownBy(request::execute) | |||
.hasMessage("Unable to parse anticipated transitions from request body.") | |||
.isInstanceOf(IllegalStateException.class); | |||
} | |||
@Test | |||
public void givenTransitionsForUserAndProjectAlreadyExistInDb_whenHandle_thenTheNewTransitionsShouldReplaceTheOldOnes() throws IOException { | |||
// given | |||
ProjectDto projectDto = mockProjectDto(); | |||
mockUser(projectDto, ISSUE_ADMIN); | |||
String requestBody = readTestResourceFile("request-with-transitions.json"); | |||
TestResponse response1 = getTestRequest(projectDto, requestBody).execute(); | |||
assertThat(anticipatedTransitionDao.selectByProjectUuid(db.getSession(), projectDto.getUuid())).hasSize(2); | |||
assertThat(response1.getStatus()).isEqualTo(202); | |||
// when | |||
String requestBody2 = """ | |||
[ | |||
{ | |||
"ruleKey": "squid:S0003", | |||
"issueMessage": "issueMessage3", | |||
"filePath": "filePath3", | |||
"line": 3, | |||
"lineHash": "lineHash3", | |||
"transition": "transition3", | |||
"comment": "comment3" | |||
} | |||
]"""; | |||
TestResponse response2 = getTestRequest(projectDto, requestBody2).execute(); | |||
// then | |||
assertThat(anticipatedTransitionDao.selectByProjectUuid(db.getSession(), projectDto.getUuid())).hasSize(1); | |||
assertThat(response2.getStatus()).isEqualTo(202); | |||
} | |||
@Test | |||
public void givenRequestWithNoTransitions_whenHandle_thenExistingTransitionsForUserAndProjectShouldBePurged() throws IOException { | |||
// given | |||
ProjectDto projectDto = mockProjectDto(); | |||
mockUser(projectDto, ISSUE_ADMIN); | |||
String requestBody = readTestResourceFile("request-with-transitions.json"); | |||
TestResponse response1 = getTestRequest(projectDto, requestBody).execute(); | |||
assertThat(anticipatedTransitionDao.selectByProjectUuid(db.getSession(), projectDto.getUuid())).hasSize(2); | |||
assertThat(response1.getStatus()).isEqualTo(202); | |||
// when | |||
String requestBody2 = "[]"; | |||
TestResponse response2 = getTestRequest(projectDto, requestBody2).execute(); | |||
// then | |||
assertThat(anticipatedTransitionDao.selectByProjectUuid(db.getSession(), projectDto.getUuid())).isEmpty(); | |||
assertThat(response2.getStatus()).isEqualTo(202); | |||
} | |||
@Test | |||
public void givenUserWithoutAdminIssuesPermission_whenHandle_thenThrowException() throws IOException { | |||
// given | |||
ProjectDto projectDto = mockProjectDto(); | |||
mockUser(projectDto, CODEVIEWER); | |||
String requestBody = readTestResourceFile("request-with-transitions.json"); | |||
// when | |||
TestRequest request = getTestRequest(projectDto, requestBody); | |||
// then | |||
assertThatThrownBy(request::execute) | |||
.hasMessage("Insufficient privileges") | |||
.isInstanceOf(ForbiddenException.class); | |||
} | |||
private TestRequest getTestRequest(ProjectDto projectDto, String requestBody) { | |||
return ws.newRequest() | |||
.setParam("projectKey", projectDto.getKey()) | |||
.setMethod("POST") | |||
.setMediaType("application/json") | |||
.setPayload(requestBody); | |||
} | |||
private void mockUser(ProjectDto projectDto, String permission) { | |||
UserDto user = db.users().insertUser(); | |||
db.users().insertProjectPermissionOnUser(user, permission, projectDto); | |||
userSession.logIn(user); | |||
} | |||
private ProjectDto mockProjectDto() { | |||
ProjectDto projectDto = newPrivateProjectDto(PROJECT_UUID); | |||
db.getDbClient().projectDao().insert(db.getSession(), projectDto); | |||
db.commit(); | |||
return projectDto; | |||
} | |||
private String readTestResourceFile(String fileName) throws IOException { | |||
return Files.readString(Path.of(getClass().getResource(fileName).getPath())); | |||
} | |||
} |
@@ -32,6 +32,10 @@ import org.sonar.server.issue.WebIssueStorage; | |||
import org.sonar.server.issue.index.IssueQueryFactory; | |||
import org.sonar.server.issue.workflow.FunctionExecutor; | |||
import org.sonar.server.issue.workflow.IssueWorkflow; | |||
import org.sonar.server.issue.ws.anticipatedtransition.AnticipatedTransitionHandler; | |||
import org.sonar.server.issue.ws.anticipatedtransition.AnticipatedTransitionParser; | |||
import org.sonar.server.issue.ws.anticipatedtransition.AnticipatedTransitionsAction; | |||
import org.sonar.server.issue.ws.anticipatedtransition.AnticipatedTransitionsActionValidator; | |||
import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator; | |||
import org.sonar.server.issue.ws.pull.PullActionResponseWriter; | |||
import org.sonar.server.issue.ws.pull.PullTaintActionProtobufObjectGenerator; | |||
@@ -80,6 +84,11 @@ public class IssueWsModule extends Module { | |||
PullTaintAction.class, | |||
PullActionResponseWriter.class, | |||
PullActionProtobufObjectGenerator.class, | |||
PullTaintActionProtobufObjectGenerator.class); | |||
PullTaintActionProtobufObjectGenerator.class, | |||
AnticipatedTransitionParser.class, | |||
AnticipatedTransitionHandler.class, | |||
AnticipatedTransitionsActionValidator.class, | |||
AnticipatedTransitionsAction.class | |||
); | |||
} | |||
} |
@@ -0,0 +1,69 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.util.List; | |||
import org.sonar.core.issue.AnticipatedTransition; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.issue.AnticipatedTransitionDao; | |||
import org.sonar.db.issue.AnticipatedTransitionDto; | |||
import org.sonar.db.project.ProjectDto; | |||
public class AnticipatedTransitionHandler { | |||
private final AnticipatedTransitionParser anticipatedTransitionParser; | |||
private final AnticipatedTransitionDao anticipatedTransitionDao; | |||
private final UuidFactory uuidFactory; | |||
private final DbClient dbClient; | |||
public AnticipatedTransitionHandler(AnticipatedTransitionParser anticipatedTransitionParser, | |||
AnticipatedTransitionDao anticipatedTransitionDao, UuidFactory uuidFactory, DbClient dbClient) { | |||
this.anticipatedTransitionParser = anticipatedTransitionParser; | |||
this.anticipatedTransitionDao = anticipatedTransitionDao; | |||
this.uuidFactory = uuidFactory; | |||
this.dbClient = dbClient; | |||
} | |||
public void handleRequestBody(String requestBody, String userUuid, ProjectDto projectDto) { | |||
// parse anticipated transitions from request body | |||
List<AnticipatedTransition> anticipatedTransitions = anticipatedTransitionParser.parse(requestBody, userUuid, projectDto.getKey()); | |||
try (DbSession dbSession = dbClient.openSession(true)) { | |||
// delete previous anticipated transitions for the user and project | |||
deletePreviousAnticipatedTransitionsForUserAndProject(dbSession, userUuid, projectDto.getUuid()); | |||
// insert new anticipated transitions | |||
insertAnticipatedTransitions(dbSession, anticipatedTransitions, projectDto.getUuid()); | |||
dbSession.commit(); | |||
} | |||
} | |||
private void deletePreviousAnticipatedTransitionsForUserAndProject(DbSession dbSession, String userUuid, String projectUuid) { | |||
anticipatedTransitionDao.deleteByProjectAndUser(dbSession, projectUuid, userUuid); | |||
} | |||
private void insertAnticipatedTransitions(DbSession dbSession, List<AnticipatedTransition> anticipatedTransitions, String projectUuid) { | |||
anticipatedTransitions.forEach(anticipatedTransition -> | |||
anticipatedTransitionDao.insert(dbSession, AnticipatedTransitionDto.toDto(anticipatedTransition, uuidFactory.create(), projectUuid))); | |||
} | |||
} |
@@ -0,0 +1,56 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import com.google.gson.Gson; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.core.issue.AnticipatedTransition; | |||
public class AnticipatedTransitionParser { | |||
private static final Gson GSON = new Gson(); | |||
public List<AnticipatedTransition> parse(String requestBody, String userUuid, String projectKey) { | |||
try { | |||
List<GsonAnticipatedTransition> anticipatedTransitions = Arrays.asList(GSON.fromJson(requestBody, GsonAnticipatedTransition[].class)); | |||
return mapBodyToAnticipatedTransitions(anticipatedTransitions, userUuid, projectKey); | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Unable to parse anticipated transitions from request body.", e); | |||
} | |||
} | |||
private static List<AnticipatedTransition> mapBodyToAnticipatedTransitions(List<GsonAnticipatedTransition> anticipatedTransitions, String userUuid, String projectKey) { | |||
return anticipatedTransitions.stream() | |||
.map(anticipatedTransition -> new AnticipatedTransition( | |||
projectKey, | |||
"branch", | |||
userUuid, | |||
RuleKey.parse(anticipatedTransition.ruleKey()), | |||
anticipatedTransition.message(), | |||
anticipatedTransition.filePath(), | |||
anticipatedTransition.line(), | |||
anticipatedTransition.lineHash(), | |||
anticipatedTransition.transition(), | |||
anticipatedTransition.comment())) | |||
.toList(); | |||
} | |||
} |
@@ -0,0 +1,105 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.io.BufferedReader; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.server.issue.ws.IssuesWsAction; | |||
import org.sonarqube.ws.client.issue.IssuesWsParameters; | |||
import static java.net.HttpURLConnection.HTTP_ACCEPTED; | |||
public class AnticipatedTransitionsAction implements IssuesWsAction { | |||
private static final String PARAM_PROJECT_KEY = "projectKey"; | |||
private final AnticipatedTransitionsActionValidator validator; | |||
private final AnticipatedTransitionHandler anticipatedTransitionHandler; | |||
public AnticipatedTransitionsAction(AnticipatedTransitionsActionValidator validator, AnticipatedTransitionHandler anticipatedTransitionHandler) { | |||
this.validator = validator; | |||
this.anticipatedTransitionHandler = anticipatedTransitionHandler; | |||
} | |||
@Override | |||
public void define(WebService.NewController controller) { | |||
WebService.NewAction action = controller.createAction(IssuesWsParameters.ACTION_ANTICIPATED_TRANSITIONS).setDescription(""" | |||
Receive a list of anticipated transitions that can be applied to not yet discovered issues on a specific project.<br> | |||
Requires the following permission: 'Administer' on the specified project.<br><br> | |||
Upon successful execution, the HTTP status code returned is 202 (Accepted).<br><br> | |||
Request example: | |||
<pre><code> | |||
[ | |||
{ | |||
"ruleKey": "squid:S0001", | |||
"issueMessage": "issueMessage1", | |||
"filePath": "filePath1", | |||
"line": 1, | |||
"lineHash": "lineHash1", | |||
"transition": "transition1", | |||
"comment": "comment1" | |||
}, | |||
{ | |||
"ruleKey": "squid:S0002", | |||
"issueMessage": "issueMessage2", | |||
"filePath": "filePath2", | |||
"line": 2, | |||
"lineHash": "lineHash2", | |||
"transition": "transition2", | |||
"comment": "comment2" | |||
} | |||
]""") | |||
.setSince("10.2") | |||
.setHandler(this) | |||
.setInternal(true) | |||
.setPost(true); | |||
action.createParam(PARAM_PROJECT_KEY) | |||
.setDescription("Project key") | |||
.setRequired(true) | |||
.setDescription("The key of the project") | |||
.setExampleValue("my_project") | |||
.setSince("10.2"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) throws Exception { | |||
// validation | |||
String userUuid = validator.validateUserLoggedIn(); | |||
ProjectDto projectDto = validator.validateProjectKey(request.mandatoryParam(PARAM_PROJECT_KEY)); | |||
validator.validateUserHasAdministerIssuesPermission(projectDto.getUuid()); | |||
String requestBody = getRequestBody(request); | |||
anticipatedTransitionHandler.handleRequestBody(requestBody, userUuid, projectDto); | |||
response.stream().setStatus(HTTP_ACCEPTED); | |||
} | |||
private static String getRequestBody(Request request) { | |||
BufferedReader reader = request.getReader(); | |||
StringBuilder sb = new StringBuilder(); | |||
reader.lines().forEach(sb::append); | |||
return sb.toString(); | |||
} | |||
} |
@@ -0,0 +1,69 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.util.Objects; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException; | |||
public class AnticipatedTransitionsActionValidator { | |||
private final DbClient dbClient; | |||
private final ComponentFinder componentFinder; | |||
private final UserSession userSession; | |||
public AnticipatedTransitionsActionValidator(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) { | |||
this.dbClient = dbClient; | |||
this.componentFinder = componentFinder; | |||
this.userSession = userSession; | |||
} | |||
public ProjectDto validateProjectKey(String projectKey) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
return componentFinder.getProjectByKey(dbSession, projectKey); | |||
} catch (NotFoundException e) { | |||
// To hide information about the existence or not of the project. | |||
throw insufficientPrivilegesException(); | |||
} | |||
} | |||
public String validateUserLoggedIn() { | |||
userSession.checkLoggedIn(); | |||
return userSession.getUuid(); | |||
} | |||
public void validateUserHasAdministerIssuesPermission(String projectUuid) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
String userUuid = Objects.requireNonNull(userSession.getUuid()); | |||
if (!dbClient.authorizationDao().selectEntityPermissions(dbSession, projectUuid, userUuid).contains(ISSUE_ADMIN)){ | |||
throw insufficientPrivilegesException(); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import com.google.gson.annotations.SerializedName; | |||
public record GsonAnticipatedTransition(@SerializedName("ruleKey") String ruleKey, @SerializedName("issueMessage") String message, | |||
@SerializedName("filePath") String filePath, @SerializedName("line") Integer line, | |||
@SerializedName("lineHash") String lineHash, @SerializedName("transition") String transition, | |||
@SerializedName("comment") String comment) { | |||
} |
@@ -0,0 +1,111 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.util.List; | |||
import org.junit.Test; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.core.issue.AnticipatedTransition; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.issue.AnticipatedTransitionDao; | |||
import org.sonar.db.project.ProjectDto; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.Mockito.doReturn; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.never; | |||
import static org.mockito.Mockito.times; | |||
import static org.mockito.Mockito.verify; | |||
public class AnticipatedTransitionHandlerTest { | |||
private static final UuidFactory UUID_FACTORY = mock(UuidFactory.class); | |||
public static final String USER_UUID = "userUuid"; | |||
public static final String PROJECT_KEY = "projectKey"; | |||
private final AnticipatedTransitionParser anticipatedTransitionParser = mock(AnticipatedTransitionParser.class); | |||
private final AnticipatedTransitionDao anticipatedTransitionDao = mock(AnticipatedTransitionDao.class); | |||
private final DbClient dbClient = mock(DbClient.class); | |||
private final AnticipatedTransitionHandler underTest = new AnticipatedTransitionHandler(anticipatedTransitionParser, anticipatedTransitionDao, UUID_FACTORY, dbClient); | |||
@Test | |||
public void givenRequestBodyWithNoTransitions_whenHandleRequestBody_thenPreviousTransitionsArePurged() { | |||
// given | |||
ProjectDto projectDto = new ProjectDto() | |||
.setKey(PROJECT_KEY); | |||
String requestBody = "body_with_no_transitions"; | |||
doReturn(List.of()) | |||
.when(anticipatedTransitionParser).parse(requestBody, USER_UUID, PROJECT_KEY); | |||
DbSession dbSession = mockDbSession(); | |||
// when | |||
underTest.handleRequestBody(requestBody, USER_UUID, projectDto); | |||
// then | |||
verify(dbClient).openSession(true); | |||
verify(anticipatedTransitionDao).deleteByProjectAndUser(dbSession, projectDto.getUuid(), USER_UUID); | |||
verify(anticipatedTransitionDao, never()).insert(eq(dbSession), any()); | |||
} | |||
@Test | |||
public void fivenRequestBodyWithTransitions_whenHandleRequestBody_thenTransitionsAreInserted() { | |||
// given | |||
ProjectDto projectDto = new ProjectDto() | |||
.setKey(PROJECT_KEY); | |||
String requestBody = "body_with_transitions"; | |||
doReturn(List.of(populateAnticipatedTransition(), populateAnticipatedTransition())) | |||
.when(anticipatedTransitionParser).parse(requestBody, USER_UUID, PROJECT_KEY); | |||
DbSession dbSession = mockDbSession(); | |||
// when | |||
underTest.handleRequestBody(requestBody, USER_UUID, projectDto); | |||
// then | |||
verify(dbClient).openSession(true); | |||
verify(anticipatedTransitionDao).deleteByProjectAndUser(dbSession, projectDto.getUuid(), USER_UUID); | |||
verify(anticipatedTransitionDao, times(2)).insert(eq(dbSession), any()); | |||
} | |||
private DbSession mockDbSession() { | |||
DbSession dbSession = mock(DbSession.class); | |||
doReturn(dbSession).when(dbClient).openSession(true); | |||
return dbSession; | |||
} | |||
private AnticipatedTransition populateAnticipatedTransition() { | |||
return new AnticipatedTransition( | |||
PROJECT_KEY, | |||
"branch", | |||
USER_UUID, | |||
RuleKey.of("repo", "squid:S0001"), | |||
"issueMessage1", | |||
"filePath1", | |||
1, | |||
"lineHash1", | |||
"transition1", | |||
"comment1"); | |||
} | |||
} |
@@ -0,0 +1,108 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.io.IOException; | |||
import java.nio.file.Files; | |||
import java.nio.file.Path; | |||
import java.util.List; | |||
import org.assertj.core.api.Assertions; | |||
import org.junit.Test; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.core.issue.AnticipatedTransition; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class AnticipatedTransitionParserTest { | |||
private static final String USER_UUID = "userUuid"; | |||
private static final String PROJECT_KEY = "projectKey"; | |||
AnticipatedTransitionParser underTest = new AnticipatedTransitionParser(); | |||
@Test | |||
public void givenRequestBodyWithMultipleTransition_whenParse_thenAllTransitionsAreReturned() throws IOException { | |||
// given | |||
String requestBody = readTestResourceFile("request-with-transitions.json"); | |||
// when | |||
List<AnticipatedTransition> anticipatedTransitions = underTest.parse(requestBody, USER_UUID, PROJECT_KEY); | |||
// then | |||
// assert that all transitions are returned | |||
assertThat(anticipatedTransitions) | |||
.hasSize(2) | |||
.containsExactlyElementsOf(transitionsExpectedFromTestFile()); | |||
} | |||
@Test | |||
public void givenRequestBodyWithNoTransitions_whenParse_ThenAnEmptyListIsReturned() { | |||
// given | |||
String requestBody = "[]"; | |||
// when | |||
List<AnticipatedTransition> anticipatedTransitions = underTest.parse(requestBody, USER_UUID, PROJECT_KEY); | |||
// then | |||
assertThat(anticipatedTransitions).isEmpty(); | |||
} | |||
@Test | |||
public void givenRequestBodyWithInvalidJson_whenParse_thenExceptionIsThrown() { | |||
// given | |||
String requestBody = "invalidJson"; | |||
// when then | |||
Assertions.assertThatThrownBy(() -> underTest.parse(requestBody, USER_UUID, PROJECT_KEY)) | |||
.isInstanceOf(IllegalStateException.class) | |||
.hasMessage("Unable to parse anticipated transitions from request body."); | |||
} | |||
// Handwritten Anticipated Transitions that are expected from the request-with-transitions.json file | |||
private List<AnticipatedTransition> transitionsExpectedFromTestFile() { | |||
return List.of( | |||
new AnticipatedTransition( | |||
PROJECT_KEY, | |||
"branch", | |||
USER_UUID, | |||
RuleKey.parse("squid:S0001"), | |||
"issueMessage1", | |||
"filePath1", | |||
1, | |||
"lineHash1", | |||
"transition1", | |||
"comment1"), | |||
new AnticipatedTransition( | |||
PROJECT_KEY, | |||
"branch", | |||
USER_UUID, | |||
RuleKey.parse("squid:S0002"), | |||
"issueMessage2", | |||
"filePath2", | |||
2, | |||
"lineHash2", | |||
"transition2", | |||
"comment2")); | |||
} | |||
private String readTestResourceFile(String fileName) throws IOException { | |||
return Files.readString(Path.of(getClass().getResource(fileName).getPath())); | |||
} | |||
} |
@@ -0,0 +1,137 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.anticipatedtransition; | |||
import java.util.Set; | |||
import org.junit.Test; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.permission.AuthorizationDao; | |||
import org.sonar.db.project.ProjectDto; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.user.UserSession; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatCode; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.Mockito.doReturn; | |||
import static org.mockito.Mockito.doThrow; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.times; | |||
import static org.mockito.Mockito.verify; | |||
import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
public class AnticipatedTransitionsActionValidatorTest { | |||
private final DbClient dbClient = mock(DbClient.class); | |||
private final ComponentFinder componentFinder = mock(ComponentFinder.class); | |||
private final UserSession userSession = mock(UserSession.class); | |||
AnticipatedTransitionsActionValidator underTest = new AnticipatedTransitionsActionValidator(dbClient, componentFinder, userSession); | |||
@Test | |||
public void givenValidProjectKey_whenValidateProjectKey_thenReturnProjectDto() { | |||
// given | |||
String projectKey = "validProjectKey"; | |||
DbSession dbSession = mockDbSession(); | |||
ProjectDto projectDto = new ProjectDto(); | |||
doReturn(projectDto).when(componentFinder).getProjectByKey(dbSession, projectKey); | |||
// when | |||
ProjectDto returnedProjectDto = underTest.validateProjectKey(projectKey); | |||
// then | |||
assertThat(projectDto).isEqualTo(returnedProjectDto); | |||
} | |||
@Test | |||
public void givenInvalidProjectKey_whenValidateProjectKey_thenThrowForbiddenException() { | |||
// given | |||
String projectKey = "invalidProjectKey"; | |||
DbSession dbSession = mockDbSession(); | |||
doThrow(NotFoundException.class).when(componentFinder).getProjectByKey(dbSession, projectKey); | |||
// when then | |||
assertThatThrownBy(() -> underTest.validateProjectKey(projectKey)) | |||
.withFailMessage("Insufficient privileges") | |||
.isInstanceOf(ForbiddenException.class); | |||
} | |||
@Test | |||
public void givenUserLoggedIn_whenValidateUserLoggedIn_thenReturnUserUuid() { | |||
// given | |||
String userUuid = "userUuid"; | |||
doReturn(userUuid).when(userSession).getUuid(); | |||
// when | |||
String returnedUserUuid = underTest.validateUserLoggedIn(); | |||
// then | |||
assertThat(returnedUserUuid).isEqualTo(userUuid); | |||
} | |||
@Test | |||
public void givenUserHasAdministerIssuesPermission_whenValidateUserHasAdministerIssuesPermission_thenDoNothing() { | |||
// given | |||
String userUuid = "userUuid"; | |||
doReturn(userUuid).when(userSession).getUuid(); | |||
String projectUuid = "projectUuid"; | |||
DbSession dbSession = mockDbSession(); | |||
AuthorizationDao authorizationDao = mockAuthorizationDao(); | |||
doReturn(Set.of("permission1", ISSUE_ADMIN)).when(authorizationDao).selectEntityPermissions(dbSession, projectUuid, userUuid); | |||
// when, then | |||
assertThatCode(() -> underTest.validateUserHasAdministerIssuesPermission(projectUuid)) | |||
.doesNotThrowAnyException(); | |||
verify(dbClient, times(1)).authorizationDao(); | |||
} | |||
@Test | |||
public void givenUserDoesNotHaveAdministerIssuesPermission_whenValidateUserHasAdministerIssuesPermission_thenThrowForbiddenException() { | |||
// given | |||
String userUuid = "userUuid"; | |||
doReturn(userUuid).when(userSession).getUuid(); | |||
String projectUuid = "projectUuid"; | |||
DbSession dbSession = mockDbSession(); | |||
AuthorizationDao authorizationDao = mockAuthorizationDao(); | |||
doReturn(Set.of("permission1")).when(authorizationDao).selectEntityPermissions(dbSession, projectUuid, userUuid); | |||
// when, then | |||
assertThatThrownBy(() -> underTest.validateUserHasAdministerIssuesPermission(projectUuid)) | |||
.withFailMessage("Insufficient privileges") | |||
.isInstanceOf(ForbiddenException.class); | |||
verify(dbClient, times(1)).authorizationDao(); | |||
} | |||
private AuthorizationDao mockAuthorizationDao() { | |||
AuthorizationDao authorizationDao = mock(AuthorizationDao.class); | |||
doReturn(authorizationDao).when(dbClient).authorizationDao(); | |||
return authorizationDao; | |||
} | |||
private DbSession mockDbSession() { | |||
DbSession dbSession = mock(DbSession.class); | |||
doReturn(dbSession).when(dbClient).openSession(false); | |||
return dbSession; | |||
} | |||
} |
@@ -0,0 +1,20 @@ | |||
[ | |||
{ | |||
"ruleKey": "squid:S0001", | |||
"issueMessage": "issueMessage1", | |||
"filePath": "filePath1", | |||
"line": 1, | |||
"lineHash": "lineHash1", | |||
"transition": "transition1", | |||
"comment": "comment1" | |||
}, | |||
{ | |||
"ruleKey": "squid:S0002", | |||
"issueMessage": "issueMessage2", | |||
"filePath": "filePath2", | |||
"line": 2, | |||
"lineHash": "lineHash2", | |||
"transition": "transition2", | |||
"comment": "comment2" | |||
} | |||
] |
@@ -0,0 +1,150 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.core.issue; | |||
import java.time.Instant; | |||
import java.util.Date; | |||
import java.util.Objects; | |||
import org.apache.commons.lang.builder.HashCodeBuilder; | |||
import org.jetbrains.annotations.Nullable; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.core.issue.tracking.Trackable; | |||
public class AnticipatedTransition implements Trackable { | |||
private final String projectKey; | |||
private final String branch; | |||
private final String transition; | |||
private final String userUuid; | |||
private final String comment; | |||
private final String filePath; | |||
private final Integer line; | |||
private final String message; | |||
private final String lineHash; | |||
private final RuleKey ruleKey; | |||
public AnticipatedTransition( | |||
String projectKey, | |||
@Nullable String branch, | |||
String userUuid, | |||
@Nullable RuleKey ruleKey, | |||
@Nullable String message, | |||
@Nullable String filePath, | |||
@Nullable Integer line, | |||
@Nullable String lineHash, | |||
String transition, | |||
@Nullable String comment) { | |||
this.projectKey = projectKey; | |||
this.branch = branch; | |||
this.transition = transition; | |||
this.userUuid = userUuid; | |||
this.comment = comment; | |||
this.filePath = filePath; | |||
this.line = line; | |||
this.message = message; | |||
this.lineHash = lineHash; | |||
this.ruleKey = ruleKey; | |||
} | |||
public String getProjectKey() { | |||
return projectKey; | |||
} | |||
public String getBranch() { | |||
return branch; | |||
} | |||
public String getTransition() { | |||
return transition; | |||
} | |||
public String getUserUuid() { | |||
return userUuid; | |||
} | |||
public String getComment() { | |||
return comment; | |||
} | |||
public String getFilePath() { | |||
return filePath; | |||
} | |||
@Nullable | |||
@Override | |||
public Integer getLine() { | |||
return line; | |||
} | |||
@Nullable | |||
@Override | |||
public String getMessage() { | |||
return message; | |||
} | |||
@Nullable | |||
@Override | |||
public String getLineHash() { | |||
return lineHash; | |||
} | |||
@Override | |||
public RuleKey getRuleKey() { | |||
return ruleKey; | |||
} | |||
@Override | |||
public String getStatus() { | |||
// Since it's an anticipated transition, the issue will always be open upon the first analysis. | |||
return Issue.STATUS_OPEN; | |||
} | |||
@Override | |||
public Date getUpdateDate() { | |||
return Date.from(Instant.now()); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
AnticipatedTransition that = (AnticipatedTransition) o; | |||
return Objects.equals(projectKey, that.projectKey) | |||
&& Objects.equals(branch, that.branch) | |||
&& Objects.equals(transition, that.transition) | |||
&& Objects.equals(userUuid, that.userUuid) | |||
&& Objects.equals(comment, that.comment) | |||
&& Objects.equals(filePath, that.filePath) | |||
&& Objects.equals(line, that.line) | |||
&& Objects.equals(message, that.message) | |||
&& Objects.equals(lineHash, that.lineHash) | |||
&& Objects.equals(ruleKey, that.ruleKey); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return HashCodeBuilder.reflectionHashCode(this); | |||
} | |||
} |
@@ -0,0 +1,74 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2023 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.core.issue; | |||
import org.assertj.core.api.Assertions; | |||
import org.junit.Test; | |||
import org.sonar.api.rule.RuleKey; | |||
public class AnticipatedTransitionTest { | |||
@Test | |||
public void givenTwoAnticipatedTransitions_whenFieldsHaveTheSameValue_theyShouldBeEqual() { | |||
AnticipatedTransition anticipatedTransition = getAnticipatedTransition(); | |||
AnticipatedTransition anticipatedTransition2 = getAnticipatedTransition(); | |||
assertFieldsAreTheSame(anticipatedTransition, anticipatedTransition2); | |||
Assertions.assertThat(anticipatedTransition).isEqualTo(anticipatedTransition2); | |||
} | |||
@Test | |||
public void givenTwoAnticipatedTransitions_whenFieldsHaveTheSameValue_hashcodeShouldBeTheSame() { | |||
AnticipatedTransition anticipatedTransition = getAnticipatedTransition(); | |||
AnticipatedTransition anticipatedTransition2 = getAnticipatedTransition(); | |||
assertFieldsAreTheSame(anticipatedTransition, anticipatedTransition2); | |||
Assertions.assertThat(anticipatedTransition).hasSameHashCodeAs(anticipatedTransition2); | |||
} | |||
private void assertFieldsAreTheSame(AnticipatedTransition anticipatedTransition, AnticipatedTransition anticipatedTransition2) { | |||
Assertions.assertThat(anticipatedTransition.getProjectKey()).isEqualTo(anticipatedTransition2.getProjectKey()); | |||
Assertions.assertThat(anticipatedTransition.getBranch()).isEqualTo(anticipatedTransition2.getBranch()); | |||
Assertions.assertThat(anticipatedTransition.getUserUuid()).isEqualTo(anticipatedTransition2.getUserUuid()); | |||
Assertions.assertThat(anticipatedTransition.getTransition()).isEqualTo(anticipatedTransition2.getTransition()); | |||
Assertions.assertThat(anticipatedTransition.getComment()).isEqualTo(anticipatedTransition2.getComment()); | |||
Assertions.assertThat(anticipatedTransition.getFilePath()).isEqualTo(anticipatedTransition2.getFilePath()); | |||
Assertions.assertThat(anticipatedTransition.getLine()).isEqualTo(anticipatedTransition2.getLine()); | |||
Assertions.assertThat(anticipatedTransition.getMessage()).isEqualTo(anticipatedTransition2.getMessage()); | |||
Assertions.assertThat(anticipatedTransition.getLineHash()).isEqualTo(anticipatedTransition2.getLineHash()); | |||
Assertions.assertThat(anticipatedTransition.getRuleKey()).isEqualTo(anticipatedTransition2.getRuleKey()); | |||
} | |||
private AnticipatedTransition getAnticipatedTransition() { | |||
return new AnticipatedTransition( | |||
"projectKey", | |||
"branch", | |||
"userUuid", | |||
RuleKey.parse("rule:key"), | |||
"message", | |||
"filepath", | |||
1, | |||
"lineHash", | |||
"transition", | |||
"comment" | |||
); | |||
} | |||
} |
@@ -41,6 +41,7 @@ public class IssuesWsParameters { | |||
public static final String ACTION_BULK_CHANGE = "bulk_change"; | |||
public static final String ACTION_PULL = "pull"; | |||
public static final String ACTION_PULL_TAINT = "pull_taint"; | |||
public static final String ACTION_ANTICIPATED_TRANSITIONS = "anticipated_transitions"; | |||
public static final String PARAM_ISSUE = "issue"; | |||
public static final String PARAM_COMMENT = "comment"; |