From 333daf6a6fdf749f2c96b41ca6a4f8afe6dfc4ac Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Fri, 6 Dec 2019 17:08:53 +0100 Subject: [PATCH] SONAR-12719 add changelog to response of WS api/hotspots/show --- .../sonar/server/hotspot/ws/ShowAction.java | 19 +- .../sonar/server/issue/IssueChangelog.java | 237 ++++++++++++++++++ .../server/issue/ws/ChangelogAction.java | 148 ++--------- .../sonar/server/issue/ws/IssueWsModule.java | 4 +- .../server/hotspot/ws/ShowActionTest.java | 59 ++++- .../server/issue/ws/ChangelogActionTest.java | 6 +- .../server/issue/ws/IssueWsModuleTest.java | 2 +- sonar-ws/src/main/protobuf/ws-hotspots.proto | 1 + 8 files changed, 346 insertions(+), 130 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java index 5aa202080ae..083cf8edc54 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ShowAction.java @@ -19,7 +19,9 @@ */ package org.sonar.server.hotspot.ws; +import com.google.common.collect.ImmutableSet; import java.util.Objects; +import java.util.Set; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.server.ws.Request; @@ -33,6 +35,8 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.IssueChangelog; +import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext; import org.sonar.server.issue.TextRangeResponseFormatter; import org.sonar.server.security.SecurityStandards; import org.sonar.server.user.UserSession; @@ -54,12 +58,15 @@ public class ShowAction implements HotspotsWsAction { private final UserSession userSession; private final HotspotWsResponseFormatter responseFormatter; private final TextRangeResponseFormatter textRangeFormatter; + private final IssueChangelog issueChangelog; - public ShowAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter) { + public ShowAction(DbClient dbClient, UserSession userSession, HotspotWsResponseFormatter responseFormatter, + TextRangeResponseFormatter textRangeFormatter, IssueChangelog issueChangelog) { this.dbClient = dbClient; this.userSession = userSession; this.responseFormatter = responseFormatter; this.textRangeFormatter = textRangeFormatter; + this.issueChangelog = issueChangelog; } @Override @@ -95,6 +102,7 @@ public class ShowAction implements HotspotsWsAction { formatComponents(components, responseBuilder); formatRule(responseBuilder, rule); formatTextRange(hotspot, responseBuilder); + formatChangelog(dbSession, hotspot, components, responseBuilder); writeProtobuf(responseBuilder.build(), request, response); } @@ -140,6 +148,15 @@ public class ShowAction implements HotspotsWsAction { textRangeFormatter.formatTextRange(hotspot, responseBuilder::setTextRange); } + private void formatChangelog(DbSession dbSession, IssueDto hotspot, Components components, ShowWsResponse.Builder responseBuilder) { + Set preloadedComponents = ImmutableSet.of(components.project, components.component); + ChangelogLoadingContext changelogLoadingContext = issueChangelog + .newChangelogLoadingContext(dbSession, hotspot, ImmutableSet.of(), preloadedComponents); + + issueChangelog.formatChangelog(dbSession, changelogLoadingContext) + .forEach(responseBuilder::addChangelog); + } + private RuleDefinitionDto loadRule(DbSession dbSession, IssueDto hotspot) { RuleKey ruleKey = hotspot.getRuleKey(); return dbClient.ruleDao().selectDefinitionByKey(dbSession, ruleKey) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java new file mode 100644 index 00000000000..56f0e70d017 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/IssueChangelog.java @@ -0,0 +1,237 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.core.issue.FieldDiffs; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.user.UserDto; +import org.sonarqube.ws.Common; + +import static com.google.common.base.Strings.emptyToNull; +import static java.util.Collections.emptyMap; +import static java.util.Optional.ofNullable; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.server.issue.IssueFieldsSetter.FILE; +import static org.sonar.server.issue.IssueFieldsSetter.TECHNICAL_DEBT; + +public class IssueChangelog { + private static final String EFFORT_CHANGELOG_KEY = "effort"; + + private final DbClient dbClient; + private final AvatarResolver avatarFactory; + + public IssueChangelog(DbClient dbClient, AvatarResolver avatarFactory) { + this.dbClient = dbClient; + this.avatarFactory = avatarFactory; + } + + public ChangelogLoadingContext newChangelogLoadingContext(DbSession dbSession, IssueDto dto) { + return newChangelogLoadingContext(dbSession, dto, ImmutableSet.of(), ImmutableSet.of()); + } + + public ChangelogLoadingContext newChangelogLoadingContext(DbSession dbSession, IssueDto dto, Set preloadedUsers, Set preloadedComponents) { + List changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dto.getKey()); + return new ChangelogLoadingContextImpl(changes, preloadedUsers, preloadedComponents); + } + + public Stream formatChangelog(DbSession dbSession, ChangelogLoadingContext loadingContext) { + Map usersByUuid = loadUsers(dbSession, loadingContext); + Map filesByUuid = loadFiles(dbSession, loadingContext); + FormatableChangeLog changeLogResults = new FormatableChangeLog(loadingContext.getChanges(), usersByUuid, filesByUuid); + + return changeLogResults.changes.stream() + .map(toWsChangelog(changeLogResults)); + } + + private Map loadUsers(DbSession dbSession, ChangelogLoadingContext loadingContext) { + List changes = loadingContext.getChanges(); + if (changes.isEmpty()) { + return emptyMap(); + } + + Set usersByUuid = loadingContext.getPreloadedUsers(); + + Set userUuids = changes.stream() + .filter(change -> change.userUuid() != null) + .map(FieldDiffs::userUuid) + .collect(toSet()); + if (userUuids.isEmpty()) { + return emptyMap(); + } + + Set missingUsersUuids = Sets.difference(userUuids, usersByUuid).immutableCopy(); + if (missingUsersUuids.isEmpty()) { + return usersByUuid.stream() + .filter(t -> userUuids.contains(t.getUuid())) + .collect(uniqueIndex(UserDto::getUuid, userUuids.size())); + } + + return Stream.concat( + usersByUuid.stream(), + dbClient.userDao().selectByUuids(dbSession, missingUsersUuids).stream()) + .filter(t -> userUuids.contains(t.getUuid())) + .collect(uniqueIndex(UserDto::getUuid, userUuids.size())); + } + + private Map loadFiles(DbSession dbSession, ChangelogLoadingContext loadingContext) { + List changes = loadingContext.getChanges(); + if (changes.isEmpty()) { + return emptyMap(); + } + + Set fileUuids = changes.stream() + .filter(diffs -> diffs.diffs().containsKey(FILE)) + .flatMap(diffs -> Stream.of(diffs.get(FILE).newValue().toString(), diffs.get(FILE).oldValue().toString())) + .collect(toSet()); + if (fileUuids.isEmpty()) { + return emptyMap(); + } + + Set preloadedComponents = loadingContext.getPreloadedComponents(); + Set preloadedComponentUuids = preloadedComponents.stream() + .map(ComponentDto::uuid) + .collect(toSet(preloadedComponents.size())); + Set missingFileUuids = Sets.difference(fileUuids, preloadedComponentUuids).immutableCopy(); + if (missingFileUuids.isEmpty()) { + return preloadedComponents.stream() + .filter(t -> fileUuids.contains(t.uuid())) + .collect(uniqueIndex(ComponentDto::uuid, fileUuids.size())); + } + + return Stream.concat( + preloadedComponents.stream(), + dbClient.componentDao().selectByUuids(dbSession, missingFileUuids).stream()) + .filter(t -> fileUuids.contains(t.uuid())) + .collect(uniqueIndex(ComponentDto::uuid, fileUuids.size())); + } + + public interface ChangelogLoadingContext { + List getChanges(); + + Set getPreloadedUsers(); + + Set getPreloadedComponents(); + } + + @Immutable + public static final class ChangelogLoadingContextImpl implements ChangelogLoadingContext { + private final List changes; + private final Set preloadedUsers; + private final Set preloadedComponents; + + private ChangelogLoadingContextImpl(List changes, Set preloadedUsers, Set preloadedComponents) { + this.changes = ImmutableList.copyOf(changes); + this.preloadedUsers = ImmutableSet.copyOf(preloadedUsers); + this.preloadedComponents = ImmutableSet.copyOf(preloadedComponents); + } + + @Override + public List getChanges() { + return changes; + } + + @Override + public Set getPreloadedUsers() { + return preloadedUsers; + } + + @Override + public Set getPreloadedComponents() { + return preloadedComponents; + } + } + + private Function toWsChangelog(FormatableChangeLog results) { + return change -> { + String userUUuid = change.userUuid(); + Common.Changelog.Builder changelogBuilder = Common.Changelog.newBuilder(); + changelogBuilder.setCreationDate(formatDateTime(change.creationDate())); + UserDto user = userUUuid == null ? null : results.users.get(userUUuid); + if (user != null) { + changelogBuilder.setUser(user.getLogin()); + changelogBuilder.setIsUserActive(user.isActive()); + ofNullable(user.getName()).ifPresent(changelogBuilder::setUserName); + ofNullable(emptyToNull(user.getEmail())).ifPresent(email -> changelogBuilder.setAvatar(avatarFactory.create(user))); + } + change.diffs().entrySet().stream() + .map(toWsDiff(results)) + .forEach(changelogBuilder::addDiffs); + return changelogBuilder.build(); + }; + } + + private static Function, Common.Changelog.Diff> toWsDiff(FormatableChangeLog results) { + return diff -> { + FieldDiffs.Diff value = diff.getValue(); + Common.Changelog.Diff.Builder diffBuilder = Common.Changelog.Diff.newBuilder(); + String key = diff.getKey(); + String oldValue = value.oldValue() != null ? value.oldValue().toString() : null; + String newValue = value.newValue() != null ? value.newValue().toString() : null; + if (key.equals(FILE)) { + diffBuilder.setKey(key); + ofNullable(results.getFileLongName(emptyToNull(newValue))).ifPresent(diffBuilder::setNewValue); + ofNullable(results.getFileLongName(emptyToNull(oldValue))).ifPresent(diffBuilder::setOldValue); + } else { + diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key); + ofNullable(emptyToNull(newValue)).ifPresent(diffBuilder::setNewValue); + ofNullable(emptyToNull(oldValue)).ifPresent(diffBuilder::setOldValue); + } + return diffBuilder.build(); + }; + } + + private static final class FormatableChangeLog { + private final List changes; + private final Map users; + private final Map files; + + private FormatableChangeLog(List changes, Map users, Map files) { + this.changes = changes; + this.users = users; + this.files = files; + } + + @CheckForNull + String getFileLongName(@Nullable String fileUuid) { + if (fileUuid == null) { + return null; + } + ComponentDto file = files.get(fileUuid); + return file == null ? null : file.longName(); + } + + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java index 675da21d846..ff6246b5149 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java @@ -19,61 +19,40 @@ */ package org.sonar.server.issue.ws; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; -import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; -import org.sonar.core.issue.FieldDiffs; -import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.issue.AvatarResolver; +import org.sonar.server.issue.IssueChangelog; import org.sonar.server.issue.IssueFinder; import org.sonar.server.user.UserSession; -import org.sonarqube.ws.Common.Changelog; import org.sonarqube.ws.Issues.ChangelogWsResponse; import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Strings.emptyToNull; -import static java.util.Optional.ofNullable; -import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; -import static org.sonar.server.issue.IssueFieldsSetter.FILE; -import static org.sonar.server.issue.IssueFieldsSetter.TECHNICAL_DEBT; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_CHANGELOG; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE; public class ChangelogAction implements IssuesWsAction { - private static final String EFFORT_CHANGELOG_KEY = "effort"; - private final DbClient dbClient; private final IssueFinder issueFinder; - private final AvatarResolver avatarFactory; private final UserSession userSession; + private final IssueChangelog issueChangelog; - public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, AvatarResolver avatarFactory, UserSession userSession) { + public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, UserSession userSession, IssueChangelog issueChangelog) { this.dbClient = dbClient; this.issueFinder = issueFinder; - this.avatarFactory = avatarFactory; this.userSession = userSession; + this.issueChangelog = issueChangelog; } @Override @@ -95,112 +74,31 @@ public class ChangelogAction implements IssuesWsAction { @Override public void handle(Request request, Response response) throws Exception { try (DbSession dbSession = dbClient.openSession(false)) { - ChangelogWsResponse wsResponse = Stream.of(request) - .map(searchChangelog(dbSession)) - .map(buildResponse()) - .collect(MoreCollectors.toOneElement()); - writeProtobuf(wsResponse, request, response); - } - } - - private Function searchChangelog(DbSession dbSession) { - return request -> new ChangeLogResults(dbSession, request.mandatoryParam(PARAM_ISSUE)); - } - - private Function buildResponse() { - return result -> Stream.of(ChangelogWsResponse.newBuilder()) - .peek(addChanges(result)) - .map(ChangelogWsResponse.Builder::build) - .collect(MoreCollectors.toOneElement()); - } - - private Consumer addChanges(ChangeLogResults results) { - return response -> results.changes.stream() - .map(toWsChangelog(results)) - .forEach(response::addChangelog); - } + IssueDto issue = issueFinder.getByKey(dbSession, request.mandatoryParam(PARAM_ISSUE)); - private Function toWsChangelog(ChangeLogResults results) { - return change -> { - String userUUuid = change.userUuid(); - Changelog.Builder changelogBuilder = Changelog.newBuilder(); - changelogBuilder.setCreationDate(formatDateTime(change.creationDate())); - UserDto user = userUUuid == null ? null : results.users.get(userUUuid); - if (user != null) { - changelogBuilder.setUser(user.getLogin()); - changelogBuilder.setIsUserActive(user.isActive()); - ofNullable(user.getName()).ifPresent(changelogBuilder::setUserName); - ofNullable(emptyToNull(user.getEmail())).ifPresent(email -> changelogBuilder.setAvatar(avatarFactory.create(user))); - } - change.diffs().entrySet().stream() - .map(toWsDiff(results)) - .forEach(changelogBuilder::addDiffs); - return changelogBuilder.build(); - }; - } - - private static Function, Changelog.Diff> toWsDiff(ChangeLogResults results) { - return diff -> { - FieldDiffs.Diff value = diff.getValue(); - Changelog.Diff.Builder diffBuilder = Changelog.Diff.newBuilder(); - String key = diff.getKey(); - String oldValue = value.oldValue() != null ? value.oldValue().toString() : null; - String newValue = value.newValue() != null ? value.newValue().toString() : null; - if (key.equals(FILE)) { - diffBuilder.setKey(key); - ofNullable(results.getFileLongName(emptyToNull(newValue))).ifPresent(diffBuilder::setNewValue); - ofNullable(results.getFileLongName(emptyToNull(oldValue))).ifPresent(diffBuilder::setOldValue); - } else { - diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key); - ofNullable(emptyToNull(newValue)).ifPresent(diffBuilder::setNewValue); - ofNullable(emptyToNull(oldValue)).ifPresent(diffBuilder::setOldValue); - } - return diffBuilder.build(); - }; - } - - private class ChangeLogResults { - private final List changes; - private final Map users; - private final Map files; - - ChangeLogResults(DbSession dbSession, String issueKey) { - IssueDto issue = issueFinder.getByKey(dbSession, issueKey); - if (isMember(dbSession, issue)) { - this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, issue.getKey()); - List userUuids = changes.stream().filter(change -> change.userUuid() != null).map(FieldDiffs::userUuid).collect(MoreCollectors.toList()); - this.users = dbClient.userDao().selectByUuids(dbSession, userUuids).stream().collect(MoreCollectors.uniqueIndex(UserDto::getUuid)); - this.files = dbClient.componentDao().selectByUuids(dbSession, getFileUuids(changes)).stream().collect(MoreCollectors.uniqueIndex(ComponentDto::uuid, Function.identity())); - } else { - changes = ImmutableList.of(); - users = ImmutableMap.of(); - files = ImmutableMap.of(); - } + ChangelogWsResponse build = handle(dbSession, issue); + writeProtobuf(build, request, response); } + } - private boolean isMember(DbSession dbSession, IssueDto issue) { - Optional project = dbClient.componentDao().selectByUuid(dbSession, issue.getProjectUuid()); - checkState(project.isPresent(), "Cannot find the project with uuid %s from issue.id %s", issue.getProjectUuid(), issue.getId()); - Optional organization = dbClient.organizationDao().selectByUuid(dbSession, project.get().getOrganizationUuid()); - checkState(organization.isPresent(), "Cannot find the organization with uuid %s from issue.id %s", project.get().getOrganizationUuid(), issue.getId()); - return userSession.hasMembership(organization.get()); + public ChangelogWsResponse handle(DbSession dbSession, IssueDto issue) { + if (!isMember(dbSession, issue)) { + return ChangelogWsResponse.newBuilder().build(); } - private Set getFileUuids(List changes) { - return changes.stream() - .filter(diffs -> diffs.diffs().containsKey(FILE)) - .flatMap(diffs -> Stream.of(diffs.get(FILE).newValue().toString(), diffs.get(FILE).oldValue().toString())) - .collect(MoreCollectors.toSet()); - } + IssueChangelog.ChangelogLoadingContext loadingContext = issueChangelog.newChangelogLoadingContext(dbSession, issue); - @CheckForNull - String getFileLongName(@Nullable String fileUuid) { - if (fileUuid == null) { - return null; - } - ComponentDto file = files.get(fileUuid); - return file == null ? null : file.longName(); - } + ChangelogWsResponse.Builder builder = ChangelogWsResponse.newBuilder(); + issueChangelog.formatChangelog(dbSession, loadingContext) + .forEach(builder::addChangelog); + return builder.build(); + } + private boolean isMember(DbSession dbSession, IssueDto issue) { + Optional project = dbClient.componentDao().selectByUuid(dbSession, issue.getProjectUuid()); + checkState(project.isPresent(), "Cannot find the project with uuid %s from issue.id %s", issue.getProjectUuid(), issue.getId()); + Optional organization = dbClient.organizationDao().selectByUuid(dbSession, project.get().getOrganizationUuid()); + checkState(organization.isPresent(), "Cannot find the organization with uuid %s from issue.id %s", project.get().getOrganizationUuid(), issue.getId()); + return userSession.hasMembership(organization.get()); } } 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 1103eee6efa..e66b4f30691 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 @@ -21,9 +21,10 @@ package org.sonar.server.issue.ws; import org.sonar.core.platform.Module; import org.sonar.server.issue.AvatarResolverImpl; -import org.sonar.server.issue.TextRangeResponseFormatter; +import org.sonar.server.issue.IssueChangelog; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; +import org.sonar.server.issue.TextRangeResponseFormatter; import org.sonar.server.issue.TransitionService; import org.sonar.server.issue.WebIssueStorage; import org.sonar.server.issue.index.IssueQueryFactory; @@ -45,6 +46,7 @@ public class IssueWsModule extends Module { IssueQueryFactory.class, IssuesWs.class, AvatarResolverImpl.class, + IssueChangelog.class, SearchResponseLoader.class, TextRangeResponseFormatter.class, SearchResponseFormat.class, diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java index 61bcb3c5bec..0c68ab3f420 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java @@ -25,20 +25,25 @@ import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Random; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; import org.sonar.api.issue.Issue; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.System2; import org.sonar.api.web.UserRole; import org.sonar.db.DbClient; +import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; @@ -51,6 +56,8 @@ import org.sonar.server.issue.TextRangeResponseFormatter; import org.sonar.server.es.EsTester; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.IssueChangelog; +import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.security.SecurityStandards; import org.sonar.server.security.SecurityStandards.SQCategory; @@ -63,6 +70,12 @@ import org.sonarqube.ws.Hotspots; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; import static org.sonar.db.component.ComponentTesting.newFileDto; @@ -82,8 +95,9 @@ public class ShowActionTest { private TextRangeResponseFormatter commonFormatter = new TextRangeResponseFormatter(); private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider); + private IssueChangelog issueChangelog = Mockito.mock(IssueChangelog.class); - private ShowAction underTest = new ShowAction(dbClient, userSessionRule, responseFormatter, commonFormatter); + private ShowAction underTest = new ShowAction(dbClient, userSessionRule, responseFormatter, commonFormatter, issueChangelog); private WsActionTester actionTester = new WsActionTester(underTest); @Test @@ -353,6 +367,36 @@ public class ShowActionTest { verifyComponent(response.getComponent(), project); } + @Test + public void returns_hotspot_changelog() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + IssueDto hotspot = dbTester.issues().insertIssue(newHotspot(project, file, rule) + .setLocations(DbIssues.Locations.newBuilder() + .setTextRange(DbCommons.TextRange.newBuilder().build()) + .build())); + ChangelogLoadingContext changelogLoadingContext = Mockito.mock(ChangelogLoadingContext.class); + List changelog = IntStream.range(0, 1 + new Random().nextInt(12)) + .mapToObj(i -> Common.Changelog.newBuilder().setUser("u" + i).build()) + .collect(Collectors.toList()); + when(issueChangelog.newChangelogLoadingContext(any(), any(), anySet(), anySet())).thenReturn(changelogLoadingContext); + when(issueChangelog.formatChangelog(any(), eq(changelogLoadingContext))) + .thenReturn(changelog.stream()); + + Hotspots.ShowWsResponse response = newRequest(hotspot) + .executeProtobuf(Hotspots.ShowWsResponse.class); + + assertThat(response.getChangelogList()) + .extracting(Common.Changelog::getUser) + .containsExactly(changelog.stream().map(Common.Changelog::getUser).toArray(String[]::new)); + verify(issueChangelog).newChangelogLoadingContext(any(DbSession.class), + argThat(new IssueDtoArgumentMatcher(hotspot)), + eq(Collections.emptySet()), eq(ImmutableSet.of(project, file))); + verify(issueChangelog).formatChangelog(any(DbSession.class), eq(changelogLoadingContext)); + } + public void verifyRule(Hotspots.Rule wsRule, RuleDefinitionDto dto) { assertThat(wsRule.getKey()).isEqualTo(dto.getKey().toString()); assertThat(wsRule.getName()).isEqualTo(dto.getName()); @@ -394,4 +438,17 @@ public class ShowActionTest { return ruleDefinition; } + private static class IssueDtoArgumentMatcher implements ArgumentMatcher { + private final IssueDto expected; + + private IssueDtoArgumentMatcher(IssueDto expected) { + this.expected = expected; + } + + @Override + public boolean matches(IssueDto argument) { + return argument != null && argument.getKey().equals(expected.getKey()); + } + } + } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java index 02aa097e1dd..aa447b0402e 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java @@ -38,6 +38,7 @@ import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTesting; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.issue.AvatarResolverImpl; +import org.sonar.server.issue.IssueChangelog; import org.sonar.server.issue.IssueFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -69,7 +70,10 @@ public class ChangelogActionTest { private ComponentDto project; private ComponentDto file; - private WsActionTester tester = new WsActionTester(new ChangelogAction(db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new AvatarResolverImpl(), userSession)); + private IssueFinder issueFinder = new IssueFinder(db.getDbClient(), userSession); + private IssueChangelog issueChangelog = new IssueChangelog(db.getDbClient(), new AvatarResolverImpl()); + private ChangelogAction underTest = new ChangelogAction(db.getDbClient(), issueFinder, userSession, issueChangelog); + private WsActionTester tester = new WsActionTester(underTest); @Before public void setUp() { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java index 4e41efcfd6f..7b1220885fe 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java @@ -30,7 +30,7 @@ public class IssueWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new IssueWsModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 29); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 30); } } diff --git a/sonar-ws/src/main/protobuf/ws-hotspots.proto b/sonar-ws/src/main/protobuf/ws-hotspots.proto index e851afb7adb..a4ce5baed97 100644 --- a/sonar-ws/src/main/protobuf/ws-hotspots.proto +++ b/sonar-ws/src/main/protobuf/ws-hotspots.proto @@ -66,6 +66,7 @@ message ShowWsResponse { optional string creationDate = 11; optional string updateDate = 12; optional sonarqube.ws.commons.TextRange textRange = 13; + repeated sonarqube.ws.commons.Changelog changelog = 14; } message Component { -- 2.39.5