From 1db20751d8d12bd3e3d93ce85c91a171ff3bca2a Mon Sep 17 00:00:00 2001 From: Jacek Poreda Date: Tue, 4 Jul 2023 09:08:31 +0200 Subject: [PATCH] SONAR-19728 add api/issues/list WS action --- .../java/org/sonar/db/issue/IssueDaoIT.java | 5 +- .../org/sonar/db/issue/IssueMapper.xml | 5 +- .../sonar/server/issue/ws/ListActionIT.java | 751 ++++++++++++++++++ .../server/component/ComponentFinder.java | 6 + .../sonar/server/issue/ws/IssueWsModule.java | 1 + .../org/sonar/server/issue/ws/ListAction.java | 329 ++++++++ .../server/issue/ws/SearchResponseFormat.java | 8 + .../ws/client/issue/IssuesWsParameters.java | 1 + sonar-ws/src/main/protobuf/ws-issues.proto | 6 + 9 files changed, 1109 insertions(+), 3 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java index b4bd72da58b..567af5f8352 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java @@ -21,6 +21,7 @@ package org.sonar.db.issue; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.Set; @@ -720,8 +721,8 @@ public class IssueDaoIT { @Test public void selectByQuery_whenFilteredWithCreatedAfter_shouldGetIssuesCreatedAfterDate() { - List createdBeforeIssues = generateIssues(3, i -> createIssueWithKey("createdBefore-" + i).setCreatedAt(1_400_000_000_000L)); - List createdAfterIssues = generateIssues(3, i -> createIssueWithKey("createdAfter-" + i).setCreatedAt(1_420_000_000_000L)); + List createdBeforeIssues = generateIssues(3, i -> createIssueWithKey("createdBefore-" + i).setResolution(null).setIssueCreationDate(new Date(1_400_000_000_000L))); + List createdAfterIssues = generateIssues(3, i -> createIssueWithKey("createdAfter-" + i).setResolution(null).setIssueCreationDate(new Date(1_420_000_000_000L))); Stream.of(createdBeforeIssues, createdAfterIssues) .flatMap(Collection::stream) .forEach(issue -> underTest.insert(db.getSession(), issue)); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index a8a33de5f15..ac6593db8e2 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -752,6 +752,9 @@ #{status,jdbcType=VARCHAR} + + AND i.resolution is not null + AND i.resolution IN @@ -762,7 +765,7 @@ AND n.uuid IS NOT NULL - AND i.created_at >= #{query.createdAfter,jdbcType=BIGINT} + AND i.issue_creation_date >= #{query.createdAfter,jdbcType=BIGINT} order by i.kee asc diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java new file mode 100644 index 00000000000..12a603d00d8 --- /dev/null +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/ListActionIT.java @@ -0,0 +1,751 @@ +/* + * 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; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.time.Clock; +import java.util.List; +import java.util.stream.IntStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sonar.api.resources.Languages; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.Durations; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ProjectData; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.db.protobuf.DbIssues; +import org.sonar.db.rule.RuleDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.component.TestComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.issue.AvatarResolverImpl; +import org.sonar.server.issue.IssueFieldsSetter; +import org.sonar.server.issue.TextRangeResponseFormatter; +import org.sonar.server.issue.TransitionService; +import org.sonar.server.issue.workflow.FunctionExecutor; +import org.sonar.server.issue.workflow.IssueWorkflow; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.MessageFormattingUtils; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Common.Severity; +import org.sonarqube.ws.Issues; +import org.sonarqube.ws.Issues.Issue; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; +import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; +import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; +import static org.sonar.api.issue.Issue.STATUS_CLOSED; +import static org.sonar.api.issue.Issue.STATUS_OPEN; +import static org.sonar.api.issue.Issue.STATUS_RESOLVED; +import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.api.utils.DateUtils.parseDate; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; +import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE; +import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; +import static org.sonar.db.rule.RuleTesting.XOO_X1; +import static org.sonar.db.rule.RuleTesting.XOO_X2; +import static org.sonar.db.rule.RuleTesting.newRule; +import static org.sonar.server.tester.UserSessionRule.standalone; + +@RunWith(DataProviderRunner.class) +public class ListActionIT { + + public static final DbIssues.MessageFormatting MESSAGE_FORMATTING = DbIssues.MessageFormatting.newBuilder() + .setStart(0).setEnd(11).setType(CODE).build(); + private final UuidFactoryFast uuidFactory = UuidFactoryFast.getInstance(); + @Rule + public UserSessionRule userSession = standalone(); + @Rule + public DbTester db = DbTester.create(); + + private final DbClient dbClient = db.getDbClient(); + private final IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); + private final IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter); + private final SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow)); + private final Languages languages = new Languages(); + private final UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl()); + private final SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new TextRangeResponseFormatter(), userFormatter); + private final ComponentFinder componentFinder = TestComponentFinder.from(db); + private final WsActionTester ws = new WsActionTester( + new ListAction(userSession, dbClient, Clock.systemUTC(), searchResponseLoader, searchResponseFormat, componentFinder)); + + @Before + public void setUp() { + issueWorkflow.start(); + } + + @Test + public void whenNoComponentOrProjectProvided_shouldFailWithMessage() { + TestRequest request = ws.newRequest(); + assertThatThrownBy(() -> request.executeProtobuf(Issues.ListWsResponse.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Either 'project' or 'component' parameter must be provided"); + } + + @Test + public void whenBranchAndPullRequestProvided_shouldFailWithMessage() { + TestRequest request = ws.newRequest() + .setParam("project", "some-project") + .setParam("branch", "some-branch") + .setParam("pullRequest", "some-pr"); + assertThatThrownBy(() -> request.executeProtobuf(Issues.ListWsResponse.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Only one of parameters 'branch' and 'pullRequest' can be provided"); + } + + @Test + public void whenAnonymousUser_shouldFailIfInsufficientPrivileges() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_RESOLVED) + .setResolution(RESOLUTION_FIXED) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + TestRequest request = ws.newRequest() + .setParam("project", projectData.projectKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()); + assertThatThrownBy(() -> request.executeProtobuf(Issues.ListWsResponse.class)) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); + } + + @Test + public void whenNoProjectOrComponent_shouldFail() { + TestRequest request = ws.newRequest() + .setParam("branch", "test-branch"); + assertThatThrownBy(() -> request.executeProtobuf(Issues.ListWsResponse.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Either 'project' or 'component' parameter must be provided"); + } + + @Test + public void whenListIssuesByProjectAndBranch_shouldReturnAllFields() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting( + Issue::getKey, Issue::getRule, Issue::getSeverity, Issue::getComponent, Issue::getResolution, Issue::getStatus, Issue::getMessage, Issue::getMessageFormattingsList, + Issue::getEffort, Issue::getAssignee, Issue::getAuthor, Issue::getLine, Issue::getHash, Issue::getTagsList, Issue::getCreationDate, Issue::getUpdateDate, + Issue::getQuickFixAvailable, Issue::getCodeVariantsList) + .containsExactlyInAnyOrder( + tuple(issue.getKey(), rule.getKey().toString(), Severity.MAJOR, file.getKey(), "", STATUS_OPEN, "the message", + MessageFormattingUtils.dbMessageFormattingListToWs(List.of(MESSAGE_FORMATTING)), "10min", + simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()), + formatDateTime(issue.getIssueUpdateDate()), false, List.of("variant1", "variant2"))); + } + + @Test + public void whenListIssuesByProject_shouldReturnIssuesFromMainBranch() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + ComponentDto anotherBranch = db.components().insertProjectBranch(project, b -> b.setKey("branch1")); + ComponentDto fileFromAnotherBranch = db.components().insertComponent(newFileDto(anotherBranch)); + IssueDto issueFromAnotherBranch = db.issues().insertIssue(rule, anotherBranch, fileFromAnotherBranch, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(issue.getKey()) + .doesNotContain(issueFromAnotherBranch.getKey()); + } + + @Test + public void whenListIssuesByProjectAndPullRequest_shouldIssuesForPullRequestOnly() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + String pullRequestId = "42"; + ComponentDto pullRequest = db.components().insertProjectBranch(project, branchDto -> branchDto.setKey(pullRequestId).setBranchType(BranchType.PULL_REQUEST)); + ComponentDto file = db.components().insertComponent(newFileDto(pullRequest)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, pullRequest, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .setParam("pullRequest", pullRequestId) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(issue.getKey()); + } + + @Test + public void whenListIssuesByProjectOnly_shouldReturnIssuesForMainBranchOnly() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(issue.getKey()); + } + + @Test + public void whenListIssuesByComponent_shouldReturnIssues() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("component", file.getKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(issue.getKey()); + } + + @Test + public void whenListIssuesByTypes_shouldReturnIssuesWithSpecifiedTypes() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i + .setType(RuleType.CODE_SMELL) + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + RuleDto bugRule = newIssueRule(XOO_X2, RuleType.BUG); + IssueDto bugIssue = db.issues().insertIssue(bugRule, project, file, i -> i + .setType(RuleType.BUG) + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.getProjectDto().getKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .setParam("types", RuleType.BUG.name()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(bugIssue.getKey()) + .doesNotContain(issue.getKey()); + } + + @Test + public void whenListIssuesByResolved_shouldReturnResolvedIssues() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IssueDto issue = db.issues().insertIssue(rule, project, file, i -> i + .setType(RuleType.CODE_SMELL) + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_CLOSED) + .setResolution(RESOLUTION_FIXED) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + RuleDto bugRule = newIssueRule(XOO_X2, RuleType.BUG); + IssueDto bugIssue = db.issues().insertIssue(bugRule, project, file, i -> i + .setType(RuleType.BUG) + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_RESOLVED) + .setResolution(RESOLUTION_WONT_FIX) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + IssueDto vulnerabilityIssue = db.issues().insertIssue(rule, project, file, i -> i + .setType(RuleType.VULNERABILITY) + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2"))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.getProjectDto().getKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .setParam("resolved", "true") + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(issue.getKey(), bugIssue.getKey()) + .doesNotContain(vulnerabilityIssue.getKey()); + + response = ws.newRequest() + .setParam("project", projectData.getProjectDto().getKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .setParam("resolved", "false") + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(vulnerabilityIssue.getKey()) + .doesNotContain(issue.getKey(), bugIssue.getKey()); + + response = ws.newRequest() + .setParam("project", projectData.getProjectDto().getKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrder(vulnerabilityIssue.getKey(), issue.getKey(), bugIssue.getKey()); + } + + @Test + public void whenListIssuesByNewCodePeriodDate_shouldReturnIssues() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + + db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodDate(parseDateTime("2014-09-05T00:00:00+0100").getTime())); + + List beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2")))) + .map(IssueDto::getKey) + .toList(); + + List afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2015-01-02")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2")))) + .map(IssueDto::getKey) + .toList(); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .setParam("inNewCodePeriod", "true") + .setParam("branch", projectData.getMainBranchDto().getKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrderElementsOf(afterNewCodePeriod) + .doesNotContainAnyElementsOf(beforeNewCodePeriod); + } + + @Test + public void whenListIssuesByNewCodePeriodReferenceBranch_shouldReturnIssues() { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + + db.components().insertSnapshot(project, s -> s.setLast(true).setPeriodMode(REFERENCE_BRANCH.name())); + MetricDto metric = db.measures().insertMetric(metricDto -> metricDto.setKey(ANALYSIS_FROM_SONARQUBE_9_4_KEY)); + db.measures().insertLiveMeasure(project, metric); + + List beforeNewCodePeriod = IntStream.range(0, 10).mapToObj(number -> db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2")))) + .map(IssueDto::getKey) + .toList(); + + List afterNewCodePeriod = IntStream.range(0, 5).mapToObj(number -> db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2015-01-02")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2")))) + .peek(issueDto -> db.issues().insertNewCodeReferenceIssue(issueDto)) + .map(IssueDto::getKey) + .toList(); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .setParam("inNewCodePeriod", "true") + .setParam("branch", projectData.getMainBranchDto().getKey()) + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()) + .extracting(Issue::getKey) + .containsExactlyInAnyOrderElementsOf(afterNewCodePeriod) + .doesNotContainAnyElementsOf(beforeNewCodePeriod); + } + + @Test + @UseDataProvider("pages") + public void whenUsingPagination_shouldReturnPaginatedResults(String page, int expectedNumberOfIssues) { + UserDto user = db.users().insertUser(); + + ProjectData projectData = db.components().insertPublicProject(); + ComponentDto project = projectData.getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + UserDto simon = db.users().insertUser(); + RuleDto rule = newIssueRule(); + IntStream.range(0, 10).forEach(number -> db.issues().insertIssue(rule, project, file, i -> i + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setMessageFormattings(DbIssues.MessageFormattings.newBuilder().addMessageFormatting(MESSAGE_FORMATTING).build()) + .setStatus(STATUS_OPEN) + .setResolution(null) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setAssigneeUuid(simon.getUuid()) + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(parseDate("2014-09-03")) + .setIssueUpdateDate(parseDate("2017-12-04")) + .setCodeVariants(List.of("variant1", "variant2")))); + + userSession + .logIn(user) + .registerProjects(projectData.getProjectDto()); + + Issues.ListWsResponse response = ws.newRequest() + .setParam("project", projectData.projectKey()) + .setParam("branch", projectData.getMainBranchDto().getKey()) + .setParam("p", page) + .setParam("ps", "3") + .executeProtobuf(Issues.ListWsResponse.class); + + assertThat(response.getIssuesList()).hasSize(expectedNumberOfIssues); + } + + private RuleDto newIssueRule() { + return newIssueRule(XOO_X1, RuleType.CODE_SMELL); + } + + private RuleDto newIssueRule(RuleKey ruleKey, RuleType ruleType) { + RuleDto rule = newRule(ruleKey, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc")) + .setLanguage("xoo") + .setName("Rule name") + .setType(ruleType) + .setStatus(RuleStatus.READY); + db.rules().insert(rule); + return rule; + } + + @DataProvider + public static Object[][] pages() { + return new Object[][] { + {"1", 3}, + {"2", 3}, + {"3", 3}, + {"4", 1}, + }; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentFinder.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentFinder.java index 0a74c3e781e..418cffc59eb 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentFinder.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ComponentFinder.java @@ -136,6 +136,12 @@ public class ComponentFinder { return new ProjectAndBranch(projectOrApp, branch); } + public ProjectAndBranch getProjectAndBranch(DbSession dbSession, String projectKey, @Nullable String branchKey, @Nullable String pullRequestKey) { + ProjectDto project = getProjectByKey(dbSession, projectKey); + BranchDto branch = getBranchOrPullRequest(dbSession, project, branchKey, pullRequestKey); + return new ProjectAndBranch(project, branch); + } + public static class ProjectAndBranch { private final ProjectDto project; private final BranchDto branch; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index b924c97a36e..f4a83670298 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -62,6 +62,7 @@ public class IssueWsModule extends Module { AssignAction.class, DoTransitionAction.class, SearchAction.class, + ListAction.class, SetSeverityAction.class, TagsAction.class, SetTagsAction.class, diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java new file mode 100644 index 00000000000..d61d3990344 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ListAction.java @@ -0,0 +1,329 @@ +/* + * 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; + +import java.time.Clock; +import com.google.common.base.Preconditions; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; +import org.sonar.api.rules.RuleType; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.Paging; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.Pagination; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.issue.IssueListQuery; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.component.ComponentFinder.ProjectAndBranch; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Issues; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.lang.String.format; +import static java.util.Collections.singletonList; +import static org.sonar.api.measures.CoreMetrics.ANALYSIS_FROM_SONARQUBE_9_4_KEY; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.api.utils.Paging.forPageIndex; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; +import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE; +import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_STATUSES; +import static org.sonar.server.ws.WsUtils.writeProtobuf; +import static org.sonarqube.ws.WsUtils.checkArgument; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_LIST; + +public class ListAction implements IssuesWsAction { + + private static final String PARAM_PROJECT = "project"; + private static final String PARAM_BRANCH = "branch"; + private static final String PARAM_PULL_REQUEST = "pullRequest"; + private static final String PARAM_COMPONENT = "component"; + private static final String PARAM_TYPES = "types"; + private static final String PARAM_RESOLVED = "resolved"; + private static final String PARAM_IN_NEW_CODE_PERIOD = "inNewCodePeriod"; + private final UserSession userSession; + private final DbClient dbClient; + private final Clock clock; + private final SearchResponseLoader searchResponseLoader; + private final SearchResponseFormat searchResponseFormat; + private final ComponentFinder componentFinder; + + public ListAction(UserSession userSession, DbClient dbClient, Clock clock, SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, ComponentFinder componentFinder) { + this.userSession = userSession; + this.dbClient = dbClient; + this.clock = clock; + this.searchResponseLoader = searchResponseLoader; + this.searchResponseFormat = searchResponseFormat; + this.componentFinder = componentFinder; + } + + @Override + public void define(WebService.NewController controller) { + WebService.NewAction action = controller + .createAction(ACTION_LIST) + .setHandler(this) + .setInternal(true) + .setDescription("List issues. This endpoint is used in degraded mode, when issue indexation is running." + + "
Either 'project' or 'component' parameter is required." + + "
Total number of issues will be always equal to a page size, as this counting all issues is not supported. " + + "
Requires the 'Browse' permission on the specified project. ") + .setSince("10.2") + .setResponseExample(getClass().getResource("list-example.json")); + + action.addPagingParams(100, MAX_PAGE_SIZE); + + action.createParam(PARAM_PROJECT) + .setDescription("Project key") + .setExampleValue("my-project-key"); + + action.createParam(PARAM_BRANCH) + .setDescription("Branch key. Not available in the community edition.") + .setExampleValue("feature/my-new-feature"); + + action.createParam(PARAM_PULL_REQUEST) + .setDescription("Filter issues that belong to the specified pull request. Not available in the community edition.") + .setExampleValue("42"); + + action.createParam(PARAM_COMPONENT) + .setDescription("Component key") + .setExampleValue("my_project:my_file.js"); + + action.createParam(PARAM_TYPES) + .setDescription("Comma-separated list of issue types") + .setExampleValue("BUG, VULNERABILITY") + .setPossibleValues(RuleType.BUG.name(), RuleType.VULNERABILITY.name(), RuleType.CODE_SMELL.name()); + + action.createParam(PARAM_IN_NEW_CODE_PERIOD) + .setDescription("Filter issues created in the new code period of the project") + .setExampleValue("true") + .setDefaultValue(false) + .setBooleanPossibleValues(); + + action.createParam(PARAM_RESOLVED) + .setDescription("Filter issues that are resolved or not, if not provided all issues will be returned") + .setExampleValue("true") + .setBooleanPossibleValues(); + + } + + @Override + public final void handle(Request request, Response response) { + WsRequest wsRequest = toWsRequest(request); + ProjectAndBranch projectAndBranch = validateRequest(wsRequest); + List issues = getIssueKeys(wsRequest, projectAndBranch); + Issues.ListWsResponse wsResponse = formatResponse(wsRequest, issues); + writeProtobuf(wsResponse, request, response); + } + + private static WsRequest toWsRequest(Request request) { + WsRequest wsRequest = new WsRequest(); + wsRequest.project(request.param(PARAM_PROJECT)); + wsRequest.component(request.param(PARAM_COMPONENT)); + wsRequest.branch(request.param(PARAM_BRANCH)); + wsRequest.pullRequest(request.param(PARAM_PULL_REQUEST)); + List types = request.paramAsStrings(PARAM_TYPES); + wsRequest.types(types == null ? List.of(RuleType.BUG.getDbConstant(), RuleType.VULNERABILITY.getDbConstant(), RuleType.CODE_SMELL.getDbConstant()) + : types.stream().map(RuleType::valueOf).map(RuleType::getDbConstant).toList()); + wsRequest.newCodePeriod(request.mandatoryParamAsBoolean(PARAM_IN_NEW_CODE_PERIOD)); + wsRequest.resolved(request.paramAsBoolean(PARAM_RESOLVED)); + wsRequest.page(request.mandatoryParamAsInt(PAGE)); + wsRequest.pageSize(request.mandatoryParamAsInt(PAGE_SIZE)); + return wsRequest; + } + + private ProjectAndBranch validateRequest(WsRequest wsRequest) { + checkArgument(!isNullOrEmpty(wsRequest.project) || !isNullOrEmpty(wsRequest.component), + "Either '%s' or '%s' parameter must be provided", PARAM_PROJECT, PARAM_COMPONENT); + Preconditions.checkArgument(isNullOrEmpty(wsRequest.branch) || isNullOrEmpty(wsRequest.pullRequest), + "Only one of parameters '%s' and '%s' can be provided", PARAM_BRANCH, PARAM_PULL_REQUEST); + + ProjectAndBranch projectAndBranch; + try (DbSession dbSession = dbClient.openSession(false)) { + if (!isNullOrEmpty(wsRequest.component)) { + projectAndBranch = checkComponentPermission(wsRequest, dbSession); + } else { + projectAndBranch = checkProjectAndBranchPermission(wsRequest, dbSession); + } + } + return projectAndBranch; + } + + private ProjectAndBranch checkComponentPermission(WsRequest wsRequest, DbSession dbSession) { + ComponentDto componentDto = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, wsRequest.component, wsRequest.branch, wsRequest.pullRequest); + BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, componentDto.branchUuid()) + .orElseThrow(() -> new IllegalStateException("Branch does not exist: " + componentDto.branchUuid())); + ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid()) + .orElseThrow(() -> new IllegalArgumentException("Project does not exist: " + wsRequest.project)); + userSession.checkEntityPermission(UserRole.USER, projectDto); + return new ProjectAndBranch(projectDto, branchDto); + } + + private ProjectAndBranch checkProjectAndBranchPermission(WsRequest wsRequest, DbSession dbSession) { + ProjectAndBranch projectAndBranch = componentFinder.getProjectAndBranch(dbSession, wsRequest.project, wsRequest.branch, wsRequest.pullRequest); + userSession.checkEntityPermission(UserRole.USER, projectAndBranch.getProject()); + return projectAndBranch; + } + + private List getIssueKeys(WsRequest wsRequest, ProjectAndBranch projectAndBranch) { + try (DbSession dbSession = dbClient.openSession(false)) { + BranchDto branch = projectAndBranch.getBranch(); + IssueListQuery.IssueListQueryBuilder queryBuilder = IssueListQuery.IssueListQueryBuilder.newIssueListQueryBuilder() + .project(wsRequest.project) + .component(wsRequest.component) + .branch(branch.getBranchKey()) + .pullRequest(branch.getPullRequestKey()) + .resolved(wsRequest.resolved) + .statuses(ISSUE_STATUSES) + .types(wsRequest.types); + + if (wsRequest.inNewCodePeriod) { + setNewCodePeriod(dbSession, wsRequest, queryBuilder); + } + + Pagination pagination = Pagination.forPage(wsRequest.page).andSize(wsRequest.pageSize); + return dbClient.issueDao().selectByQuery(dbSession, queryBuilder.build(), pagination); + } + } + + private void setNewCodePeriod(DbSession dbSession, WsRequest wsRequest, IssueListQuery.IssueListQueryBuilder queryBuilder) { + ComponentDto componentDto = dbClient.componentDao().selectByKeyAndBranch(dbSession, wsRequest.project, wsRequest.branch) + .orElseThrow(() -> new IllegalStateException(format("Could not find component for project: %s, branch: %s", wsRequest.project, wsRequest.branch))); + Optional snapshot = getLastAnalysis(dbSession, componentDto); + if (snapshot.isPresent() && isLastAnalysisFromReAnalyzedReferenceBranch(dbSession, snapshot.get())) { + queryBuilder.newCodeOnReference(true); + } else { + // if last analysis has no period date, then no issue should be considered new. + long createdAfterFromSnapshot = snapshot.map(SnapshotDto::getPeriodDate).orElse(clock.millis()); + queryBuilder.createdAfter(createdAfterFromSnapshot); + } + } + + private Optional getLastAnalysis(DbSession dbSession, ComponentDto component) { + return dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid()); + } + + private boolean isLastAnalysisFromReAnalyzedReferenceBranch(DbSession dbSession, SnapshotDto snapshot) { + return isLastAnalysisUsingReferenceBranch(snapshot) && + isLastAnalysisFromSonarQube94Onwards(dbSession, snapshot.getRootComponentUuid()); + } + + private boolean isLastAnalysisFromSonarQube94Onwards(DbSession dbSession, String componentUuid) { + return dbClient.liveMeasureDao().selectMeasure(dbSession, componentUuid, ANALYSIS_FROM_SONARQUBE_9_4_KEY).isPresent(); + } + + private static boolean isLastAnalysisUsingReferenceBranch(SnapshotDto snapshot) { + return !isNullOrEmpty(snapshot.getPeriodMode()) && REFERENCE_BRANCH.name().equals(snapshot.getPeriodMode()); + } + + private Issues.ListWsResponse formatResponse(WsRequest request, List issues) { + Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder(); + response.setPaging(Common.Paging.newBuilder() + .setPageIndex(request.page) + .setPageSize(issues.size()) + .build()); + + List issueKeys = issues.stream().map(IssueDto::getKey).toList(); + SearchResponseLoader.Collector collector = new SearchResponseLoader.Collector(issueKeys); + collectLoggedInUser(collector); + SearchResponseData preloadedData = new SearchResponseData(issues); + EnumSet additionalFields = EnumSet.of(SearchAdditionalField.ACTIONS, SearchAdditionalField.COMMENTS, SearchAdditionalField.TRANSITIONS); + SearchResponseData data = searchResponseLoader.load(preloadedData, collector, additionalFields, null); + + Paging paging = forPageIndex(request.page) + .withPageSize(request.pageSize) + .andTotal(request.pageSize); + return searchResponseFormat.formatList(additionalFields, data, paging); + } + + private void collectLoggedInUser(SearchResponseLoader.Collector collector) { + if (userSession.isLoggedIn()) { + collector.addUserUuids(singletonList(userSession.getUuid())); + } + } + + private static class WsRequest { + private String project = null; + private String component = null; + private String branch = null; + private String pullRequest = null; + private List types = null; + private boolean inNewCodePeriod = false; + private Boolean resolved = null; + private int page = 1; + private int pageSize = 100; + + public WsRequest project(@Nullable String project) { + this.project = project; + return this; + } + + public WsRequest component(@Nullable String component) { + this.component = component; + return this; + } + + public WsRequest branch(@Nullable String branch) { + this.branch = branch; + return this; + } + + public WsRequest pullRequest(@Nullable String pullRequest) { + this.pullRequest = pullRequest; + return this; + } + + public WsRequest types(@Nullable List types) { + this.types = types; + return this; + } + + public WsRequest newCodePeriod(boolean newCodePeriod) { + inNewCodePeriod = newCodePeriod; + return this; + } + + public WsRequest resolved(@Nullable Boolean resolved) { + this.resolved = resolved; + return this; + } + + public WsRequest page(int page) { + this.page = page; + return this; + } + + public WsRequest pageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index 843aecd9ed8..ed1704a316e 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -118,6 +118,14 @@ public class SearchResponseFormat { return response.build(); } + Issues.ListWsResponse formatList(Set fields, SearchResponseData data, Paging paging) { + Issues.ListWsResponse.Builder response = Issues.ListWsResponse.newBuilder(); + + response.setPaging(formatPaging(paging)); + response.addAllIssues(createIssues(fields, data)); + return response.build(); + } + Operation formatOperation(SearchResponseData data) { Operation.Builder response = Operation.newBuilder(); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java index 8791003f93f..634b5962bcc 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java @@ -27,6 +27,7 @@ public class IssuesWsParameters { public static final String CONTROLLER_ISSUES = "api/issues"; public static final String ACTION_SEARCH = "search"; + public static final String ACTION_LIST = "list"; public static final String ACTION_CHANGELOG = "changelog"; public static final String ACTION_ADD_COMMENT = "add_comment"; public static final String ACTION_EDIT_COMMENT = "edit_comment"; diff --git a/sonar-ws/src/main/protobuf/ws-issues.proto b/sonar-ws/src/main/protobuf/ws-issues.proto index 1956a10558a..bb1e56e870c 100644 --- a/sonar-ws/src/main/protobuf/ws-issues.proto +++ b/sonar-ws/src/main/protobuf/ws-issues.proto @@ -296,3 +296,9 @@ message Flow { optional string description = 2; optional sonarqube.ws.commons.FlowType type = 3; } + +// Response of GET api/issues/list +message ListWsResponse { + optional sonarqube.ws.commons.Paging paging = 1; + repeated Issue issues = 2; +} -- 2.39.5