| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.acceptance.api.change; |
| |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; |
| import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; |
| import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder; |
| import static com.google.gerrit.server.project.testing.TestLabels.value; |
| import static com.google.gerrit.testing.GerritJUnit.assertThrows; |
| import static java.util.stream.Collectors.toList; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.any; |
| import static org.mockito.Mockito.clearInvocations; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| import static org.mockito.MockitoAnnotations.initMocks; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.truth.Correspondence; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.ExtensionRegistry; |
| import com.google.gerrit.acceptance.ExtensionRegistry.Registration; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestAccount; |
| import com.google.gerrit.acceptance.config.GerritConfig; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AttentionSetUpdate; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.LabelFunction; |
| import com.google.gerrit.entities.LabelId; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.SubmitRecord; |
| import com.google.gerrit.extensions.annotations.Exports; |
| import com.google.gerrit.extensions.api.changes.DraftInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling; |
| import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput; |
| import com.google.gerrit.extensions.api.changes.ReviewResult; |
| import com.google.gerrit.extensions.api.changes.ReviewerInput; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.client.ReviewerState; |
| import com.google.gerrit.extensions.client.Side; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.ChangeMessageInfo; |
| import com.google.gerrit.extensions.common.RobotCommentInfo; |
| import com.google.gerrit.extensions.config.FactoryModule; |
| import com.google.gerrit.extensions.events.CommentAddedListener; |
| import com.google.gerrit.extensions.events.ReviewerAddedListener; |
| import com.google.gerrit.extensions.events.ReviewerDeletedListener; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.extensions.validators.CommentForValidation; |
| import com.google.gerrit.extensions.validators.CommentValidationContext; |
| import com.google.gerrit.extensions.validators.CommentValidator; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.change.EmailReviewComments; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.restapi.change.OnPostReview; |
| import com.google.gerrit.server.restapi.change.PostReview; |
| import com.google.gerrit.server.rules.SubmitRule; |
| import com.google.gerrit.server.update.CommentsRejectedException; |
| import com.google.gerrit.testing.FakeEmailSender; |
| import com.google.gerrit.testing.TestCommentHelper; |
| import com.google.inject.Inject; |
| import com.google.inject.Module; |
| import java.sql.Timestamp; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Captor; |
| |
| /** Tests for comment validation in {@link PostReview}. */ |
| public class PostReviewIT extends AbstractDaemonTest { |
| |
| @Inject private CommentValidator mockCommentValidator; |
| @Inject private TestCommentHelper testCommentHelper; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private ExtensionRegistry extensionRegistry; |
| @Inject private ProjectOperations projectOperations; |
| |
| private static final String COMMENT_TEXT = "The comment text"; |
| private static final CommentForValidation FILE_COMMENT_FOR_VALIDATION = |
| CommentForValidation.create( |
| CommentForValidation.CommentSource.HUMAN, |
| CommentForValidation.CommentType.FILE_COMMENT, |
| COMMENT_TEXT, |
| COMMENT_TEXT.length()); |
| private static final CommentForValidation INLINE_COMMENT_FOR_VALIDATION = |
| CommentForValidation.create( |
| CommentForValidation.CommentSource.HUMAN, |
| CommentForValidation.CommentType.INLINE_COMMENT, |
| COMMENT_TEXT, |
| COMMENT_TEXT.length()); |
| private static final CommentForValidation CHANGE_MESSAGE_FOR_VALIDATION = |
| CommentForValidation.create( |
| CommentForValidation.CommentSource.HUMAN, |
| CommentForValidation.CommentType.CHANGE_MESSAGE, |
| COMMENT_TEXT, |
| COMMENT_TEXT.length()); |
| |
| @Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> captor; |
| @Captor private ArgumentCaptor<CommentValidationContext> captorCtx; |
| |
| private static final Correspondence<CommentForValidation, CommentForValidation> |
| COMMENT_CORRESPONDENCE = |
| Correspondence.from( |
| (left, right) -> |
| left != null |
| && right != null |
| && left.getSource() == right.getSource() |
| && left.getType() == right.getType() |
| && left.getText().equals(right.getText()), |
| "matches (ignoring size approximation)"); |
| |
| @Override |
| public Module createModule() { |
| return new FactoryModule() { |
| @Override |
| public void configure() { |
| CommentValidator mockCommentValidator = mock(CommentValidator.class); |
| |
| // by default return no validation errors |
| when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of()); |
| |
| bind(CommentValidator.class) |
| .annotatedWith(Exports.named(mockCommentValidator.getClass())) |
| .toInstance(mockCommentValidator); |
| bind(CommentValidator.class).toInstance(mockCommentValidator); |
| } |
| }; |
| } |
| |
| @Before |
| public void resetMock() { |
| initMocks(this); |
| clearInvocations(mockCommentValidator); |
| } |
| |
| @Test |
| public void validateCommentsInInput_commentOK() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(captorCtx.capture(), captor.capture())) |
| .thenReturn(ImmutableList.of()); |
| |
| ReviewInput input = new ReviewInput().message(COMMENT_TEXT); |
| CommentInput comment = newComment(r.getChange().currentFilePaths().get(0)); |
| comment.updated = new Timestamp(0); |
| input.comments = ImmutableMap.of(comment.path, ImmutableList.of(comment)); |
| |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| assertValidatorCalledWith(CHANGE_MESSAGE_FOR_VALIDATION, FILE_COMMENT_FOR_VALIDATION); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1); |
| assertThat(captorCtx.getAllValues()).containsExactly(contextFor(r)); |
| } |
| |
| @Test |
| public void validateCommentsInInput_commentRejected() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(eq(contextFor(r)), captor.capture())) |
| .thenReturn(ImmutableList.of(FILE_COMMENT_FOR_VALIDATION.failValidation("Oh no!"))); |
| |
| ReviewInput input = new ReviewInput().message(COMMENT_TEXT); |
| CommentInput comment = newComment(r.getChange().currentFilePaths().get(0)); |
| comment.updated = new Timestamp(0); |
| input.comments = ImmutableMap.of(comment.path, ImmutableList.of(comment)); |
| |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| BadRequestException badRequestException = |
| assertThrows( |
| BadRequestException.class, |
| () -> gApi.changes().id(r.getChangeId()).current().review(input)); |
| assertValidatorCalledWith(CHANGE_MESSAGE_FOR_VALIDATION, FILE_COMMENT_FOR_VALIDATION); |
| assertThat(badRequestException.getCause()).isInstanceOf(CommentsRejectedException.class); |
| assertThat( |
| Iterables.getOnlyElement( |
| ((CommentsRejectedException) badRequestException.getCause()) |
| .getCommentValidationFailures()) |
| .getComment() |
| .getText()) |
| .isEqualTo(COMMENT_TEXT); |
| assertThat(badRequestException.getCause()).hasMessageThat().contains("Oh no!"); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| } |
| |
| @Test |
| public void validateCommentsInInput_commentCleanedUp() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| |
| // posting a comment which is empty after trim is a no-op, as the empty comment is dropped |
| // during comment cleanup |
| ReviewInput input = new ReviewInput(); |
| CommentInput comment = |
| TestCommentHelper.populate( |
| new CommentInput(), r.getChange().currentFilePaths().get(0), " "); |
| comment.updated = new Timestamp(0); |
| input.comments = ImmutableMap.of(comment.path, ImmutableList.of(comment)); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| } |
| |
| @Test |
| public void validateDrafts_draftOK() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(eq(contextFor(r)), captor.capture())) |
| .thenReturn(ImmutableList.of()); |
| |
| DraftInput draft = |
| testCommentHelper.newDraft( |
| r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT); |
| gApi.changes().id(r.getChangeId()).revision(r.getCommit().getName()).createDraft(draft); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| |
| ReviewInput input = new ReviewInput().message(COMMENT_TEXT); |
| input.drafts = DraftHandling.PUBLISH; |
| |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| // Comment validators called twice: first when the draft was created, and second when it was |
| // published. |
| assertValidatorCalledWith( |
| /* numInvocations= */ 2, CHANGE_MESSAGE_FOR_VALIDATION, INLINE_COMMENT_FOR_VALIDATION); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1); |
| } |
| |
| @Test |
| public void validateDrafts_draftRejected() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(eq(contextFor(r)), captor.capture())) |
| .thenReturn(ImmutableList.of(INLINE_COMMENT_FOR_VALIDATION.failValidation("Oh no!"))); |
| |
| DraftInput draft = |
| testCommentHelper.newDraft( |
| r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT); |
| BadRequestException badRequestException = |
| assertThrows( |
| BadRequestException.class, |
| () -> testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draft)); |
| assertValidatorCalledWith(INLINE_COMMENT_FOR_VALIDATION); |
| assertThat(badRequestException.getCause()).isInstanceOf(CommentsRejectedException.class); |
| assertThat( |
| Iterables.getOnlyElement( |
| ((CommentsRejectedException) badRequestException.getCause()) |
| .getCommentValidationFailures()) |
| .getComment() |
| .getText()) |
| .isEqualTo(draft.message); |
| assertThat(badRequestException.getCause()).hasMessageThat().contains("Oh no!"); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| } |
| |
| @Test |
| public void validateDrafts_inlineVsFileComments_allOK() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| DraftInput draftInline = |
| testCommentHelper.newDraft( |
| r.getChange().currentFilePaths().get(0), Side.REVISION, 1, COMMENT_TEXT); |
| testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draftInline); |
| DraftInput draftFile = testCommentHelper.newDraft(COMMENT_TEXT); |
| testCommentHelper.addDraft(r.getChangeId(), r.getCommit().getName(), draftFile); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).isEmpty(); |
| |
| when(mockCommentValidator.validateComments(any(), captor.capture())) |
| .thenReturn(ImmutableList.of()); |
| |
| ReviewInput input = new ReviewInput().message(COMMENT_TEXT); |
| input.drafts = DraftHandling.PUBLISH; |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(2); |
| |
| assertValidatorCalledWith( |
| CHANGE_MESSAGE_FOR_VALIDATION, |
| CommentForValidation.create( |
| CommentForValidation.CommentSource.HUMAN, |
| CommentForValidation.CommentType.INLINE_COMMENT, |
| draftInline.message, |
| draftInline.message.length()), |
| CommentForValidation.create( |
| CommentForValidation.CommentSource.HUMAN, |
| CommentForValidation.CommentType.FILE_COMMENT, |
| draftFile.message, |
| draftFile.message.length())); |
| } |
| |
| @Test |
| public void validateCommentsInChangeMessage_messageOK() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(eq(contextFor(r)), captor.capture())) |
| .thenReturn(ImmutableList.of()); |
| |
| ReviewInput input = new ReviewInput().message(COMMENT_TEXT); |
| int numMessages = gApi.changes().id(r.getChangeId()).get().messages.size(); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| assertValidatorCalledWith(CHANGE_MESSAGE_FOR_VALIDATION); |
| assertThat(gApi.changes().id(r.getChangeId()).get().messages).hasSize(numMessages + 1); |
| ChangeMessageInfo message = |
| Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages); |
| assertThat(message.message).contains(COMMENT_TEXT); |
| } |
| |
| @Test |
| public void validateCommentsInChangeMessage_messageRejected() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(eq(contextFor(r)), captor.capture())) |
| .thenReturn(ImmutableList.of(CHANGE_MESSAGE_FOR_VALIDATION.failValidation("Oh no!"))); |
| |
| ReviewInput input = new ReviewInput().message(COMMENT_TEXT); |
| assertThat(gApi.changes().id(r.getChangeId()).get().messages) |
| .hasSize(1); // From the initial commit. |
| BadRequestException badRequestException = |
| assertThrows( |
| BadRequestException.class, |
| () -> gApi.changes().id(r.getChangeId()).current().review(input)); |
| assertValidatorCalledWith(CHANGE_MESSAGE_FOR_VALIDATION); |
| assertThat(badRequestException.getCause()).isInstanceOf(CommentsRejectedException.class); |
| assertThat( |
| Iterables.getOnlyElement( |
| ((CommentsRejectedException) badRequestException.getCause()) |
| .getCommentValidationFailures()) |
| .getComment() |
| .getText()) |
| .isEqualTo(COMMENT_TEXT); |
| assertThat(badRequestException.getCause()).hasMessageThat().contains("Oh no!"); |
| assertThat(gApi.changes().id(r.getChangeId()).get().messages) |
| .hasSize(1); // Unchanged from before. |
| ChangeMessageInfo message = |
| Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages); |
| assertThat(message.message).doesNotContain(COMMENT_TEXT); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.maxComments", value = "7") |
| public void restrictNumberOfComments() throws Exception { |
| when(mockCommentValidator.validateComments(any(), any())).thenReturn(ImmutableList.of()); |
| |
| PushOneCommit.Result r = createChange(); |
| String filePath = r.getChange().currentFilePaths().get(0); |
| CommentInput commentInput = new CommentInput(); |
| commentInput.line = 1; |
| commentInput.message = "foo"; |
| commentInput.path = filePath; |
| RobotCommentInput robotCommentInput = |
| TestCommentHelper.createRobotCommentInputWithMandatoryFields(filePath); |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.comments = ImmutableMap.of(filePath, ImmutableList.of(commentInput)); |
| reviewInput.robotComments = ImmutableMap.of(filePath, ImmutableList.of(robotCommentInput)); |
| gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| // Counting change messages plus comments we now have 4. |
| |
| // reviewInput still has both a user and a robot comment (and deduplication is false). We also |
| // create a draft, and there's the change message, so that in total there would be 8 comments. |
| // The limit is set to 7, so this verifies that all new comments are considered. |
| DraftInput draftInline = testCommentHelper.newDraft(filePath, Side.REVISION, 1, "a draft"); |
| testCommentHelper.addDraft(r.getChangeId(), r.getPatchSetId().getId(), draftInline); |
| reviewInput.drafts = DraftHandling.PUBLISH; |
| reviewInput.omitDuplicateComments = false; |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> gApi.changes().id(r.getChangeId()).current().review(reviewInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .contains("Exceeding maximum number of comments: 4 (existing) + 4 (new) > 7"); |
| |
| assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1); |
| assertThat(getRobotComments(r.getChangeId())).hasSize(1); |
| } |
| |
| @Test |
| @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "7k") |
| public void validateCumulativeCommentSize() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| when(mockCommentValidator.validateComments(eq(contextFor(r)), any())) |
| .thenReturn(ImmutableList.of()); |
| |
| // Use large sizes because autogenerated messages already have O(100) bytes. |
| String commentText2000Bytes = new String(new char[2000]).replace("\0", "x"); |
| String filePath = r.getChange().currentFilePaths().get(0); |
| ReviewInput reviewInput = new ReviewInput().message(commentText2000Bytes); |
| CommentInput commentInput = new CommentInput(); |
| commentInput.line = 1; |
| commentInput.message = commentText2000Bytes; |
| commentInput.path = filePath; |
| reviewInput.comments = ImmutableMap.of(filePath, ImmutableList.of(commentInput)); |
| |
| // Use up ~4000 bytes. |
| gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| |
| // Hit the limit when trying that again. |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> gApi.changes().id(r.getChangeId()).current().review(reviewInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .contains("Exceeding maximum cumulative size of comments"); |
| } |
| |
| @Test |
| public void ccToReviewer() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // User adds themselves and changes state |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput input = new ReviewInput().reviewer(user.id().toString(), ReviewerState.CC, false); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| Map<ReviewerState, Collection<AccountInfo>> reviewers = |
| gApi.changes().id(r.getChangeId()).get().reviewers; |
| assertThat(reviewers).hasSize(1); |
| AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.CC)); |
| assertThat(reviewer._accountId).isEqualTo(user.id().get()); |
| |
| // CC -> Reviewer |
| ReviewInput input2 = new ReviewInput().reviewer(user.id().toString()); |
| gApi.changes().id(r.getChangeId()).current().review(input2); |
| |
| Map<ReviewerState, Collection<AccountInfo>> reviewers2 = |
| gApi.changes().id(r.getChangeId()).get().reviewers; |
| assertThat(reviewers2).hasSize(1); |
| AccountInfo reviewer2 = Iterables.getOnlyElement(reviewers2.get(ReviewerState.REVIEWER)); |
| assertThat(reviewer2._accountId).isEqualTo(user.id().get()); |
| } |
| |
| @Test |
| public void reviewerToCc() throws Exception { |
| // Admin owns the change |
| PushOneCommit.Result r = createChange(); |
| // User adds themselves and changes state |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput input = new ReviewInput().reviewer(user.id().toString()); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| Map<ReviewerState, Collection<AccountInfo>> reviewers = |
| gApi.changes().id(r.getChangeId()).get().reviewers; |
| assertThat(reviewers).hasSize(1); |
| AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.REVIEWER)); |
| assertThat(reviewer._accountId).isEqualTo(user.id().get()); |
| |
| // Reviewer -> CC |
| ReviewInput input2 = new ReviewInput().reviewer(user.id().toString(), ReviewerState.CC, false); |
| gApi.changes().id(r.getChangeId()).current().review(input2); |
| |
| Map<ReviewerState, Collection<AccountInfo>> reviewers2 = |
| gApi.changes().id(r.getChangeId()).get().reviewers; |
| assertThat(reviewers2).hasSize(1); |
| AccountInfo reviewer2 = Iterables.getOnlyElement(reviewers2.get(ReviewerState.CC)); |
| assertThat(reviewer2._accountId).isEqualTo(user.id().get()); |
| } |
| |
| @Test |
| public void votingMakesCallerReviewer() throws Exception { |
| // Admin owns the change |
| PushOneCommit.Result r = createChange(); |
| // User adds themselves and changes state |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| Map<ReviewerState, Collection<AccountInfo>> reviewers = |
| gApi.changes().id(r.getChangeId()).get().reviewers; |
| assertThat(reviewers).hasSize(1); |
| AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.REVIEWER)); |
| assertThat(reviewer._accountId).isEqualTo(user.id().get()); |
| } |
| |
| @Test |
| public void commentingMakesUserCC() throws Exception { |
| // Admin owns the change |
| PushOneCommit.Result r = createChange(); |
| // User adds themselves and changes state |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput input = new ReviewInput().message("Foo bar!"); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| Map<ReviewerState, Collection<AccountInfo>> reviewers = |
| gApi.changes().id(r.getChangeId()).get().reviewers; |
| assertThat(reviewers).hasSize(1); |
| AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.CC)); |
| assertThat(reviewer._accountId).isEqualTo(user.id().get()); |
| } |
| |
| @Test |
| public void extendChangeMessageFromPlugin() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| String testMessage = "hello from plugin"; |
| TestOnPostReview testOnPostReview = new TestOnPostReview(testMessage); |
| try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages; |
| assertThat(Iterables.getLast(messages).message) |
| .isEqualTo(String.format("Patch Set 1: Code-Review+1\n\n%s\n", testMessage)); |
| } |
| } |
| |
| @Test |
| public void extendChangeMessageFromMultiplePlugins() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| String testMessage1 = "hello from plugin 1"; |
| String testMessage2 = "message from plugin 2"; |
| TestOnPostReview testOnPostReview1 = new TestOnPostReview(testMessage1); |
| TestOnPostReview testOnPostReview2 = new TestOnPostReview(testMessage2); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(testOnPostReview1).add(testOnPostReview2)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages; |
| assertThat(Iterables.getLast(messages).message) |
| .isEqualTo( |
| String.format( |
| "Patch Set 1: Code-Review+1\n\n%s\n\n%s\n", testMessage1, testMessage2)); |
| } |
| } |
| |
| @Test |
| public void onPostReviewExtensionThatDoesntExtendTheChangeMessage() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null); |
| try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages; |
| assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review+1"); |
| } |
| } |
| |
| @Test |
| public void onPostReviewCallbackGetsCorrectChangeAndPatchSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| amendChange(r.getChangeId()); |
| |
| TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null); |
| try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| |
| // Vote on current patch set. |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertChangeAndPatchSet(r.getChange().getId(), 2); |
| |
| // Vote on old patch set. |
| gApi.changes().id(r.getChangeId()).revision(1).review(input); |
| testOnPostReview.assertChangeAndPatchSet(r.getChange().getId(), 1); |
| } |
| } |
| |
| @Test |
| public void onPostReviewCallbackGetsCorrectUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null); |
| try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| |
| // Vote from admin. |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertUser(admin); |
| |
| // Vote from user. |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertUser(user); |
| } |
| } |
| |
| @Test |
| public void onPostReviewApprovedIsReturnedForLabelsAndDetailedLabels() throws Exception { |
| // Create Verify label with NO_BLOCK function and allow voting on it. |
| // When the NO_BLOCK function is used for a label, the "approved" is set by the |
| // LabelsJson.setLabelScores method instead of LabelsJson.initLabels method. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder verified = |
| labelBuilder( |
| LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")) |
| .setFunction(LabelFunction.NO_BLOCK); |
| u.getConfig().upsertLabelType(verified.build()); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(LabelId.VERIFIED) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| PushOneCommit.Result r = createChange(); |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2).label(LabelId.VERIFIED, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| input = new ReviewInput(); |
| input.message = "first message"; |
| input.responseFormatOptions = ImmutableList.of(ListChangesOption.DETAILED_LABELS); |
| ReviewResult reviewResult = gApi.changes().id(r.getChangeId()).current().review(input); |
| assertThat(reviewResult.changeInfo.labels.keySet()) |
| .containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED); |
| assertThat(reviewResult.changeInfo.labels.get(LabelId.CODE_REVIEW).approved).isNotNull(); |
| assertThat(reviewResult.changeInfo.labels.get(LabelId.VERIFIED).approved).isNotNull(); |
| |
| input = new ReviewInput(); |
| input.message = "second message"; |
| input.responseFormatOptions = ImmutableList.of(ListChangesOption.LABELS); |
| reviewResult = gApi.changes().id(r.getChangeId()).current().review(input); |
| assertThat(reviewResult.changeInfo.labels).containsKey(LabelId.CODE_REVIEW); |
| assertThat(reviewResult.changeInfo.labels.get(LabelId.CODE_REVIEW).approved).isNotNull(); |
| assertThat(reviewResult.changeInfo.labels.get(LabelId.VERIFIED).approved).isNotNull(); |
| } |
| |
| @Test |
| public void onPostReviewCallbackGetsCorrectApprovals() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null); |
| try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) { |
| // Add a new vote. |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertApproval( |
| LabelId.CODE_REVIEW, /* expectedOldValue= */ 0, /* expectedNewValue= */ 1); |
| |
| // Update an existing vote. |
| input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertApproval( |
| LabelId.CODE_REVIEW, /* expectedOldValue= */ 1, /* expectedNewValue= */ 2); |
| |
| // Post without changing the vote. |
| input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertApproval( |
| LabelId.CODE_REVIEW, /* expectedOldValue= */ null, /* expectedNewValue= */ 2); |
| |
| // Delete the vote. |
| input = new ReviewInput().label(LabelId.CODE_REVIEW, 0); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| testOnPostReview.assertApproval( |
| LabelId.CODE_REVIEW, /* expectedOldValue= */ 2, /* expectedNewValue= */ 0); |
| } |
| } |
| |
| @Test |
| public void votingTheSameVoteSecondTime() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| gApi.changes().id(r.getChangeId()).addReviewer(user.email()); |
| sender.clear(); |
| |
| // Add a new vote. |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| assertThat(r.getChange().approvals().values()).hasSize(1); |
| |
| // Post without changing the vote. |
| ChangeNotes notes = notesFactory.create(project, r.getChange().getId()); |
| ObjectId metaId = notes.getMetaId(); |
| assertAttentionSet(notes.getAttentionSet(), user.id()); |
| input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| notes = notesFactory.create(project, r.getChange().getId()); |
| // Change meta ID did not change since the update is No/Op. Attention set is same. |
| assertThat(notes.getMetaId()).isEqualTo(metaId); |
| assertAttentionSet(notes.getAttentionSet(), user.id()); |
| |
| // Second vote replaced the original vote, so still only one vote. |
| assertThat(r.getChange().approvals().values()).hasSize(1); |
| List<ChangeMessageInfo> changeMessages = gApi.changes().id(r.getChangeId()).messages(); |
| |
| // Only the last change message is about Code-Review+2 |
| assertThat(Iterables.getLast(changeMessages).message).isEqualTo("Patch Set 1: Code-Review+2"); |
| changeMessages.remove(changeMessages.size() - 1); |
| assertThat(Iterables.getLast(changeMessages).message) |
| .isNotEqualTo("Patch Set 1: Code-Review+2"); |
| |
| // Only one email is about Code-Review +2 was sent. |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .contains("Patch Set 1: Code-Review+2"); |
| } |
| |
| @Test |
| public void votingTheSameVoteSecondTimeExtendsOnPostReviewWithOldNullValue() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add a new vote. |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| assertThat(r.getChange().approvals().values()).hasSize(1); |
| |
| TestOnPostReview testOnPostReview = new TestOnPostReview(/* message= */ null); |
| try (Registration registration = extensionRegistry.newRegistration().add(testOnPostReview)) { |
| // Post without changing the vote. |
| input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| testOnPostReview.assertApproval( |
| LabelId.CODE_REVIEW, /* expectedOldValue= */ null, /* expectedNewValue= */ 2); |
| } |
| } |
| |
| @Test |
| public void votingTheSameVoteSecondTimeDoesNotFireOnCommentAdded() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add a new vote. |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| assertThat(r.getChange().approvals().values()).hasSize(1); |
| |
| TestListener testListener = new TestListener(); |
| try (Registration registration = extensionRegistry.newRegistration().add(testListener)) { |
| // Post without changing the vote. |
| input = new ReviewInput().label(LabelId.CODE_REVIEW, 2); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| // Event not fired. |
| assertThat(testListener.lastCommentAddedEvent).isNull(); |
| } |
| } |
| |
| @Test |
| public void currentRevisionNumberIsSetOnReturnedChangeInfo() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeInfo changeInfo = |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.dislike()).changeInfo; |
| assertThat(changeInfo.currentRevisionNumber).isEqualTo(1); |
| amendChange(r.getChangeId()); |
| changeInfo = |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend()).changeInfo; |
| assertThat(changeInfo.currentRevisionNumber).isEqualTo(2); |
| |
| // Check that the current revision number is also returned when list changes options are |
| // requested. |
| ReviewInput reviewInput = ReviewInput.approve(); |
| reviewInput.responseFormatOptions = |
| ImmutableList.copyOf(EnumSet.allOf(ListChangesOption.class)); |
| changeInfo = gApi.changes().id(r.getChangeId()).current().review(reviewInput).changeInfo; |
| assertThat(changeInfo.currentRevisionNumber).isEqualTo(2); |
| } |
| |
| @Test |
| public void submitRulesAreInvokedOnlyOnce() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestSubmitRule testSubmitRule = new TestSubmitRule(); |
| try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| } |
| |
| assertThat(testSubmitRule.count).isEqualTo(1); |
| } |
| |
| @Test |
| public void submitRulesAreInvokedOnlyOnce_allOptionsSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestSubmitRule testSubmitRule = new TestSubmitRule(); |
| try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) { |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 1); |
| input.responseFormatOptions = ImmutableList.copyOf(EnumSet.allOf(ListChangesOption.class)); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| } |
| |
| assertThat(testSubmitRule.count).isEqualTo(1); |
| } |
| |
| @Test |
| public void addingReviewers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestAccount user2 = accountCreator.user2(); |
| |
| TestReviewerAddedListener testReviewerAddedListener = new TestReviewerAddedListener(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(testReviewerAddedListener)) { |
| // add user and user2 |
| ReviewResult reviewResult = |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(ReviewInput.create().reviewer(user.email()).reviewer(user2.email())); |
| |
| assertThat( |
| reviewResult.reviewers.values().stream() |
| .filter(a -> a.reviewers != null) |
| .map(a -> Iterables.getOnlyElement(a.reviewers).name) |
| .collect(toImmutableSet())) |
| .containsExactly(user.fullName(), user2.fullName()); |
| } |
| |
| assertThat( |
| gApi.changes().id(r.getChangeId()).reviewers().stream() |
| .map(a -> a.name) |
| .collect(toImmutableSet())) |
| .containsExactly(user.fullName(), user2.fullName()); |
| |
| // Ensure only one batch email was sent for this operation |
| FakeEmailSender.Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .containsMatch( |
| Pattern.quote("Hello ") |
| + "(" |
| + Pattern.quote(String.format("%s, %s", user.fullName(), user2.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s, %s", user2.fullName(), user.fullName())) |
| + ")"); |
| assertThat(message.htmlBody()) |
| .containsMatch( |
| "(" |
| + Pattern.quote(String.format("%s and %s", user.fullName(), user2.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s and %s", user2.fullName(), user.fullName())) |
| + ")" |
| + Pattern.quote(" to <strong>review</strong> this change")); |
| |
| // Ensure that a batch event has been sent: |
| // * 1 batch event for adding user and user2 as reviewers |
| assertThat(testReviewerAddedListener.receivedEvents).hasSize(1); |
| assertThat(testReviewerAddedListener.getReviewerIds()).containsExactly(user.id(), user2.id()); |
| } |
| |
| @Test |
| public void rejectAddingReviewerIfReviewerUserIdentifierIsMissing() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add reviewer with ReviewerInput where the 'reviewer' field is not set. |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.state = ReviewerState.REVIEWER; |
| |
| ReviewInput reviewInput = ReviewInput.create(); |
| reviewInput.reviewers = ImmutableList.of(reviewerInput); |
| |
| ReviewResult reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| assertThat(reviewResult.error).isEqualTo("error adding reviewer"); |
| assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer); |
| assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error) |
| .isEqualTo("reviewer user identifier is required"); |
| |
| // Add reviewer with ReviewerInput where the 'reviewer' field is set to an empty string. |
| reviewerInput.reviewer = ""; |
| reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| assertThat(reviewResult.error).isEqualTo("error adding reviewer"); |
| assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer); |
| assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error) |
| .isEqualTo("reviewer user identifier is required"); |
| |
| // Add reviewer with ReviewerInput where the 'reviewer' field is set to an empty string after |
| // trimming it. |
| reviewerInput.reviewer = " "; |
| reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput); |
| assertThat(reviewResult.error).isEqualTo("error adding reviewer"); |
| assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer); |
| assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error) |
| .isEqualTo("reviewer user identifier is required"); |
| } |
| |
| @Test |
| public void deletingReviewers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestAccount user2 = accountCreator.user2(); |
| |
| // add user and user2 |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(ReviewInput.create().reviewer(user.email()).reviewer(user2.email())); |
| |
| sender.clear(); |
| |
| TestReviewerDeletedListener testReviewerDeletedListener = new TestReviewerDeletedListener(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(testReviewerDeletedListener)) { |
| // remove user and user2 |
| ReviewResult reviewResult = |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true) |
| .reviewer(user2.email(), ReviewerState.REMOVED, /* confirmed= */ true)); |
| |
| assertThat( |
| reviewResult.reviewers.values().stream() |
| .map(a -> a.removed.name) |
| .collect(toImmutableSet())) |
| .containsExactly(user.fullName(), user2.fullName()); |
| } |
| |
| assertThat(gApi.changes().id(r.getChangeId()).reviewers()).isEmpty(); |
| |
| // Ensure only one batch email was sent for this operation |
| FakeEmailSender.Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .containsMatch( |
| Pattern.quote("removed ") |
| + "(" |
| + Pattern.quote(String.format("%s, %s", user.fullName(), user2.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s, %s", user2.fullName(), user.fullName())) |
| + ")"); |
| assertThat(message.htmlBody()) |
| .containsMatch( |
| Pattern.quote("removed ") |
| + "(" |
| + Pattern.quote(String.format("%s and %s", user.fullName(), user2.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s and %s", user2.fullName(), user.fullName())) |
| + ")"); |
| |
| // Ensure that events have been sent: |
| // * 2 events for removing user and user2 as reviewers (one event per removed reviewer, batch |
| // event not available for reviewer removal) |
| assertThat(testReviewerDeletedListener.receivedEvents).hasSize(2); |
| assertThat(testReviewerDeletedListener.getReviewerIds()).containsExactly(user.id(), user2.id()); |
| } |
| |
| @Test |
| public void addingAndDeletingReviewers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| TestAccount user2 = accountCreator.user2(); |
| TestAccount user3 = accountCreator.create("user3", "user3@email.com", "user3", "user3"); |
| TestAccount user4 = accountCreator.create("user4", "user4@email.com", "user4", "user4"); |
| |
| // add user and user2 |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(ReviewInput.create().reviewer(user.email()).reviewer(user2.email())); |
| |
| sender.clear(); |
| |
| TestReviewerAddedListener testReviewerAddedListener = new TestReviewerAddedListener(); |
| TestReviewerDeletedListener testReviewerDeletedListener = new TestReviewerDeletedListener(); |
| try (Registration registration = |
| extensionRegistry |
| .newRegistration() |
| .add(testReviewerAddedListener) |
| .add(testReviewerDeletedListener)) { |
| // remove user and user2 while adding user3 and user4 |
| ReviewResult reviewResult = |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true) |
| .reviewer(user2.email(), ReviewerState.REMOVED, /* confirmed= */ true) |
| .reviewer(user3.email()) |
| .reviewer(user4.email())); |
| |
| assertThat( |
| reviewResult.reviewers.values().stream() |
| .filter(a -> a.removed != null) |
| .map(a -> a.removed.name) |
| .collect(toImmutableSet())) |
| .containsExactly(user.fullName(), user2.fullName()); |
| assertThat( |
| reviewResult.reviewers.values().stream() |
| .filter(a -> a.reviewers != null) |
| .map(a -> Iterables.getOnlyElement(a.reviewers).name) |
| .collect(toImmutableSet())) |
| .containsExactly(user3.fullName(), user4.fullName()); |
| } |
| |
| assertThat( |
| gApi.changes().id(r.getChangeId()).reviewers().stream() |
| .map(a -> a.name) |
| .collect(toImmutableSet())) |
| .containsExactly(user3.fullName(), user4.fullName()); |
| |
| // Ensure only one batch email was sent for this operation |
| FakeEmailSender.Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .containsMatch( |
| Pattern.quote("Hello ") |
| + "(" |
| + Pattern.quote(String.format("%s, %s", user3.fullName(), user4.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s, %s", user4.fullName(), user3.fullName())) |
| + ")"); |
| assertThat(message.htmlBody()) |
| .containsMatch( |
| "(" |
| + Pattern.quote(String.format("%s and %s", user3.fullName(), user4.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s and %s", user4.fullName(), user3.fullName())) |
| + ")" |
| + Pattern.quote(" to <strong>review</strong> this change")); |
| |
| assertThat(message.body()) |
| .containsMatch( |
| Pattern.quote("removed ") |
| + "(" |
| + Pattern.quote(String.format("%s, %s", user.fullName(), user2.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s, %s", user2.fullName(), user.fullName())) |
| + ")"); |
| assertThat(message.htmlBody()) |
| .containsMatch( |
| Pattern.quote("removed ") |
| + "(" |
| + Pattern.quote(String.format("%s and %s", user.fullName(), user2.fullName())) |
| + "|" |
| + Pattern.quote(String.format("%s and %s", user2.fullName(), user.fullName())) |
| + ")"); |
| |
| // Ensure that events have been sent: |
| // * 1 batch event for adding user3 and user4 as reviewers |
| // * 2 events for removing user and user2 as reviewers (one event per removed reviewer, batch |
| // event not available for reviewer removal) |
| assertThat(testReviewerAddedListener.receivedEvents).hasSize(1); |
| assertThat(testReviewerAddedListener.getReviewerIds()).containsExactly(user3.id(), user4.id()); |
| assertThat(testReviewerDeletedListener.receivedEvents).hasSize(2); |
| assertThat(testReviewerDeletedListener.getReviewerIds()).containsExactly(user.id(), user2.id()); |
| } |
| |
| @Test |
| public void deletingNonExistingReviewerFails() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| ResourceNotFoundException resourceNotFoundException = |
| assertThrows( |
| ResourceNotFoundException.class, |
| () -> |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true))); |
| assertThat(resourceNotFoundException) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "Reviewer %s doesn't exist in the change, hence can't delete it", user.fullName())); |
| } |
| |
| @Test |
| public void addingAndDeletingSameReviewerFails() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| ResourceNotFoundException resourceNotFoundException = |
| assertThrows( |
| ResourceNotFoundException.class, |
| () -> |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email()) |
| .reviewer(user.email(), ReviewerState.REMOVED, true))); |
| assertThat(resourceNotFoundException) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "Reviewer %s doesn't exist in the change," + " hence can't delete it", |
| user.fullName())); |
| } |
| |
| @Test |
| public void votesInChangeMessageAreSorted() throws Exception { |
| // Create Verify label and allow voting on it. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder verified = |
| labelBuilder( |
| LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| u.getConfig().upsertLabelType(verified.build()); |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(LabelId.VERIFIED) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| |
| ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 2).label(LabelId.VERIFIED, 1); |
| gApi.changes().id(r.getChangeId()).current().review(input); |
| |
| Collection<ChangeMessageInfo> messages = gApi.changes().id(r.getChangeId()).get().messages; |
| assertThat(Iterables.getLast(messages).message) |
| .isEqualTo(String.format("Patch Set 1: Code-Review+2 Verified+1")); |
| } |
| |
| private static class TestListener implements CommentAddedListener { |
| public CommentAddedListener.Event lastCommentAddedEvent; |
| |
| @Override |
| public void onCommentAdded(Event event) { |
| lastCommentAddedEvent = event; |
| } |
| } |
| |
| private List<RobotCommentInfo> getRobotComments(String changeId) throws RestApiException { |
| return gApi.changes().id(changeId).robotComments().values().stream() |
| .flatMap(Collection::stream) |
| .collect(toList()); |
| } |
| |
| private static CommentInput newComment(String path) { |
| return TestCommentHelper.populate(new CommentInput(), path, PostReviewIT.COMMENT_TEXT); |
| } |
| |
| private static CommentValidationContext contextFor(PushOneCommit.Result result) { |
| return CommentValidationContext.create( |
| result.getChange().getId().get(), |
| result.getChange().project().get(), |
| result.getChange().change().getDest().branch()); |
| } |
| |
| private void assertValidatorCalledWith(CommentForValidation... commentsForValidation) { |
| assertValidatorCalledWith(/* numInvocations= */ 1, commentsForValidation); |
| } |
| |
| private void assertValidatorCalledWith( |
| int numInvocations, CommentForValidation... commentsForValidation) { |
| assertThat(captor.getAllValues()).hasSize(numInvocations); |
| assertThat(captor.getValue()) |
| .comparingElementsUsing(COMMENT_CORRESPONDENCE) |
| .containsExactly(commentsForValidation); |
| } |
| |
| private static class TestOnPostReview implements OnPostReview { |
| private final Optional<String> message; |
| |
| private Change.Id changeId; |
| private PatchSet.Id patchSetId; |
| private Account.Id accountId; |
| private Map<String, Short> oldApprovals; |
| private Map<String, Short> approvals; |
| |
| TestOnPostReview(@Nullable String message) { |
| this.message = Optional.ofNullable(message); |
| } |
| |
| @Override |
| public Optional<String> getChangeMessageAddOn( |
| Instant when, |
| IdentifiedUser user, |
| ChangeNotes changeNotes, |
| PatchSet patchSet, |
| Map<String, Short> oldApprovals, |
| Map<String, Short> approvals) { |
| this.changeId = changeNotes.getChangeId(); |
| this.patchSetId = patchSet.id(); |
| this.accountId = user.getAccountId(); |
| this.oldApprovals = oldApprovals; |
| this.approvals = approvals; |
| return message; |
| } |
| |
| public void assertChangeAndPatchSet(Change.Id expectedChangeId, int expectedPatchSetNum) { |
| assertThat(changeId).isEqualTo(expectedChangeId); |
| assertThat(patchSetId.get()).isEqualTo(expectedPatchSetNum); |
| } |
| |
| public void assertUser(TestAccount expectedUser) { |
| assertThat(accountId).isEqualTo(expectedUser.id()); |
| } |
| |
| public void assertApproval( |
| String labelName, @Nullable Integer expectedOldValue, int expectedNewValue) { |
| assertThat(oldApprovals) |
| .containsExactly( |
| labelName, expectedOldValue != null ? expectedOldValue.shortValue() : null); |
| assertThat(approvals).containsExactly(labelName, (short) expectedNewValue); |
| } |
| } |
| |
| private static class TestSubmitRule implements SubmitRule { |
| int count; |
| |
| @Override |
| public Optional<SubmitRecord> evaluate(ChangeData changeData) { |
| if (!isAsyncCallForSendingReviewCommentsEmail()) { |
| count++; |
| } |
| return Optional.empty(); |
| } |
| |
| private boolean isAsyncCallForSendingReviewCommentsEmail() { |
| return Arrays.stream(Thread.currentThread().getStackTrace()) |
| .map(StackTraceElement::getClassName) |
| .anyMatch(className -> EmailReviewComments.class.getName().equals(className)); |
| } |
| } |
| |
| private static class TestReviewerAddedListener implements ReviewerAddedListener { |
| List<ReviewerAddedListener.Event> receivedEvents = new ArrayList<>(); |
| |
| @Override |
| public void onReviewersAdded(ReviewerAddedListener.Event event) { |
| receivedEvents.add(event); |
| } |
| |
| public ImmutableSet<Account.Id> getReviewerIds() { |
| return receivedEvents.stream() |
| .flatMap(e -> e.getReviewers().stream()) |
| .map(accountInfo -> Account.id(accountInfo._accountId)) |
| .collect(toImmutableSet()); |
| } |
| } |
| |
| private static class TestReviewerDeletedListener implements ReviewerDeletedListener { |
| List<ReviewerDeletedListener.Event> receivedEvents = new ArrayList<>(); |
| |
| @Override |
| public void onReviewerDeleted(ReviewerDeletedListener.Event event) { |
| receivedEvents.add(event); |
| } |
| |
| public ImmutableSet<Account.Id> getReviewerIds() { |
| return receivedEvents.stream() |
| .map(ReviewerDeletedListener.Event::getReviewer) |
| .map(accountInfo -> Account.id(accountInfo._accountId)) |
| .collect(toImmutableSet()); |
| } |
| } |
| |
| private static void assertAttentionSet( |
| ImmutableSet<AttentionSetUpdate> attentionSet, Account.Id... accounts) { |
| assertThat(attentionSet.stream().map(AttentionSetUpdate::account).collect(Collectors.toList())) |
| .containsExactlyElementsIn(accounts); |
| } |
| } |