+/*
+ * 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;
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);
+ }
}
+/*
+ * 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;
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);
}
package org.sonar.db.issue;
import javax.annotation.Nullable;
+import org.sonar.core.issue.AnticipatedTransition;
public class AnticipatedTransitionDto {
private String uuid;
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());
+ }
}
void delete(@Param("uuid") String uuid);
+ void deleteByProjectAndUser(@Param("projectUuid") String projectUuid, @Param("userUuid") String userUuid);
+
List<AnticipatedTransitionDto> selectByProjectUuid(@Param("projectUuid") String projectUuid);
}
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"/>
--- /dev/null
+/*
+ * 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()));
+ }
+
+}
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;
PullTaintAction.class,
PullActionResponseWriter.class,
PullActionProtobufObjectGenerator.class,
- PullTaintActionProtobufObjectGenerator.class);
+ PullTaintActionProtobufObjectGenerator.class,
+ AnticipatedTransitionParser.class,
+ AnticipatedTransitionHandler.class,
+ AnticipatedTransitionsActionValidator.class,
+ AnticipatedTransitionsAction.class
+ );
}
}
--- /dev/null
+/*
+ * 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)));
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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) {
+}
--- /dev/null
+/*
+ * 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");
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * 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()));
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * 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;
+ }
+}
\ No newline at end of file
--- /dev/null
+[
+ {
+ "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"
+ }
+]
\ No newline at end of file
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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"
+ );
+ }
+
+}
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";