<select id="selectByKey" parameterType="String" resultType="Issue">
select
- <include refid="issueColumns"/>
+ <include refid="issueColumns"/>,
+ u.login as assigneeLogin
from issues i
inner join rules r on r.uuid=i.rule_uuid
inner join components p on p.uuid=i.component_uuid
inner join components root on root.uuid=i.project_uuid
+ left join users u on i.assignee = u.uuid
left join new_code_reference_issues n on i.kee = n.issue_key
where i.kee=#{kee,jdbcType=VARCHAR}
</select>
*/
package org.sonar.db.issue;
+import com.google.protobuf.InvalidProtocolBufferException;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Calendar;
.build();
@Test
- public void toDefaultIssue_set_issue_fields() {
+ public void toDefaultIssue_ShouldSetIssueFields() throws InvalidProtocolBufferException {
Date createdAt = DateUtils.addDays(new Date(), -5);
Date updatedAt = DateUtils.addDays(new Date(), -3);
Date closedAt = DateUtils.addDays(new Date(), -1);
.setIssueCloseDate(closedAt)
.setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
+ DefaultIssue expected = new DefaultIssue()
+ .setKey("100")
+ .setType(RuleType.VULNERABILITY)
+ .setRuleKey(RuleKey.of("java", "AvoidCycle"))
+ .setLanguage("xoo")
+ .setComponentUuid("CDEF")
+ .setProjectUuid("GHIJ")
+ .setComponentKey("org.sonar.sample:Sample")
+ .setProjectKey("org.sonar.sample")
+ .setStatus(Issue.STATUS_CLOSED)
+ .setResolution(Issue.RESOLUTION_FALSE_POSITIVE)
+ .setGap(15.0)
+ .setEffort(Duration.create(10L))
+ .setLine(6)
+ .setSeverity("BLOCKER")
+ .setMessage("message")
+ .setMessageFormattings(DbIssues.MessageFormattings.parseFrom(EXAMPLE_MESSAGE_FORMATTINGS.toByteArray()))
+ .setManualSeverity(true)
+ .setAssigneeUuid("perceval")
+ .setAuthorLogin("pierre")
+ .setCreationDate(DateUtils.truncate(createdAt, Calendar.SECOND))
+ .setUpdateDate(DateUtils.truncate(updatedAt, Calendar.SECOND))
+ .setCloseDate(DateUtils.truncate(closedAt, Calendar.SECOND))
+ .setNew(false)
+ .setIsNewCodeReferenceIssue(false)
+ .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
+ .setCodeVariants(Set.of())
+ .setTags(Set.of());
+
DefaultIssue issue = dto.toDefaultIssue();
- assertThat(issue.key()).isEqualTo("100");
- assertThat(issue.type()).isEqualTo(RuleType.VULNERABILITY);
- assertThat(issue.ruleKey()).hasToString("java:AvoidCycle");
- assertThat(issue.language()).isEqualTo("xoo");
- assertThat(issue.componentUuid()).isEqualTo("CDEF");
- assertThat(issue.projectUuid()).isEqualTo("GHIJ");
- assertThat(issue.componentKey()).isEqualTo("org.sonar.sample:Sample");
- assertThat(issue.projectKey()).isEqualTo("org.sonar.sample");
- assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED);
- assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
- assertThat(issue.gap()).isEqualTo(15.0);
- assertThat(issue.effort()).isEqualTo(Duration.create(10L));
- assertThat(issue.line()).isEqualTo(6);
- assertThat(issue.severity()).isEqualTo("BLOCKER");
- assertThat(issue.message()).isEqualTo("message");
- assertThat((DbIssues.MessageFormattings) issue.getMessageFormattings()).isEqualTo(EXAMPLE_MESSAGE_FORMATTINGS);
- assertThat(issue.manualSeverity()).isTrue();
- assertThat(issue.assignee()).isEqualTo("perceval");
- assertThat(issue.authorLogin()).isEqualTo("pierre");
- assertThat(issue.creationDate()).isEqualTo(DateUtils.truncate(createdAt, Calendar.SECOND));
- assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(updatedAt, Calendar.SECOND));
- assertThat(issue.closeDate()).isEqualTo(DateUtils.truncate(closedAt, Calendar.SECOND));
- assertThat(issue.isNew()).isFalse();
- assertThat(issue.isNewCodeReferenceIssue()).isFalse();
- assertThat(issue.getRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
+
+ assertThat(issue).usingRecursiveComparison().isEqualTo(expected);
}
@Test
--- /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.pushapi.hotspots;
+
+public interface HotspotChangeEventService {
+ void distributeHotspotChangedEvent(String projectUuid, HotspotChangedEvent hotspotEvent);
+
+}
--- /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.pushapi.hotspots;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.pushevent.PushEventDto;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@ServerSide
+public class HotspotChangeEventServiceImpl implements HotspotChangeEventService {
+ private static final Gson GSON = new GsonBuilder().create();
+
+ private static final String EVENT_NAME = "SecurityHotspotChanged";
+
+ private final DbClient dbClient;
+
+ public HotspotChangeEventServiceImpl(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public void distributeHotspotChangedEvent(String projectUuid, HotspotChangedEvent hotspotEvent) {
+ persistEvent(projectUuid, hotspotEvent);
+ }
+
+ private void persistEvent(String projectUuid, HotspotChangedEvent hotspotEvent) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ PushEventDto eventDto = new PushEventDto()
+ .setName(EVENT_NAME)
+ .setProjectUuid(projectUuid)
+ .setPayload(serializeIssueToPushEvent(hotspotEvent));
+ dbClient.pushEventDao().insert(dbSession, eventDto);
+ dbSession.commit();
+ }
+ }
+
+ private static byte[] serializeIssueToPushEvent(HotspotChangedEvent event) {
+ return GSON.toJson(event).getBytes(UTF_8);
+ }
+
+
+}
--- /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.pushapi.hotspots;
+
+import java.io.Serializable;
+import java.util.Date;
+import javax.annotation.CheckForNull;
+
+public class HotspotChangedEvent implements Serializable {
+
+ private final String key;
+ private final String projectKey;
+ private final Long updateDate;
+ private final String status;
+ private final String assignee;
+ private final String resolution;
+ private final String filePath;
+
+ private HotspotChangedEvent(Builder builder) {
+ this.key = builder.getKey();
+ this.projectKey = builder.getProjectKey();
+ this.updateDate = builder.getUpdateDate() == null ? null : builder.getUpdateDate().getTime();
+ this.status = builder.getStatus();
+ this.filePath = builder.getFilePath();
+ this.resolution = builder.getResolution();
+ this.assignee = builder.getAssignee();
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getProjectKey() {
+ return projectKey;
+ }
+
+ public Long getUpdateDate() {
+ return updateDate;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ @CheckForNull
+ public String getResolution() {
+ return resolution;
+ }
+
+ @CheckForNull
+ public String getAssignee() {
+ return assignee;
+ }
+
+ public static class Builder {
+ private String key;
+ private String projectKey;
+ private Date updateDate;
+ private String status;
+ private String assignee;
+ private String resolution;
+ private String filePath;
+
+ public Builder setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public Builder setProjectKey(String projectKey) {
+ this.projectKey = projectKey;
+ return this;
+ }
+
+ public Builder setUpdateDate(Date updateDate) {
+ this.updateDate = updateDate;
+ return this;
+ }
+
+ public Builder setStatus(String status) {
+ this.status = status;
+ return this;
+ }
+
+ public Builder setAssignee(String assignee) {
+ this.assignee = assignee;
+ return this;
+ }
+
+ public Builder setResolution(String resolution) {
+ this.resolution = resolution;
+ return this;
+ }
+
+ public Builder setFilePath(String filePath) {
+ this.filePath = filePath;
+ return this;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getProjectKey() {
+ return projectKey;
+ }
+
+ public Date getUpdateDate() {
+ return updateDate;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getAssignee() {
+ return assignee;
+ }
+
+ public String getResolution() {
+ return resolution;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public HotspotChangedEvent build() {
+ return new HotspotChangedEvent(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.server.pushapi.hotspots;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Deque;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.DbTester;
+import org.sonar.db.pushevent.PushEventDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class HotspotChangeEventServiceImplTest {
+
+ @Rule
+ public DbTester db = DbTester.create();
+
+ public final HotspotChangeEventServiceImpl underTest = new HotspotChangeEventServiceImpl(db.getDbClient());
+
+ @Test
+ public void distributeHotspotChangedEvent_whenCalled_shouldPersistCorrectEventData() {
+ HotspotChangedEvent hotspotChangedEvent = new HotspotChangedEvent.Builder()
+ .setKey("key")
+ .setProjectKey("project-key")
+ .setUpdateDate(new Date(1L))
+ .setStatus("REVIEWED")
+ .setResolution("ACKNOWLEDGED")
+ .setAssignee("assignee")
+ .setFilePath("path/to/file")
+ .build();
+ assertPushEventIsPersisted(hotspotChangedEvent);
+ }
+
+ private void assertPushEventIsPersisted(HotspotChangedEvent hotspotChangedEvent) {
+ underTest.distributeHotspotChangedEvent("project-uuid", hotspotChangedEvent);
+
+ Deque<PushEventDto> events = db.getDbClient().pushEventDao()
+ .selectChunkByProjectUuids(db.getSession(), Set.of("project-uuid"), 1L, null, 1);
+ assertThat(events).isNotEmpty();
+ assertThat(events).extracting(PushEventDto::getName, PushEventDto::getProjectUuid)
+ .contains(tuple("SecurityHotspotChanged", "project-uuid"));
+
+ String payload = new String(events.getLast().getPayload(), StandardCharsets.UTF_8);
+ assertJson(payload).isSimilarTo("{" +
+ "\"key\": \"" + hotspotChangedEvent.getKey() + "\"," +
+ "\"projectKey\": \"" + hotspotChangedEvent.getProjectKey() + "\"," +
+ "\"updateDate\": " + hotspotChangedEvent.getUpdateDate() + "," +
+ "\"status\": \"" + hotspotChangedEvent.getStatus() + "\"," +
+ "\"filePath\": \"" + hotspotChangedEvent.getFilePath() + "\"," +
+ "\"assignee\": \"" + hotspotChangedEvent.getAssignee() + "\"," +
+ "\"resolution\": \"" + hotspotChangedEvent.getResolution() + "\"" +
+ "}");
+ }
+
+}
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.assertj.core.api.Condition;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.ws.IssueUpdater;
+import org.sonar.server.pushapi.hotspots.HotspotChangeEventService;
+import org.sonar.server.pushapi.hotspots.HotspotChangedEvent;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
private final DbClient dbClient = dbTester.getDbClient();
private final IssueUpdater issueUpdater = mock(IssueUpdater.class);
+ private HotspotChangeEventService hotspotChangeEventService = mock(HotspotChangeEventService.class);
private final System2 system2 = mock(System2.class);
private final IssueFieldsSetter issueFieldsSetter = mock(IssueFieldsSetter.class);
private final HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, system2);
- private final AssignAction underTest = new AssignAction(dbClient, hotspotWsSupport, issueFieldsSetter, issueUpdater);
+ private final AssignAction underTest = new AssignAction(dbClient, hotspotWsSupport, issueFieldsSetter, issueUpdater, hotspotChangeEventService);
private final WsActionTester actionTester = new WsActionTester(underTest);
+ private BranchDto branchDto = mock(BranchDto.class);
+
+ @Before
+ public void setMock() {
+ when(issueUpdater.getBranch(any(), any())).thenReturn(branchDto);
+ }
@Test
- public void ws_definition_check() {
+ public void wsExecution_whenDefined() {
WebService.Action wsDefinition = actionTester.getDef();
assertThat(wsDefinition.isPost()).isTrue();
}
@Test
- public void assign_hotspot_to_someone_for_public_project() {
+ public void wsExecution_whenAssignedForPublicProject() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void unassign_hotspot_for_public_project() {
+ public void wsExecution_whenUnassignedForPublicProject() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
UserDto assignee = insertUser(randomAlphanumeric(15));
}
@Test
- public void assign_hotspot_to_me_for_public_project() {
+ public void wsExecution_whenMyselfAssignedForPublicProject() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void unassign_hotspot_myself_for_public_project() {
+ public void wsExecution_whenMyselfUnassignedForPublicProject() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
UserDto me = insertUser(randomAlphanumeric(10));
}
@Test
- public void assign_hotspot_to_someone_for_private_project() {
+ public void wsExecution_whenAssigneeForPrivateProject() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void unassign_hotspot_for_private_project() {
+ public void wsExecution_whenUnassignedForPrivateProject() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
UserDto assignee = insertUser(randomAlphanumeric(15));
}
@Test
- public void assign_hotspot_to_someone_for_private_project_branch() {
+ public void wsExecution_whenAssigneeForPrivateProjectBranch() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto branch = dbTester.components().insertProjectBranch(project);
ComponentDto file = dbTester.components().insertComponent(newFileDto(branch, project.uuid()));
}
@Test
- public void fail_if_assignee_does_not_have_access_for_private_project() {
+ public void wsExecution_whenAssigneeDoesNotHaveAccessToPrivateProject_shouldFail() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void fail_if_assignee_does_not_have_access_for_private_project_branch() {
+ public void wsExecution_whenAssigneeDoesNotHaveAccessToPrivateProjectBranch_shouldFail() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto branch = dbTester.components().insertProjectBranch(project);
ComponentDto file = dbTester.components().insertComponent(newFileDto(branch, project.uuid()));
}
@Test
- public void assign_hotspot_to_me_for_private_project() {
+ public void wsExecution_whenAssignHotspotToMeForPrivateProject() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
}
@Test
- public void assign_hotspot_with_comment() {
+ public void wsExecution_whenAssignHotspotWithComment() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
}
@Test
- public void assign_twice_same_user_to_hotspot_does_not_reload() {
+ public void wsExecution_whenAssignTwiceSameUserHotspotDoesNotReload_shouldFail() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void fail_if_assigning_to_not_existing_user() {
+ public void wsExecution_whenBranchTypeIsBranch_shouldDistributeEvents() {
+ ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
+
+ UserDto userDto = insertUser(randomAlphanumeric(10));
+ userSessionRule.logIn(userDto).registerComponents(project);
+
+ UserDto assignee = insertUser(randomAlphanumeric(15));
+ when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH);
+ when(issueFieldsSetter.assign(eq(hotspot.toDefaultIssue()), userMatcher(assignee), any(IssueChangeContext.class))).thenReturn(true);
+
+ executeRequest(hotspot, assignee.getLogin(), null);
+ verify(hotspotChangeEventService).distributeHotspotChangedEvent(eq(project.uuid()), any(HotspotChangedEvent.class));
+ }
+
+ @Test
+ public void wsExecution_whenBranchIsPullRequest_shouldNotDistributeEvents() {
+ ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
+
+ UserDto userDto = insertUser(randomAlphanumeric(10));
+ userSessionRule.logIn(userDto).registerComponents(project);
+
+ UserDto assignee = insertUser(randomAlphanumeric(15));
+ when(branchDto.getBranchType()).thenReturn(BranchType.PULL_REQUEST);
+ when(issueFieldsSetter.assign(eq(hotspot.toDefaultIssue()), userMatcher(assignee), any(IssueChangeContext.class))).thenReturn(true);
+
+ executeRequest(hotspot, assignee.getLogin(), null);
+ verifyNoInteractions(hotspotChangeEventService);
+ }
+
+
+ @Test
+ public void wsExecution_whenAssigningToNonExistingUser_shouldFail() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
@Test
@UseDataProvider("allIssueStatusesAndResolutionsThatThrowOnAssign")
- public void fail_if_assign_user_to_hotspot_for_which_it_is_forbidden(String status, String resolution) {
+ public void wsExecution_whenAssigningToUserIfForbidden_shouldFail(String status, String resolution) {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
@Test
@UseDataProvider("allIssueStatusesAndResolutionsThatDoNotThrowOnAssign")
- public void fail_if_assign_user_to_hotspot_for_which_it_is_allowed(String status, String resolution) {
+ public void wsExecution_whenAssigningToUserIfAllowed_shouldNotFail(String status, String resolution) {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file, h -> {
}
@Test
- public void fail_if_not_authenticated() {
+ public void wsExecution_whenNotAuthenticated_shouldFail() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void fail_if_missing_browse_permission() {
+ public void wsExecution_whenMissingBrowserAthentication_shouldFail() {
ComponentDto project = dbTester.components().insertPrivateProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
}
@Test
- public void fail_if_hotspot_does_not_exist() {
+ public void wsExecution_whenHotspotDoesNotExist_shouldFail() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
UserDto me = insertUser(randomAlphanumeric(10));
@Test
@UseDataProvider("allRuleTypesWithStatusesExceptHotspot")
- public void fail_if_trying_to_assign_issue(RuleType ruleType, String status) {
+ public void wsExecution_whenAssigningToNonexistantIssue_shouldFail(RuleType ruleType, String status) {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
RuleDto rule = newRule(ruleType);
}
@Test
- public void fail_with_NotFoundException_if_hotspot_is_closed() {
+ public void wsExecution_whenHotspotIsClosed_shouldFailWithNotFoundException() {
ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
RuleDto rule = newRule(SECURITY_HOTSPOT);
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.annotation.Nullable;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueTesting;
+import org.sonar.db.project.ProjectDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.db.rule.RuleTesting;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.TransitionService;
import org.sonar.server.issue.ws.IssueUpdater;
+import org.sonar.server.pushapi.hotspots.HotspotChangeEventService;
+import org.sonar.server.pushapi.hotspots.HotspotChangedEvent;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;
private DbClient dbClient = dbTester.getDbClient();
private TransitionService transitionService = mock(TransitionService.class);
private IssueUpdater issueUpdater = mock(IssueUpdater.class);
+ private HotspotChangeEventService hotspotChangeEventService = mock(HotspotChangeEventService.class);
private System2 system2 = mock(System2.class);
private IssueFieldsSetter issueFieldsSetter = mock(IssueFieldsSetter.class);
private HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, system2);
- private ChangeStatusAction underTest = new ChangeStatusAction(dbClient, hotspotWsSupport, transitionService, issueFieldsSetter, issueUpdater);
+ private ChangeStatusAction underTest = new ChangeStatusAction(dbClient, hotspotWsSupport, transitionService, issueFieldsSetter, issueUpdater, hotspotChangeEventService);
private WsActionTester actionTester = new WsActionTester(underTest);
+ private final ComponentDbTester componentDbTester = new ComponentDbTester(dbTester);
+ private BranchDto branchDto = mock(BranchDto.class);
+
+ @Before
+ public void setMock() {
+ when(issueUpdater.getBranch(any(), any())).thenReturn(branchDto);
+ }
@Test
public void define_shouldMarkWebServiceAsPublic() {
}
}
+ @Test
+ public void wsExecution_whenOnMainBranch_shouldDistributeEvents() {
+ ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
+ userSessionRule.logIn().registerComponents(project)
+ .addProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN, project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH);
+ IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
+ when(transitionService.doTransition(any(), any(), any())).thenReturn(true);
+
+ newRequest(hotspot, STATUS_REVIEWED, RESOLUTION_FIXED, NO_COMMENT).execute();
+
+ verify(hotspotChangeEventService).distributeHotspotChangedEvent(eq(project.uuid()), any(HotspotChangedEvent.class));
+ }
+
+ @Test
+ public void wsExecution_whenOnNonMainBranch_shouldDistributeEvents() {
+ ProjectDto project = dbTester.components().insertPublicProject().getProjectDto();
+ ComponentDto projectComponentDto = dbTester.components().getComponentDto(project);
+ BranchDto branch = dbTester.components().insertProjectBranch(project, b -> b.setKey("develop"));
+ ComponentDto branchComponentDto = dbTester.components().getComponentDto(branch);
+
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(branchComponentDto));
+ when(branchDto.getBranchType()).thenReturn(BranchType.BRANCH);
+ IssueDto hotspot = dbTester.issues().insertHotspot(branchComponentDto, file);
+ when(transitionService.doTransition(any(), any(), any())).thenReturn(true);
+
+ userSessionRule.logIn().registerComponents(projectComponentDto)
+ .addProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN, projectComponentDto, branchComponentDto);
+ newRequest(hotspot, STATUS_REVIEWED, RESOLUTION_FIXED, NO_COMMENT).execute();
+
+ verify(hotspotChangeEventService).distributeHotspotChangedEvent(eq(branchComponentDto.uuid()), any(HotspotChangedEvent.class));
+ }
+
+ @Test
+ public void wsExecution_whenBranchTypeIsPullRequest_shouldNotDistributeEvents() {
+ ComponentDto project = dbTester.components().insertPublicProject().getMainBranchComponent();
+ userSessionRule.logIn().registerComponents(project)
+ .addProjectPermission(UserRole.SECURITYHOTSPOT_ADMIN, project);
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ when(branchDto.getBranchType()).thenReturn(BranchType.PULL_REQUEST);
+ IssueDto hotspot = dbTester.issues().insertHotspot(project, file);
+ when(transitionService.doTransition(any(), any(), any())).thenReturn(true);
+
+ newRequest(hotspot, STATUS_REVIEWED, RESOLUTION_FIXED, NO_COMMENT).execute();
+
+ verifyNoInteractions(hotspotChangeEventService);
+ }
+
@DataProvider
public static Object[][] reviewedResolutionsAndExpectedTransitionKey() {
return new Object[][] {
import org.sonar.core.util.Uuids;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.ws.IssueUpdater;
+import org.sonar.server.pushapi.hotspots.HotspotChangeEventService;
+import org.sonar.server.pushapi.hotspots.HotspotChangedEvent;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.sonar.api.issue.Issue.RESOLUTION_ACKNOWLEDGED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.db.component.BranchType.BRANCH;
import static org.sonar.server.exceptions.NotFoundException.checkFound;
import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
private final HotspotWsSupport hotspotWsSupport;
private final IssueFieldsSetter issueFieldsSetter;
private final IssueUpdater issueUpdater;
+ private final HotspotChangeEventService hotspotChangeEventService;
public AssignAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, IssueFieldsSetter issueFieldsSetter,
- IssueUpdater issueUpdater) {
+ IssueUpdater issueUpdater, HotspotChangeEventService hotspotChangeEventService) {
this.dbClient = dbClient;
this.hotspotWsSupport = hotspotWsSupport;
this.issueFieldsSetter = issueFieldsSetter;
this.issueUpdater = issueUpdater;
+ this.hotspotChangeEventService = hotspotChangeEventService;
}
@Override
if (issueFieldsSetter.assign(defaultIssue, assignee, context)) {
issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
+
+ BranchDto branch = issueUpdater.getBranch(dbSession, defaultIssue);
+ if (BRANCH.equals(branch.getBranchType())) {
+ HotspotChangedEvent hotspotChangedEvent = buildEventData(defaultIssue, assignee, hotspotDto.getFilePath());
+ hotspotChangeEventService.distributeHotspotChangedEvent(defaultIssue.projectUuid(), hotspotChangedEvent);
+ }
}
}
}
private boolean hasProjectPermission(DbSession dbSession, String userUuid, String projectUuid) {
return dbClient.authorizationDao().selectProjectPermissions(dbSession, projectUuid, userUuid).contains(UserRole.USER);
}
+
+ private static HotspotChangedEvent buildEventData(DefaultIssue defaultIssue, @Nullable UserDto assignee, String filePath) {
+ return new HotspotChangedEvent.Builder()
+ .setKey(defaultIssue.key())
+ .setProjectKey(defaultIssue.projectKey())
+ .setStatus(defaultIssue.status())
+ .setResolution(defaultIssue.resolution())
+ .setUpdateDate(defaultIssue.updateDate())
+ .setAssignee(assignee == null ? null : assignee.getLogin())
+ .setFilePath(filePath)
+ .build();
+ }
}
import org.sonar.core.util.Uuids;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.TransitionService;
import org.sonar.server.issue.ws.IssueUpdater;
+import org.sonar.server.pushapi.hotspots.HotspotChangeEventService;
+import org.sonar.server.pushapi.hotspots.HotspotChangedEvent;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang.StringUtils.trimToNull;
import static org.sonar.api.issue.Issue.SECURITY_HOTSPOT_RESOLUTIONS;
import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.db.component.BranchType.BRANCH;
public class ChangeStatusAction implements HotspotsWsAction {
private final TransitionService transitionService;
private final IssueFieldsSetter issueFieldsSetter;
private final IssueUpdater issueUpdater;
+ private final HotspotChangeEventService hotspotChangeEventService;
public ChangeStatusAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, TransitionService transitionService,
- IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater) {
+ IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, HotspotChangeEventService hotspotChangeEventService) {
this.dbClient = dbClient;
this.hotspotWsSupport = hotspotWsSupport;
this.transitionService = transitionService;
this.issueFieldsSetter = issueFieldsSetter;
this.issueUpdater = issueUpdater;
+ this.hotspotChangeEventService = hotspotChangeEventService;
}
@Override
if (comment != null) {
issueFieldsSetter.addComment(defaultIssue, comment, context);
}
+
issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context);
+
+ BranchDto branch = issueUpdater.getBranch(session, defaultIssue);
+ if (BRANCH.equals(branch.getBranchType())) {
+ HotspotChangedEvent hotspotChangedEvent = buildEventData(defaultIssue, issueDto);
+ hotspotChangeEventService.distributeHotspotChangedEvent(defaultIssue.projectUuid(), hotspotChangedEvent);
+ }
}
}
+ private static HotspotChangedEvent buildEventData(DefaultIssue defaultIssue, IssueDto issueDto) {
+ return new HotspotChangedEvent.Builder()
+ .setKey(defaultIssue.key())
+ .setProjectKey(defaultIssue.projectKey())
+ .setStatus(defaultIssue.status())
+ .setResolution(defaultIssue.resolution())
+ .setUpdateDate(defaultIssue.updateDate())
+ .setAssignee(issueDto.getAssigneeLogin())
+ .setFilePath(issueDto.getFilePath())
+ .build();
+ }
+
}
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).withRefreshMeasures().build();
transitionService.checkTransitionPermission(transitionKey, defaultIssue);
if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
- BranchDto branch = issueUpdater.getBranch(session, defaultIssue, defaultIssue.projectUuid());
+ BranchDto branch = issueUpdater.getBranch(session, defaultIssue);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, branch);
if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(defaultIssue.projectUuid()) != null) {
*/
package org.sonar.server.issue.ws;
+import java.util.Date;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
}
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context) {
- BranchDto branch = getBranch(dbSession, issue, issue.projectUuid());
+ BranchDto branch = getBranch(dbSession, issue);
return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, branch);
}
return result;
}
- protected BranchDto getBranch(DbSession dbSession, DefaultIssue issue, @Nullable String projectUuid) {
+ public BranchDto getBranch(DbSession dbSession, DefaultIssue issue) {
String issueKey = issue.key();
- checkState(projectUuid != null, "Issue '%s' has no project", issueKey);
- BranchDto component = dbClient.branchDao().selectByUuid(dbSession, projectUuid).orElse(null);
- checkState(component != null, "Branch uuid '%s' for issue key '%s' cannot be found", projectUuid, issueKey);
- return component;
+ String componentUuid = issue.componentUuid();
+ checkState(componentUuid != null, "Component uuid for issue key '%s' cannot be null", issueKey);
+ Optional<ComponentDto> componentDto = dbClient.componentDao().selectByUuid(dbSession, componentUuid);
+ checkState(componentDto.isPresent(), "Component not found for issue with key '%s'", issueKey);
+ BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, componentDto.get().branchUuid()).orElse(null);
+ checkState(branchDto != null, "Branch not found for issue with key '%s'", issueKey);
+ return branchDto;
}
private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context,
@Nullable RuleDto ruleDto, ComponentDto project, BranchDto branchDto) {
IssueDto issueDto = issueStorage.save(session, singletonList(issue)).iterator().next();
+ Date updateDate = issue.updateDate();
if (
// since this method is called after an update of the issue, date should never be null
- issue.updateDate() == null
+ updateDate == null
// name of rule is displayed in notification, rule must therefor be present
|| ruleDto == null
// notification are not supported on PRs
.setBranchName(branchDto.isMain() ? null : branchDto.getKey())
.build())
.build()),
- new UserChange(issue.updateDate().getTime(), new User(author.getUuid(), author.getLogin(), author.getName())));
+ new UserChange(updateDate.getTime(), new User(author.getUuid(), author.getLogin(), author.getName())));
notificationService.scheduleForSending(notificationSerializer.serialize(notificationBuilder));
return issueDto;
}
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), userSession.getUuid()).withRefreshMeasures().build();
if (issueFieldsSetter.setManualSeverity(issue, severity, context)) {
- BranchDto branch = issueUpdater.getBranch(session, issue, issue.projectUuid());
+ BranchDto branch = issueUpdater.getBranch(session, issue);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, branch);
if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).withRefreshMeasures().build();
if (issueFieldsSetter.setType(issue, ruleType, context)) {
- BranchDto branch = issueUpdater.getBranch(session, issue, issue.projectUuid());
+ BranchDto branch = issueUpdater.getBranch(session, issue);
SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, branch);
if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
issueChangeEventService.distributeIssueChangeEvent(issue, null, ruleType.name(), null, branch,
import org.sonar.server.projecttag.ws.ProjectTagsWsModule;
import org.sonar.server.property.InternalPropertiesImpl;
import org.sonar.server.pushapi.ServerPushModule;
+import org.sonar.server.pushapi.hotspots.HotspotChangeEventServiceImpl;
import org.sonar.server.pushapi.issues.IssueChangeEventServiceImpl;
import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventServiceImpl;
import org.sonar.server.qualitygate.ProjectsInWarningModule;
MyNewIssuesNotificationHandler.class,
MyNewIssuesNotificationHandler.newMetadata(),
IssueChangeEventServiceImpl.class,
+ HotspotChangeEventServiceImpl.class,
// issues actions
AssignAction.class,