@@ -38,8 +38,9 @@ import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.IssueChangelog; | |||
import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport.Load; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||
import org.sonar.server.issue.ws.UserResponseFormatter; | |||
import org.sonar.server.security.SecurityStandards; | |||
@@ -50,6 +51,7 @@ import org.sonarqube.ws.Hotspots.ShowWsResponse; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static com.google.common.base.Strings.nullToEmpty; | |||
import static java.lang.String.format; | |||
import static java.util.Collections.singleton; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.api.utils.DateUtils.formatDateTime; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
@@ -63,17 +65,17 @@ public class ShowAction implements HotspotsWsAction { | |||
private final HotspotWsResponseFormatter responseFormatter; | |||
private final TextRangeResponseFormatter textRangeFormatter; | |||
private final UserResponseFormatter userFormatter; | |||
private final IssueChangelog issueChangelog; | |||
private final IssueChangeWSSupport issueChangeSupport; | |||
public ShowAction(DbClient dbClient, HotspotWsSupport hotspotWsSupport, | |||
HotspotWsResponseFormatter responseFormatter, TextRangeResponseFormatter textRangeFormatter, | |||
UserResponseFormatter userFormatter, IssueChangelog issueChangelog) { | |||
UserResponseFormatter userFormatter, IssueChangeWSSupport issueChangeSupport) { | |||
this.dbClient = dbClient; | |||
this.hotspotWsSupport = hotspotWsSupport; | |||
this.responseFormatter = responseFormatter; | |||
this.textRangeFormatter = textRangeFormatter; | |||
this.userFormatter = userFormatter; | |||
this.issueChangelog = issueChangelog; | |||
this.issueChangeSupport = issueChangeSupport; | |||
} | |||
@Override | |||
@@ -109,7 +111,7 @@ public class ShowAction implements HotspotsWsAction { | |||
formatComponents(components, responseBuilder); | |||
formatRule(responseBuilder, rule); | |||
formatTextRange(hotspot, responseBuilder); | |||
formatChangelog(dbSession, hotspot, components, responseBuilder); | |||
formatChangeLogAndComments(dbSession, hotspot, components, responseBuilder); | |||
writeProtobuf(responseBuilder.build(), request, response); | |||
} | |||
@@ -175,13 +177,15 @@ public class ShowAction implements HotspotsWsAction { | |||
textRangeFormatter.formatTextRange(hotspot, responseBuilder::setTextRange); | |||
} | |||
private void formatChangelog(DbSession dbSession, IssueDto hotspot, Components components, ShowWsResponse.Builder responseBuilder) { | |||
private void formatChangeLogAndComments(DbSession dbSession, IssueDto hotspot, Components components, ShowWsResponse.Builder responseBuilder) { | |||
Set<ComponentDto> preloadedComponents = ImmutableSet.of(components.project, components.component); | |||
ChangelogLoadingContext changelogLoadingContext = issueChangelog | |||
.newChangelogLoadingContext(dbSession, hotspot, ImmutableSet.of(), preloadedComponents); | |||
FormattingContext formattingContext = issueChangeSupport | |||
.newFormattingContext(dbSession, singleton(hotspot), Load.ALL, ImmutableSet.of(), preloadedComponents); | |||
issueChangelog.formatChangelog(dbSession, changelogLoadingContext) | |||
issueChangeSupport.formatChangelog(hotspot, formattingContext) | |||
.forEach(responseBuilder::addChangelog); | |||
issueChangeSupport.formatComments(hotspot, Common.Comment.newBuilder(), formattingContext) | |||
.forEach(responseBuilder::addComment); | |||
} | |||
private RuleDefinitionDto loadRule(DbSession dbSession, IssueDto hotspot) { |
@@ -0,0 +1,354 @@ | |||
/* | |||
* 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.base.Strings; | |||
import com.google.common.collect.ImmutableList; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.Multimap; | |||
import com.google.common.collect.Ordering; | |||
import com.google.common.collect.Sets; | |||
import java.io.Serializable; | |||
import java.util.Collection; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.api.utils.DateUtils; | |||
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.IssueChangeDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.markdown.Markdown; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Common; | |||
import static com.google.common.base.Strings.emptyToNull; | |||
import static java.util.Collections.emptyMap; | |||
import static java.util.Optional.empty; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.api.utils.DateUtils.formatDateTime; | |||
import static org.sonar.core.util.stream.MoreCollectors.toList; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT; | |||
import static org.sonar.db.issue.IssueChangeDto.TYPE_FIELD_CHANGE; | |||
import static org.sonar.server.issue.IssueFieldsSetter.FILE; | |||
import static org.sonar.server.issue.IssueFieldsSetter.TECHNICAL_DEBT; | |||
public class IssueChangeWSSupport { | |||
private static final String EFFORT_CHANGELOG_KEY = "effort"; | |||
private static final Ordering<IssueChangeDto> ISSUE_CHANGE_CREATED_AT_COMPARATOR = Ordering.natural().onResultOf(IssueChangeDto::getCreatedAt); | |||
private final DbClient dbClient; | |||
private final AvatarResolver avatarFactory; | |||
private final UserSession userSession; | |||
public IssueChangeWSSupport(DbClient dbClient, AvatarResolver avatarFactory, UserSession userSession) { | |||
this.dbClient = dbClient; | |||
this.avatarFactory = avatarFactory; | |||
this.userSession = userSession; | |||
} | |||
public enum Load { | |||
CHANGE_LOG, COMMENTS, ALL; | |||
} | |||
public interface FormattingContext { | |||
List<FieldDiffs> getChanges(IssueDto dto); | |||
List<IssueChangeDto> getComments(IssueDto dto); | |||
Optional<UserDto> getUserByUuid(@Nullable String uuid); | |||
Optional<ComponentDto> getFileByUuid(@Nullable String uuid); | |||
boolean isUpdatableComment(IssueChangeDto comment); | |||
} | |||
public FormattingContext newFormattingContext(DbSession dbSession, Set<IssueDto> dtos, Load load) { | |||
return newFormattingContext(dbSession, dtos, load, ImmutableSet.of(), ImmutableSet.of()); | |||
} | |||
public FormattingContext newFormattingContext(DbSession dbSession, Set<IssueDto> dtos, Load load, | |||
Set<UserDto> preloadedUsers, Set<ComponentDto> preloadedComponents) { | |||
Set<String> issueKeys = dtos.stream().map(IssueDto::getKey).collect(toSet()); | |||
List<IssueChangeDto> changes = ImmutableList.of(); | |||
List<IssueChangeDto> comments = ImmutableList.of(); | |||
switch (load) { | |||
case CHANGE_LOG: | |||
changes = dbClient.issueChangeDao().selectByTypeAndIssueKeys(dbSession, issueKeys, TYPE_FIELD_CHANGE); | |||
break; | |||
case COMMENTS: | |||
comments = dbClient.issueChangeDao().selectByTypeAndIssueKeys(dbSession, issueKeys, TYPE_COMMENT); | |||
break; | |||
case ALL: | |||
List<IssueChangeDto> all = dbClient.issueChangeDao().selectByIssueKeys(dbSession, issueKeys); | |||
changes = all.stream() | |||
.filter(t -> TYPE_FIELD_CHANGE.equals(t.getChangeType())) | |||
.collect(toList()); | |||
comments = all.stream() | |||
.filter(t -> TYPE_COMMENT.equals(t.getChangeType())) | |||
.collect(toList()); | |||
break; | |||
default: | |||
throw new IllegalStateException("Unsupported Load value:" + load); | |||
} | |||
Map<String, List<FieldDiffs>> changesByRuleKey = indexAndSort(changes, IssueChangeDto::toFieldDiffs); | |||
Map<String, List<IssueChangeDto>> commentsByIssueKey = indexAndSort(comments, t -> t); | |||
Map<String, UserDto> usersByUuid = loadUsers(dbSession, changesByRuleKey, commentsByIssueKey, preloadedUsers); | |||
Map<String, ComponentDto> filesByUuid = loadFiles(dbSession, changesByRuleKey, preloadedComponents); | |||
Map<String, Boolean> updatableCommentByKey = loadUpdatableFlag(commentsByIssueKey); | |||
return new FormattingContextImpl(changesByRuleKey, commentsByIssueKey, usersByUuid, filesByUuid, updatableCommentByKey); | |||
} | |||
private static <T> Map<String, List<T>> indexAndSort(List<IssueChangeDto> changes, Function<IssueChangeDto, T> transform) { | |||
Multimap<String, IssueChangeDto> unordered = changes.stream() | |||
.collect(MoreCollectors.index(IssueChangeDto::getIssueKey, t -> t)); | |||
return unordered.asMap().entrySet().stream() | |||
.collect(uniqueIndex( | |||
Map.Entry::getKey, | |||
t -> t.getValue().stream() | |||
.sorted(ISSUE_CHANGE_CREATED_AT_COMPARATOR) | |||
.map(transform) | |||
.collect(toList(t.getValue().size())))); | |||
} | |||
private Map<String, UserDto> loadUsers(DbSession dbSession, Map<String, List<FieldDiffs>> changesByRuleKey, | |||
Map<String, List<IssueChangeDto>> commentsByIssueKey, Set<UserDto> preloadedUsers) { | |||
Set<String> userUuids = Stream.concat( | |||
changesByRuleKey.values().stream() | |||
.flatMap(Collection::stream) | |||
.map(FieldDiffs::userUuid), | |||
commentsByIssueKey.values().stream() | |||
.flatMap(Collection::stream) | |||
.map(IssueChangeDto::getUserUuid)) | |||
.filter(Objects::nonNull) | |||
.collect(toSet()); | |||
if (userUuids.isEmpty()) { | |||
return emptyMap(); | |||
} | |||
Set<String> preloadedUserUuids = preloadedUsers.stream().map(UserDto::getUuid).collect(Collectors.toSet()); | |||
Set<String> missingUsersUuids = Sets.difference(userUuids, preloadedUserUuids).immutableCopy(); | |||
if (missingUsersUuids.isEmpty()) { | |||
return preloadedUsers.stream() | |||
.filter(t -> userUuids.contains(t.getUuid())) | |||
.collect(uniqueIndex(UserDto::getUuid, userUuids.size())); | |||
} | |||
return Stream.concat( | |||
preloadedUsers.stream(), | |||
dbClient.userDao().selectByUuids(dbSession, missingUsersUuids).stream()) | |||
.filter(t -> userUuids.contains(t.getUuid())) | |||
.collect(uniqueIndex(UserDto::getUuid, userUuids.size())); | |||
} | |||
private Map<String, ComponentDto> loadFiles(DbSession dbSession, Map<String, List<FieldDiffs>> changesByRuleKey, Set<ComponentDto> preloadedComponents) { | |||
Set<String> fileUuids = changesByRuleKey.values().stream() | |||
.flatMap(Collection::stream) | |||
.flatMap(diffs -> { | |||
FieldDiffs.Diff diff = diffs.get(FILE); | |||
if (diff == null) { | |||
return Stream.empty(); | |||
} | |||
return Stream.of(toString(diff.newValue()), toString(diff.oldValue())); | |||
}) | |||
.map(Strings::emptyToNull) | |||
.filter(Objects::nonNull) | |||
.collect(toSet()); | |||
if (fileUuids.isEmpty()) { | |||
return emptyMap(); | |||
} | |||
Set<String> preloadedFileUuids = preloadedComponents.stream().map(ComponentDto::uuid).collect(Collectors.toSet()); | |||
Set<String> missingFileUuids = Sets.difference(fileUuids, preloadedFileUuids).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())); | |||
} | |||
private Map<String, Boolean> loadUpdatableFlag(Map<String, List<IssueChangeDto>> commentsByIssueKey) { | |||
if (!userSession.isLoggedIn()) { | |||
return emptyMap(); | |||
} | |||
String userUuid = userSession.getUuid(); | |||
if (userUuid == null) { | |||
return emptyMap(); | |||
} | |||
return commentsByIssueKey.values().stream() | |||
.flatMap(Collection::stream) | |||
.collect(uniqueIndex(IssueChangeDto::getKey, t -> userUuid.equals(t.getUserUuid()))); | |||
} | |||
public Stream<Common.Changelog> formatChangelog(IssueDto dto, FormattingContext formattingContext) { | |||
return formattingContext.getChanges(dto).stream() | |||
.map(toWsChangelog(formattingContext)); | |||
} | |||
private Function<FieldDiffs, Common.Changelog> toWsChangelog(FormattingContext formattingContext) { | |||
return change -> { | |||
String userUUuid = change.userUuid(); | |||
Common.Changelog.Builder changelogBuilder = Common.Changelog.newBuilder(); | |||
changelogBuilder.setCreationDate(formatDateTime(change.creationDate())); | |||
formattingContext.getUserByUuid(userUUuid) | |||
.ifPresent(user -> { | |||
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(formattingContext)) | |||
.forEach(changelogBuilder::addDiffs); | |||
return changelogBuilder.build(); | |||
}; | |||
} | |||
private static Function<Map.Entry<String, FieldDiffs.Diff>, Common.Changelog.Diff> toWsDiff(FormattingContext formattingContext) { | |||
return diff -> { | |||
FieldDiffs.Diff value = diff.getValue(); | |||
Common.Changelog.Diff.Builder diffBuilder = Common.Changelog.Diff.newBuilder(); | |||
String key = diff.getKey(); | |||
String oldValue = emptyToNull(toString(value.oldValue())); | |||
String newValue = emptyToNull(toString(value.newValue())); | |||
if (key.equals(FILE)) { | |||
diffBuilder.setKey(key); | |||
formattingContext.getFileByUuid(newValue).map(ComponentDto::longName).ifPresent(diffBuilder::setNewValue); | |||
formattingContext.getFileByUuid(oldValue).map(ComponentDto::longName).ifPresent(diffBuilder::setOldValue); | |||
} else { | |||
diffBuilder.setKey(key.equals(TECHNICAL_DEBT) ? EFFORT_CHANGELOG_KEY : key); | |||
ofNullable(newValue).ifPresent(diffBuilder::setNewValue); | |||
ofNullable(oldValue).ifPresent(diffBuilder::setOldValue); | |||
} | |||
return diffBuilder.build(); | |||
}; | |||
} | |||
public Stream<Common.Comment> formatComments(IssueDto dto, Common.Comment.Builder commentBuilder, FormattingContext formattingContext) { | |||
return formattingContext.getComments(dto).stream() | |||
.map(comment -> { | |||
commentBuilder | |||
.clear() | |||
.setKey(comment.getKey()) | |||
.setUpdatable(formattingContext.isUpdatableComment(comment)) | |||
.setCreatedAt(DateUtils.formatDateTime(new Date(comment.getIssueChangeCreationDate()))); | |||
String markdown = comment.getChangeData(); | |||
formattingContext.getUserByUuid(comment.getUserUuid()).ifPresent(user -> commentBuilder.setLogin(user.getLogin())); | |||
if (markdown != null) { | |||
commentBuilder | |||
.setHtmlText(Markdown.convertToHtml(markdown)) | |||
.setMarkdown(markdown); | |||
} | |||
return commentBuilder.build(); | |||
}); | |||
} | |||
private static String toString(@Nullable Serializable serializable) { | |||
if (serializable != null) { | |||
return serializable.toString(); | |||
} | |||
return null; | |||
} | |||
@Immutable | |||
public static final class FormattingContextImpl implements FormattingContext { | |||
private final Map<String, List<FieldDiffs>> changesByIssueKey; | |||
private final Map<String, List<IssueChangeDto>> commentsByIssueKey; | |||
private final Map<String, UserDto> usersByUuid; | |||
private final Map<String, ComponentDto> filesByUuid; | |||
private final Map<String, Boolean> updatableCommentByKey; | |||
private FormattingContextImpl(Map<String, List<FieldDiffs>> changesByIssueKey, | |||
Map<String, List<IssueChangeDto>> commentsByIssueKey, | |||
Map<String, UserDto> usersByUuid, Map<String, ComponentDto> filesByUuid, | |||
Map<String, Boolean> updatableCommentByKey) { | |||
this.changesByIssueKey = changesByIssueKey; | |||
this.commentsByIssueKey = commentsByIssueKey; | |||
this.usersByUuid = usersByUuid; | |||
this.filesByUuid = filesByUuid; | |||
this.updatableCommentByKey = updatableCommentByKey; | |||
} | |||
@Override | |||
public List<FieldDiffs> getChanges(IssueDto dto) { | |||
List<FieldDiffs> fieldDiffs = changesByIssueKey.get(dto.getKey()); | |||
if (fieldDiffs == null) { | |||
return ImmutableList.of(); | |||
} | |||
return ImmutableList.copyOf(fieldDiffs); | |||
} | |||
@Override | |||
public List<IssueChangeDto> getComments(IssueDto dto) { | |||
List<IssueChangeDto> comments = commentsByIssueKey.get(dto.getKey()); | |||
if (comments == null) { | |||
return ImmutableList.of(); | |||
} | |||
return ImmutableList.copyOf(comments); | |||
} | |||
@Override | |||
public Optional<UserDto> getUserByUuid(@Nullable String uuid) { | |||
if (uuid == null) { | |||
return empty(); | |||
} | |||
return Optional.ofNullable(usersByUuid.get(uuid)); | |||
} | |||
@Override | |||
public Optional<ComponentDto> getFileByUuid(@Nullable String uuid) { | |||
if (uuid == null) { | |||
return empty(); | |||
} | |||
return Optional.ofNullable(filesByUuid.get(uuid)); | |||
} | |||
@Override | |||
public boolean isUpdatableComment(IssueChangeDto comment) { | |||
Boolean flag = updatableCommentByKey.get(comment.getKey()); | |||
return flag != null && flag; | |||
} | |||
} | |||
} |
@@ -1,237 +0,0 @@ | |||
/* | |||
* 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<UserDto> preloadedUsers, Set<ComponentDto> preloadedComponents) { | |||
List<FieldDiffs> changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dto.getKey()); | |||
return new ChangelogLoadingContextImpl(changes, preloadedUsers, preloadedComponents); | |||
} | |||
public Stream<Common.Changelog> formatChangelog(DbSession dbSession, ChangelogLoadingContext loadingContext) { | |||
Map<String, UserDto> usersByUuid = loadUsers(dbSession, loadingContext); | |||
Map<String, ComponentDto> filesByUuid = loadFiles(dbSession, loadingContext); | |||
FormatableChangeLog changeLogResults = new FormatableChangeLog(loadingContext.getChanges(), usersByUuid, filesByUuid); | |||
return changeLogResults.changes.stream() | |||
.map(toWsChangelog(changeLogResults)); | |||
} | |||
private Map<String, UserDto> loadUsers(DbSession dbSession, ChangelogLoadingContext loadingContext) { | |||
List<FieldDiffs> changes = loadingContext.getChanges(); | |||
if (changes.isEmpty()) { | |||
return emptyMap(); | |||
} | |||
Set<UserDto> usersByUuid = loadingContext.getPreloadedUsers(); | |||
Set<String> userUuids = changes.stream() | |||
.filter(change -> change.userUuid() != null) | |||
.map(FieldDiffs::userUuid) | |||
.collect(toSet()); | |||
if (userUuids.isEmpty()) { | |||
return emptyMap(); | |||
} | |||
Set<String> 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<String, ComponentDto> loadFiles(DbSession dbSession, ChangelogLoadingContext loadingContext) { | |||
List<FieldDiffs> changes = loadingContext.getChanges(); | |||
if (changes.isEmpty()) { | |||
return emptyMap(); | |||
} | |||
Set<String> 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<ComponentDto> preloadedComponents = loadingContext.getPreloadedComponents(); | |||
Set<String> preloadedComponentUuids = preloadedComponents.stream() | |||
.map(ComponentDto::uuid) | |||
.collect(toSet(preloadedComponents.size())); | |||
Set<String> 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<FieldDiffs> getChanges(); | |||
Set<UserDto> getPreloadedUsers(); | |||
Set<ComponentDto> getPreloadedComponents(); | |||
} | |||
@Immutable | |||
public static final class ChangelogLoadingContextImpl implements ChangelogLoadingContext { | |||
private final List<FieldDiffs> changes; | |||
private final Set<UserDto> preloadedUsers; | |||
private final Set<ComponentDto> preloadedComponents; | |||
private ChangelogLoadingContextImpl(List<FieldDiffs> changes, Set<UserDto> preloadedUsers, Set<ComponentDto> preloadedComponents) { | |||
this.changes = ImmutableList.copyOf(changes); | |||
this.preloadedUsers = ImmutableSet.copyOf(preloadedUsers); | |||
this.preloadedComponents = ImmutableSet.copyOf(preloadedComponents); | |||
} | |||
@Override | |||
public List<FieldDiffs> getChanges() { | |||
return changes; | |||
} | |||
@Override | |||
public Set<UserDto> getPreloadedUsers() { | |||
return preloadedUsers; | |||
} | |||
@Override | |||
public Set<ComponentDto> getPreloadedComponents() { | |||
return preloadedComponents; | |||
} | |||
} | |||
private Function<FieldDiffs, Common.Changelog> 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<Map.Entry<String, FieldDiffs.Diff>, 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<FieldDiffs> changes; | |||
private final Map<String, UserDto> users; | |||
private final Map<String, ComponentDto> files; | |||
private FormatableChangeLog(List<FieldDiffs> changes, Map<String, UserDto> users, Map<String, ComponentDto> 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(); | |||
} | |||
} | |||
} |
@@ -30,12 +30,14 @@ 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.server.issue.IssueChangelog; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueChangeWSSupport.Load; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Issues.ChangelogWsResponse; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.util.Collections.singleton; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_CHANGELOG; | |||
@@ -46,13 +48,13 @@ public class ChangelogAction implements IssuesWsAction { | |||
private final DbClient dbClient; | |||
private final IssueFinder issueFinder; | |||
private final UserSession userSession; | |||
private final IssueChangelog issueChangelog; | |||
private final IssueChangeWSSupport issueChangeSupport; | |||
public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, UserSession userSession, IssueChangelog issueChangelog) { | |||
public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, UserSession userSession, IssueChangeWSSupport issueChangeSupport) { | |||
this.dbClient = dbClient; | |||
this.issueFinder = issueFinder; | |||
this.userSession = userSession; | |||
this.issueChangelog = issueChangelog; | |||
this.issueChangeSupport = issueChangeSupport; | |||
} | |||
@Override | |||
@@ -86,10 +88,10 @@ public class ChangelogAction implements IssuesWsAction { | |||
return ChangelogWsResponse.newBuilder().build(); | |||
} | |||
IssueChangelog.ChangelogLoadingContext loadingContext = issueChangelog.newChangelogLoadingContext(dbSession, issue); | |||
IssueChangeWSSupport.FormattingContext formattingContext = issueChangeSupport.newFormattingContext(dbSession, singleton(issue), Load.CHANGE_LOG); | |||
ChangelogWsResponse.Builder builder = ChangelogWsResponse.newBuilder(); | |||
issueChangelog.formatChangelog(dbSession, loadingContext) | |||
issueChangeSupport.formatChangelog(issue, formattingContext) | |||
.forEach(builder::addChangelog); | |||
return builder.build(); | |||
} |
@@ -21,7 +21,7 @@ package org.sonar.server.issue.ws; | |||
import org.sonar.core.platform.Module; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueChangelog; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||
@@ -46,7 +46,7 @@ public class IssueWsModule extends Module { | |||
IssueQueryFactory.class, | |||
IssuesWs.class, | |||
AvatarResolverImpl.class, | |||
IssueChangelog.class, | |||
IssueChangeWSSupport.class, | |||
SearchResponseLoader.class, | |||
TextRangeResponseFormatter.class, | |||
UserResponseFormatter.class, |
@@ -59,8 +59,9 @@ import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.issue.AvatarResolver; | |||
import org.sonar.server.issue.AvatarResolverImpl; | |||
import org.sonar.server.issue.IssueChangelog; | |||
import org.sonar.server.issue.IssueChangelog.ChangelogLoadingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport.Load; | |||
import org.sonar.server.issue.TextRangeResponseFormatter; | |||
import org.sonar.server.issue.ws.UserResponseFormatter; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
@@ -101,12 +102,12 @@ public class ShowActionTest { | |||
private AvatarResolver avatarResolver = new AvatarResolverImpl(); | |||
private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider); | |||
private IssueChangelog issueChangelog = Mockito.mock(IssueChangelog.class); | |||
private IssueChangeWSSupport issueChangeSupport = Mockito.mock(IssueChangeWSSupport.class); | |||
private HotspotWsSupport hotspotWsSupport = new HotspotWsSupport(dbClient, userSessionRule, System2.INSTANCE); | |||
private UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl()); | |||
private TextRangeResponseFormatter textRangeFormatter = new TextRangeResponseFormatter(); | |||
private ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangelog); | |||
private ShowAction underTest = new ShowAction(dbClient, hotspotWsSupport, responseFormatter, textRangeFormatter, userFormatter, issueChangeSupport); | |||
private WsActionTester actionTester = new WsActionTester(underTest); | |||
@Test | |||
@@ -556,7 +557,7 @@ public class ShowActionTest { | |||
} | |||
@Test | |||
public void returns_hotspot_changelog() { | |||
public void returns_hotspot_changelog_and_comments() { | |||
ComponentDto project = dbTester.components().insertPublicProject(); | |||
userSessionRule.registerComponents(project); | |||
RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); | |||
@@ -565,13 +566,16 @@ public class ShowActionTest { | |||
.setLocations(DbIssues.Locations.newBuilder() | |||
.setTextRange(DbCommons.TextRange.newBuilder().build()) | |||
.build())); | |||
ChangelogLoadingContext changelogLoadingContext = Mockito.mock(ChangelogLoadingContext.class); | |||
FormattingContext formattingContext = Mockito.mock(FormattingContext.class); | |||
List<Common.Changelog> 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()); | |||
List<Common.Comment> comments = IntStream.range(0, 1 + new Random().nextInt(12)) | |||
.mapToObj(i -> Common.Comment.newBuilder().setKey("u" + i).build()) | |||
.collect(Collectors.toList()); | |||
when(issueChangeSupport.newFormattingContext(any(), any(), any(), anySet(), anySet())).thenReturn(formattingContext); | |||
when(issueChangeSupport.formatChangelog(any(), any())).thenReturn(changelog.stream()); | |||
when(issueChangeSupport.formatComments(any(), any(), any())).thenReturn(comments.stream()); | |||
Hotspots.ShowWsResponse response = newRequest(hotspot) | |||
.executeProtobuf(Hotspots.ShowWsResponse.class); | |||
@@ -579,10 +583,15 @@ public class ShowActionTest { | |||
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)), | |||
assertThat(response.getCommentList()) | |||
.extracting(Common.Comment::getKey) | |||
.containsExactly(comments.stream().map(Common.Comment::getKey).toArray(String[]::new)); | |||
verify(issueChangeSupport).newFormattingContext(any(DbSession.class), | |||
argThat(new IssueDtoSetArgumentMatcher(hotspot)), | |||
eq(Load.ALL), | |||
eq(Collections.emptySet()), eq(ImmutableSet.of(project, file))); | |||
verify(issueChangelog).formatChangelog(any(DbSession.class), eq(changelogLoadingContext)); | |||
verify(issueChangeSupport).formatChangelog(argThat(new IssueDtoArgumentMatcher(hotspot)), eq(formattingContext)); | |||
verify(issueChangeSupport).formatComments(argThat(new IssueDtoArgumentMatcher(hotspot)), any(Common.Comment.Builder.class), eq(formattingContext)); | |||
} | |||
public void verifyRule(Hotspots.Rule wsRule, RuleDefinitionDto dto) { | |||
@@ -636,6 +645,24 @@ public class ShowActionTest { | |||
return ruleDefinition; | |||
} | |||
private static class IssueDtoSetArgumentMatcher implements ArgumentMatcher<Set<IssueDto>> { | |||
private final IssueDto expected; | |||
private IssueDtoSetArgumentMatcher(IssueDto expected) { | |||
this.expected = expected; | |||
} | |||
@Override | |||
public boolean matches(Set<IssueDto> argument) { | |||
return argument != null && argument.size() == 1 && argument.iterator().next().getKey().equals(expected.getKey()); | |||
} | |||
@Override | |||
public String toString() { | |||
return "Set<IssueDto>[" + expected.getKey() + "]"; | |||
} | |||
} | |||
private static class IssueDtoArgumentMatcher implements ArgumentMatcher<IssueDto> { | |||
private final IssueDto expected; | |||
@@ -647,6 +674,11 @@ public class ShowActionTest { | |||
public boolean matches(IssueDto argument) { | |||
return argument != null && argument.getKey().equals(expected.getKey()); | |||
} | |||
@Override | |||
public String toString() { | |||
return "IssueDto[key=" + expected.getKey() + "]"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,606 @@ | |||
/* | |||
* 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.ImmutableSet; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Random; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.IntStream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.issue.FieldDiffs; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.ComponentTesting; | |||
import org.sonar.db.issue.IssueChangeDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.issue.IssueTesting; | |||
import org.sonar.db.organization.OrganizationTesting; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.markdown.Markdown; | |||
import org.sonar.server.issue.IssueChangeWSSupport.FormattingContext; | |||
import org.sonar.server.issue.IssueChangeWSSupport.Load; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonarqube.ws.Common.Changelog; | |||
import org.sonarqube.ws.Common.Comment; | |||
import static java.util.Collections.emptySet; | |||
import static java.util.Collections.singleton; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.sonar.api.utils.DateUtils.formatDateTime; | |||
import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT; | |||
import static org.sonar.db.issue.IssueChangeDto.TYPE_FIELD_CHANGE; | |||
@RunWith(DataProviderRunner.class) | |||
public class IssueChangeWSSupportTest { | |||
private static final Random RANDOM = new Random(); | |||
@Rule | |||
public DbTester dbTester = DbTester.create(System2.INSTANCE); | |||
@Rule | |||
public UserSessionRule userSessionRule = UserSessionRule.standalone(); | |||
private DbClient dbClient = dbTester.getDbClient(); | |||
private AvatarResolverImpl avatarResolver = new AvatarResolverImpl(); | |||
private IssueChangeWSSupport underTest = new IssueChangeWSSupport(dbClient, avatarResolver, userSessionRule); | |||
@Test | |||
public void newFormattingContext_with_Load_CHANGE_LOG_loads_only_changelog() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
List<IssueChangeDto> comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newComment(issue).setKey("comment_" + i)) | |||
.collect(Collectors.toList()); | |||
List<IssueChangeDto> fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newFieldChange(issue) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_" + i, null, null) | |||
.toEncodedString())) | |||
.collect(Collectors.toList()); | |||
insertInRandomOrder(comments, fieldChanges); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.CHANGE_LOG); | |||
assertThat(formattingContext.getChanges(issue)) | |||
.extracting(FieldDiffs::toEncodedString) | |||
.containsExactlyInAnyOrder(fieldChanges.stream().map(t -> t.toFieldDiffs().toEncodedString()).toArray(String[]::new)); | |||
assertThat(formattingContext.getComments(issue)).isEmpty(); | |||
} | |||
@Test | |||
public void newFormattingContext_with_Load_COMMENTS_loads_only_comments() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
List<IssueChangeDto> comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newComment(issue).setKey("comment_" + i)) | |||
.collect(Collectors.toList()); | |||
List<IssueChangeDto> fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newFieldChange(issue) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_" + i, null, null) | |||
.toEncodedString())) | |||
.collect(Collectors.toList()); | |||
insertInRandomOrder(comments, fieldChanges); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.COMMENTS); | |||
assertThat(formattingContext.getComments(issue)) | |||
.extracting(IssueChangeDto::getKey) | |||
.containsExactlyInAnyOrder(comments.stream().map(IssueChangeDto::getKey).toArray(String[]::new)); | |||
assertThat(formattingContext.getChanges(issue)).isEmpty(); | |||
} | |||
@Test | |||
public void newFormattingContext_with_Load_ALL_loads_changelog_and_comments() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
List<IssueChangeDto> comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newComment(issue).setKey("comment_" + i)) | |||
.collect(Collectors.toList()); | |||
List<IssueChangeDto> fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newFieldChange(issue) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_" + i, null, null) | |||
.toEncodedString())) | |||
.collect(Collectors.toList()); | |||
insertInRandomOrder(comments, fieldChanges); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL); | |||
assertThat(formattingContext.getComments(issue)) | |||
.extracting(IssueChangeDto::getKey) | |||
.containsExactlyInAnyOrder(comments.stream().map(IssueChangeDto::getKey).toArray(String[]::new)); | |||
assertThat(formattingContext.getComments(issue)) | |||
.extracting(IssueChangeDto::getKey) | |||
.containsExactlyInAnyOrder(comments.stream().map(IssueChangeDto::getKey).toArray(String[]::new)); | |||
} | |||
@Test | |||
public void newFormattingContext_with_load_CHANGE_LOG_loads_users_of_field_changes() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(); | |||
UserDto user3 = dbTester.users().insertUser(); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto fieldChangeUser1 = newFieldChange(issue) | |||
.setUserUuid(user1.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_1", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeUser2a = newFieldChange(issue) | |||
.setUserUuid(user2.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_2a", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeUser2b = newFieldChange(issue) | |||
.setUserUuid(user2.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_2b", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeNonExistingUser = newFieldChange(issue) | |||
.setUserUuid(uuid) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_unknown", null, null).toEncodedString()); | |||
insertInRandomOrder(Arrays.asList(fieldChangeUser1, fieldChangeUser2a, fieldChangeUser2b, fieldChangeNonExistingUser)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.CHANGE_LOG); | |||
assertThat(formattingContext.getUserByUuid(user1.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user2.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user3.getUuid())).isEmpty(); | |||
assertThat(formattingContext.getUserByUuid(uuid)).isEmpty(); | |||
} | |||
@Test | |||
public void newFormattingContext_with_load_COMMENTS_loads_users_of_comments() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(); | |||
UserDto user3 = dbTester.users().insertUser(); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); | |||
IssueChangeDto issueChangeUser2a = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeUser2b = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeNonExistingUser = newComment(issue).setUserUuid(uuid); | |||
insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2a, issueChangeUser2b, issueChangeNonExistingUser)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.COMMENTS); | |||
assertThat(formattingContext.getUserByUuid(user1.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user2.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user3.getUuid())).isEmpty(); | |||
assertThat(formattingContext.getUserByUuid(uuid)).isEmpty(); | |||
} | |||
@Test | |||
public void newFormattingContext_with_load_ALL_loads_users_of_fieldChanges_and_comments() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(); | |||
UserDto user3 = dbTester.users().insertUser(); | |||
UserDto user4 = dbTester.users().insertUser(); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); | |||
IssueChangeDto issueChangeUser2a = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeUser2b = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeNonExistingUser = newComment(issue).setUserUuid(uuid); | |||
IssueChangeDto fieldChangeUser1 = newFieldChange(issue) | |||
.setUserUuid(user1.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_1", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeUser4 = newFieldChange(issue) | |||
.setUserUuid(user4.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_4", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeNonExistingUser = newFieldChange(issue) | |||
.setUserUuid(uuid) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_unknown", null, null).toEncodedString()); | |||
insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2a, issueChangeUser2b, issueChangeNonExistingUser), | |||
Arrays.asList(fieldChangeUser1, fieldChangeUser4, fieldChangeNonExistingUser)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL); | |||
assertThat(formattingContext.getUserByUuid(user1.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user2.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user3.getUuid())).isEmpty(); | |||
assertThat(formattingContext.getUserByUuid(user4.getUuid())).isNotEmpty(); | |||
assertThat(formattingContext.getUserByUuid(uuid)).isEmpty(); | |||
} | |||
@Test | |||
public void newFormattingContext_with_load_CHANGE_LOG_loads_files_of_file_fieldChanges() { | |||
newFormattingContext_loads_files_of_file_fieldChanges(Load.CHANGE_LOG); | |||
} | |||
@Test | |||
public void newFormattingContext_with_load_ALL_loads_files_of_file_fieldChanges() { | |||
newFormattingContext_loads_files_of_file_fieldChanges(Load.ALL); | |||
} | |||
private void newFormattingContext_loads_files_of_file_fieldChanges(Load load) { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
ComponentDto file1 = insertFile(); | |||
ComponentDto file2 = insertFile(); | |||
ComponentDto file3 = insertFile(); | |||
ComponentDto file4 = insertFile(); | |||
ComponentDto file5 = insertFile(); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto fileChangeFile1 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", file1.uuid(), null).toEncodedString()); | |||
IssueChangeDto fileChangeFile2 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", file2.uuid(), null).toEncodedString()); | |||
IssueChangeDto fileChangeFile3 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", null, file3.uuid()).toEncodedString()); | |||
IssueChangeDto fileChangeFile4 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", file4.uuid(), file4.uuid()).toEncodedString()); | |||
IssueChangeDto fileChangeNotExistingFile = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", uuid, uuid).toEncodedString()); | |||
insertInRandomOrder(Arrays.asList(fileChangeFile1, fileChangeFile2, fileChangeFile3, fileChangeFile4, fileChangeNotExistingFile)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); | |||
assertThat(formattingContext.getFileByUuid(file1.uuid())).isNotEmpty(); | |||
assertThat(formattingContext.getFileByUuid(file2.uuid())).isNotEmpty(); | |||
assertThat(formattingContext.getFileByUuid(file3.uuid())).isNotEmpty(); | |||
assertThat(formattingContext.getFileByUuid(file4.uuid())).isNotEmpty(); | |||
assertThat(formattingContext.getFileByUuid(file5.uuid())).isEmpty(); | |||
assertThat(formattingContext.getFileByUuid(uuid)).isEmpty(); | |||
} | |||
@Test | |||
public void newFormattingContext_does_not_load_preloaded_users_from_DB() { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(); | |||
UserDto user3 = dbTester.users().insertUser(); | |||
UserDto user4 = dbTester.users().insertUser(); | |||
IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); | |||
IssueChangeDto issueChangeUser2 = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto fieldChangeUser1 = newFieldChange(issue) | |||
.setUserUuid(user1.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_1", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeUser3 = newFieldChange(issue) | |||
.setUserUuid(user3.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_3", null, null).toEncodedString()); | |||
IssueChangeDto fieldChangeUser4 = newFieldChange(issue) | |||
.setUserUuid(user4.getUuid()) | |||
.setChangeData(new FieldDiffs().setDiff("f_change_user_4", null, null).toEncodedString()); | |||
insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2), | |||
Arrays.asList(fieldChangeUser1, fieldChangeUser3, fieldChangeUser4)); | |||
user1.setEmail("post_insert_changed" + user1.getUuid()); | |||
user2.setEmail("post_insert_changed" + user2.getUuid()); | |||
user3.setEmail("post_insert_changed" + user3.getUuid()); | |||
user4.setEmail("post_insert_changed" + user4.getUuid()); | |||
// no users are preloaded | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL, | |||
emptySet(), emptySet()); | |||
assertThat(formattingContext.getUserByUuid(user1.getUuid()).get().getEmail()).isNotEqualTo(user1.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user2.getUuid()).get().getEmail()).isNotEqualTo(user2.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user3.getUuid()).get().getEmail()).isNotEqualTo(user3.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user4.getUuid()).get().getEmail()).isNotEqualTo(user4.getEmail()); | |||
// some users are preloaded | |||
formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL, | |||
ImmutableSet.of(user1, user4), emptySet()); | |||
assertThat(formattingContext.getUserByUuid(user1.getUuid()).get().getEmail()).isEqualTo(user1.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user2.getUuid()).get().getEmail()).isNotEqualTo(user2.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user3.getUuid()).get().getEmail()).isNotEqualTo(user3.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user4.getUuid()).get().getEmail()).isEqualTo(user4.getEmail()); | |||
// all users are preloaded | |||
formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), Load.ALL, | |||
ImmutableSet.of(user1, user2, user3, user4), emptySet()); | |||
assertThat(formattingContext.getUserByUuid(user1.getUuid()).get().getEmail()).isEqualTo(user1.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user2.getUuid()).get().getEmail()).isEqualTo(user2.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user3.getUuid()).get().getEmail()).isEqualTo(user3.getEmail()); | |||
assertThat(formattingContext.getUserByUuid(user4.getUuid()).get().getEmail()).isEqualTo(user4.getEmail()); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrChangelog") | |||
public void newFormattingContext_does_not_load_preloaded_files_from_DB(Load load) { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
ComponentDto file1 = insertFile(); | |||
ComponentDto file2 = insertFile(); | |||
ComponentDto file3 = insertFile(); | |||
ComponentDto file4 = insertFile(); | |||
IssueChangeDto fileChangeFile1 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", file1.uuid(), null).toEncodedString()); | |||
IssueChangeDto fileChangeFile2 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", file2.uuid(), null).toEncodedString()); | |||
IssueChangeDto fileChangeFile3 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", null, file3.uuid()).toEncodedString()); | |||
IssueChangeDto fileChangeFile4 = newFieldChange(issue) | |||
.setChangeData(new FieldDiffs().setDiff("file", file4.uuid(), file4.uuid()).toEncodedString()); | |||
insertInRandomOrder(Arrays.asList(fileChangeFile1, fileChangeFile2, fileChangeFile3, fileChangeFile4)); | |||
file1.setName("preloaded_name" + file1.uuid()); | |||
file2.setName("preloaded_name" + file2.uuid()); | |||
file3.setName("preloaded_name" + file3.uuid()); | |||
file4.setName("preloaded_name" + file4.uuid()); | |||
// no files are preloaded | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load, | |||
emptySet(), emptySet()); | |||
assertThat(formattingContext.getFileByUuid(file1.uuid()).get().name()).isNotEqualTo(file1.name()); | |||
assertThat(formattingContext.getFileByUuid(file2.uuid()).get().name()).isNotEqualTo(file2.name()); | |||
assertThat(formattingContext.getFileByUuid(file3.uuid()).get().name()).isNotEqualTo(file3.name()); | |||
assertThat(formattingContext.getFileByUuid(file4.uuid()).get().name()).isNotEqualTo(file4.name()); | |||
// some files are preloaded | |||
formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load, | |||
emptySet(), ImmutableSet.of(file2, file3)); | |||
assertThat(formattingContext.getFileByUuid(file1.uuid()).get().name()).isNotEqualTo(file1.name()); | |||
assertThat(formattingContext.getFileByUuid(file2.uuid()).get().name()).isEqualTo(file2.name()); | |||
assertThat(formattingContext.getFileByUuid(file3.uuid()).get().name()).isEqualTo(file3.name()); | |||
assertThat(formattingContext.getFileByUuid(file4.uuid()).get().name()).isNotEqualTo(file4.name()); | |||
// all files are preloaded | |||
formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load, | |||
emptySet(), ImmutableSet.of(file1, file2, file3, file4)); | |||
assertThat(formattingContext.getFileByUuid(file1.uuid()).get().name()).isEqualTo(file1.name()); | |||
assertThat(formattingContext.getFileByUuid(file2.uuid()).get().name()).isEqualTo(file2.name()); | |||
assertThat(formattingContext.getFileByUuid(file3.uuid()).get().name()).isEqualTo(file3.name()); | |||
assertThat(formattingContext.getFileByUuid(file4.uuid()).get().name()).isEqualTo(file4.name()); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrComments") | |||
public void newFormattingContext_comments_without_userUuid_or_with_unknown_userUuid_are_not_updatable(Load load) { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto issueChangeUser1 = newComment(issue); | |||
IssueChangeDto issueChangeUserUnknown = newComment(issue).setUserUuid(uuid); | |||
insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUserUnknown)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser1)).isFalse(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUserUnknown)).isFalse(); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrComments") | |||
public void newFormattingContext_comments_with_userUuid_are_not_updatable_if_no_user_is_logged_in(Load load) { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto issueChangeUser1 = newComment(issue).setUserUuid(user1.getUuid()); | |||
IssueChangeDto issueChangeUser2 = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeUserUnknown = newComment(issue).setUserUuid(uuid); | |||
insertInRandomOrder(Arrays.asList(issueChangeUser1, issueChangeUser2, issueChangeUserUnknown)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser1)).isFalse(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser2)).isFalse(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUserUnknown)).isFalse(); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrComments") | |||
public void newFormattingContext_only_comments_of_logged_in_user_are_updatable(Load load) { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(); | |||
userSessionRule.logIn(user2); | |||
String uuid = randomAlphabetic(30); | |||
IssueChangeDto issueChangeUser1a = newComment(issue).setUserUuid(user1.getUuid()); | |||
IssueChangeDto issueChangeUser1b = newComment(issue).setUserUuid(user1.getUuid()); | |||
IssueChangeDto issueChangeUser2a = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeUser2b = newComment(issue).setUserUuid(user2.getUuid()); | |||
IssueChangeDto issueChangeUserUnknown = newComment(issue).setUserUuid(uuid); | |||
insertInRandomOrder(Arrays.asList(issueChangeUser1a, issueChangeUser1b, issueChangeUser2a, issueChangeUser2b, issueChangeUserUnknown)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser1a)).isFalse(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser1b)).isFalse(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser2a)).isTrue(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUser2b)).isTrue(); | |||
assertThat(formattingContext.isUpdatableComment(issueChangeUserUnknown)).isFalse(); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrChangelog") | |||
public void formatChangelog_returns_empty_if_context_has_no_changeLog_for_specified_IssueDto(Load load) { | |||
IssueDto issue1 = dbTester.issues().insertIssue(); | |||
IssueDto issue2 = dbTester.issues().insertIssue(); | |||
List<IssueChangeDto> comments = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newComment(issue1).setKey("comment_" + i)) | |||
.collect(Collectors.toList()); | |||
List<IssueChangeDto> fieldChanges = IntStream.range(0, 1 + RANDOM.nextInt(20)) | |||
.mapToObj(i -> newFieldChange(issue1) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_" + i, null, null) | |||
.toEncodedString())) | |||
.collect(Collectors.toList()); | |||
insertInRandomOrder(comments, fieldChanges); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), Load.CHANGE_LOG); | |||
assertThat(underTest.formatChangelog(issue2, formattingContext)).isEmpty(); | |||
assertThat(underTest.formatChangelog(issue1, formattingContext)).isNotEmpty(); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrChangelog") | |||
public void formatChangelog_returns_field_diff_details(Load load) { | |||
IssueDto issue1 = dbTester.issues().insertIssue(); | |||
int createdAt = 2_333_999; | |||
IssueChangeDto issueChangeDto = dbTester.issues().insertChange(newFieldChange(issue1) | |||
.setIssueChangeCreationDate(createdAt) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_1", "a", "b") | |||
.setDiff("f_change_2", "c", null) | |||
.setDiff("f_change_3", null, null) | |||
.setDiff("f_change_4", null, "e") | |||
.toEncodedString())); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); | |||
List<Changelog> wsChangelogList = underTest.formatChangelog(issue1, formattingContext).collect(Collectors.toList()); | |||
assertThat(wsChangelogList).hasSize(1); | |||
Changelog wsChangelog = wsChangelogList.iterator().next(); | |||
assertThat(wsChangelog.getCreationDate()).isEqualTo(formatDateTime(createdAt)); | |||
assertThat(wsChangelog.getDiffsList()).hasSize(4); | |||
assertThat(wsChangelog.getDiffsList().get(0).getKey()).isEqualTo("f_change_1"); | |||
assertThat(wsChangelog.getDiffsList().get(0).getOldValue()).isEqualTo("a"); | |||
assertThat(wsChangelog.getDiffsList().get(0).getNewValue()).isEqualTo("b"); | |||
assertThat(wsChangelog.getDiffsList().get(1).getKey()).isEqualTo("f_change_2"); | |||
assertThat(wsChangelog.getDiffsList().get(1).getOldValue()).isEqualTo("c"); | |||
assertThat(wsChangelog.getDiffsList().get(1).hasNewValue()).isFalse(); | |||
assertThat(wsChangelog.getDiffsList().get(2).getKey()).isEqualTo("f_change_3"); | |||
assertThat(wsChangelog.getDiffsList().get(2).hasOldValue()).isFalse(); | |||
assertThat(wsChangelog.getDiffsList().get(2).hasNewValue()).isFalse(); | |||
assertThat(wsChangelog.getDiffsList().get(3).getKey()).isEqualTo("f_change_4"); | |||
assertThat(wsChangelog.getDiffsList().get(3).hasOldValue()).isFalse(); | |||
assertThat(wsChangelog.getDiffsList().get(3).getNewValue()).isEqualTo("e"); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrChangelog") | |||
public void formatChangelog_returns_user_details_if_exists(Load load) { | |||
IssueDto issue1 = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(); | |||
UserDto user2 = dbTester.users().insertUser(t -> t.setActive(false)); | |||
String uuid = randomAlphabetic(22); | |||
dbTester.issues().insertChange(newFieldChange(issue1) | |||
.setUserUuid(user1.getUuid()) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_1", "a", "b") | |||
.toEncodedString())); | |||
dbTester.issues().insertChange(newFieldChange(issue1) | |||
.setUserUuid(uuid) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_2", "a", "b") | |||
.toEncodedString())); | |||
dbTester.issues().insertChange(newFieldChange(issue1) | |||
.setUserUuid(user2.getUuid()) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_3", "a", "b") | |||
.toEncodedString())); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); | |||
List<Changelog> wsChangelogList = underTest.formatChangelog(issue1, formattingContext).collect(Collectors.toList()); | |||
assertThat(wsChangelogList) | |||
.extracting(Changelog::hasUser, t -> t.getDiffsList().iterator().next().getKey()) | |||
.containsExactlyInAnyOrder( | |||
tuple(true, "f_change_1"), | |||
tuple(false, "f_change_2"), | |||
tuple(true, "f_change_3")); | |||
assertThat(wsChangelogList.stream().filter(Changelog::hasUser)) | |||
.extracting(Changelog::getUser, Changelog::getUserName, Changelog::getIsUserActive, Changelog::getAvatar) | |||
.containsExactlyInAnyOrder( | |||
tuple(user1.getLogin(), user1.getName(), true, avatarResolver.create(user1)), | |||
tuple(user2.getLogin(), user2.getName(), false, avatarResolver.create(user2))); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrChangelog") | |||
public void formatChangelog_returns_no_avatar_if_email_is_not_set(Load load) { | |||
IssueDto issue1 = dbTester.issues().insertIssue(); | |||
UserDto user1 = dbTester.users().insertUser(t -> t.setEmail(null)); | |||
dbTester.issues().insertChange(newFieldChange(issue1) | |||
.setUserUuid(user1.getUuid()) | |||
.setChangeData(new FieldDiffs() | |||
.setDiff("f_change_1", "a", "b") | |||
.toEncodedString())); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); | |||
List<Changelog> wsChangelogList = underTest.formatChangelog(issue1, formattingContext).collect(Collectors.toList()); | |||
assertThat(wsChangelogList).hasSize(1); | |||
assertThat(wsChangelogList.iterator().next().hasAvatar()).isFalse(); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrComments") | |||
public void formatComments_returns_empty_if_context_has_no_comment_for_specified_IssueDto(Load load) { | |||
IssueDto issue1 = dbTester.issues().insertIssue(); | |||
IssueDto issue2 = dbTester.issues().insertIssue(); | |||
IntStream.range(0, 1 + RANDOM.nextInt(22)) | |||
.forEach(t -> dbTester.issues().insertChange(newComment(issue1))); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue1), load); | |||
assertThat(underTest.formatComments(issue2, Comment.newBuilder(), formattingContext)).isEmpty(); | |||
assertThat(underTest.formatComments(issue1, Comment.newBuilder(), formattingContext)).isNotEmpty(); | |||
} | |||
@Test | |||
@UseDataProvider("loadAllOrComments") | |||
public void formatComments_returns_comment_markdown_and_html_when_available(Load load) { | |||
IssueDto issue = dbTester.issues().insertIssue(); | |||
IssueChangeDto withText = dbTester.issues().insertChange(newComment(issue).setChangeData("* foo")); | |||
IssueChangeDto noText = dbTester.issues().insertChange(newComment(issue).setChangeData(null)); | |||
FormattingContext formattingContext = underTest.newFormattingContext(dbTester.getSession(), singleton(issue), load); | |||
List<Comment> comments = underTest.formatComments(issue, Comment.newBuilder(), formattingContext).collect(Collectors.toList()); | |||
assertThat(comments) | |||
.extracting(Comment::getKey, Comment::hasMarkdown, Comment::hasHtmlText) | |||
.containsExactlyInAnyOrder( | |||
tuple(withText.getKey(), true, true), | |||
tuple(noText.getKey(), false, false) | |||
); | |||
assertThat(comments.stream().filter(Comment::hasHtmlText)) | |||
.extracting(Comment::getMarkdown, Comment::getHtmlText) | |||
.containsOnly(tuple(withText.getChangeData(), Markdown.convertToHtml(withText.getChangeData()))); | |||
} | |||
@DataProvider | |||
public static Object[][] loadAllOrChangelog() { | |||
return new Object[][] { | |||
{Load.ALL}, | |||
{Load.CHANGE_LOG} | |||
}; | |||
} | |||
@DataProvider | |||
public static Object[][] loadAllOrComments() { | |||
return new Object[][] { | |||
{Load.ALL}, | |||
{Load.COMMENTS} | |||
}; | |||
} | |||
private ComponentDto insertFile() { | |||
return dbTester.components().insertComponent(ComponentTesting.newFileDto(ComponentTesting.newPublicProjectDto(OrganizationTesting.newOrganizationDto()))); | |||
} | |||
private static IssueChangeDto newComment(IssueDto issue) { | |||
return IssueTesting.newIssuechangeDto(issue).setChangeType(TYPE_COMMENT); | |||
} | |||
private static IssueChangeDto newFieldChange(IssueDto issue) { | |||
return IssueTesting.newIssuechangeDto(issue).setChangeType(TYPE_FIELD_CHANGE); | |||
} | |||
@SafeVarargs | |||
private final void insertInRandomOrder(Collection<IssueChangeDto>... changesLists) { | |||
List<IssueChangeDto> all = new ArrayList<>(); | |||
Arrays.stream(changesLists).forEach(all::addAll); | |||
Collections.shuffle(all); | |||
all.forEach(i -> dbTester.issues().insertChange(i)); | |||
} | |||
} |
@@ -38,7 +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.IssueChangeWSSupport; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.ws.TestRequest; | |||
@@ -71,8 +71,8 @@ public class ChangelogActionTest { | |||
private ComponentDto project; | |||
private ComponentDto file; | |||
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 IssueChangeWSSupport issueChangeSupport = new IssueChangeWSSupport(db.getDbClient(), new AvatarResolverImpl(), userSession); | |||
private ChangelogAction underTest = new ChangelogAction(db.getDbClient(), issueFinder, userSession, issueChangeSupport); | |||
private WsActionTester tester = new WsActionTester(underTest); | |||
@Before |
@@ -65,6 +65,7 @@ message ShowWsResponse { | |||
optional string updateDate = 12; | |||
optional sonarqube.ws.commons.TextRange textRange = 13; | |||
repeated sonarqube.ws.commons.Changelog changelog = 14; | |||
repeated sonarqube.ws.commons.Comment comment = 15; | |||
} | |||
message Component { |