Browse Source

SONAR-19372 add api/issues/anticipated_transitions endpoint

tags/10.2.0.77647
Dimitris Kavvathas 9 months ago
parent
commit
bb784c0d35
19 changed files with 1281 additions and 1 deletions
  1. 59
    0
      server/sonar-db-dao/src/it/java/org/sonar/db/issue/AnticipatedTransitionDaoIT.java
  2. 23
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/AnticipatedTransitionDao.java
  3. 15
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/AnticipatedTransitionDto.java
  4. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/AnticipatedTransitionMapper.java
  5. 4
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/issue/AnticipatedTransitionMapper.xml
  6. 240
    0
      server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java
  7. 10
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
  8. 69
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionHandler.java
  9. 56
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParser.java
  10. 105
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsAction.java
  11. 69
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionValidator.java
  12. 28
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/GsonAnticipatedTransition.java
  13. 111
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionHandlerTest.java
  14. 108
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java
  15. 137
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionValidatorTest.java
  16. 20
    0
      server/sonar-webserver-webapi/src/test/resources/org/sonar/server/issue/ws/anticipatedtransition/request-with-transitions.json
  17. 150
    0
      sonar-core/src/main/java/org/sonar/core/issue/AnticipatedTransition.java
  18. 74
    0
      sonar-core/src/test/java/org/sonar/core/issue/AnticipatedTransitionTest.java
  19. 1
    0
      sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java

+ 59
- 0
server/sonar-db-dao/src/it/java/org/sonar/db/issue/AnticipatedTransitionDaoIT.java View File

@@ -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);
}
}

+ 23
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/AnticipatedTransitionDao.java View File

@@ -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);
}

+ 15
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/AnticipatedTransitionDto.java View File

@@ -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());
}
}

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/AnticipatedTransitionMapper.java View File

@@ -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);
}

+ 4
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/AnticipatedTransitionMapper.xml View File

@@ -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"/>

+ 240
- 0
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionIT.java View File

@@ -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()));
}

}

+ 10
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java View File

@@ -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
);
}
}

+ 69
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionHandler.java View File

@@ -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)));
}
}

+ 56
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParser.java View File

@@ -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();
}

}

+ 105
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsAction.java View File

@@ -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();
}
}

+ 69
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionValidator.java View File

@@ -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();
}
}
}

}

+ 28
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/anticipatedtransition/GsonAnticipatedTransition.java View File

@@ -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) {
}

+ 111
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionHandlerTest.java View File

@@ -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");
}
}

+ 108
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionParserTest.java View File

@@ -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()));
}

}

+ 137
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/anticipatedtransition/AnticipatedTransitionsActionValidatorTest.java View File

@@ -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;
}
}

+ 20
- 0
server/sonar-webserver-webapi/src/test/resources/org/sonar/server/issue/ws/anticipatedtransition/request-with-transitions.json View File

@@ -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"
}
]

+ 150
- 0
sonar-core/src/main/java/org/sonar/core/issue/AnticipatedTransition.java View File

@@ -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);
}
}

+ 74
- 0
sonar-core/src/test/java/org/sonar/core/issue/AnticipatedTransitionTest.java View File

@@ -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"
);
}

}

+ 1
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java View File

@@ -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";

Loading…
Cancel
Save