| // Copyright (C) 2020 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.rest.change; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; |
| import static com.google.gerrit.extensions.restapi.testing.AttentionSetUpdateSubject.assertThat; |
| 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 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.NoHttpd; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestAccount; |
| import com.google.gerrit.acceptance.UseClockStep; |
| import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes; |
| import com.google.gerrit.acceptance.config.GerritConfig; |
| import com.google.gerrit.acceptance.testsuite.account.AccountOperations; |
| import com.google.gerrit.acceptance.testsuite.change.ChangeOperations; |
| 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.AccountGroup; |
| import com.google.gerrit.entities.AttentionSetUpdate; |
| import com.google.gerrit.entities.AttentionSetUpdate.Operation; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.LabelId; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.Patch; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.PatchSetApproval; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.api.changes.AttentionSetInput; |
| import com.google.gerrit.extensions.api.changes.CustomKeyedValuesInput; |
| import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; |
| import com.google.gerrit.extensions.api.changes.DeleteVoteInput; |
| import com.google.gerrit.extensions.api.changes.HashtagsInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.api.changes.ReviewerInput; |
| import com.google.gerrit.extensions.api.groups.GroupInput; |
| import com.google.gerrit.extensions.client.GeneralPreferencesInfo; |
| import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy; |
| import com.google.gerrit.extensions.client.ReviewerState; |
| import com.google.gerrit.extensions.client.Side; |
| import com.google.gerrit.extensions.common.AttentionSetInfo; |
| import com.google.gerrit.extensions.common.GroupInfo; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.server.account.ServiceUserClassifier; |
| import com.google.gerrit.server.change.ReaddOwnerUtil; |
| import com.google.gerrit.server.project.testing.TestLabels; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| import com.google.gerrit.server.restapi.change.GetAttentionSet; |
| import com.google.gerrit.server.util.AccountTemplateUtil; |
| import com.google.gerrit.server.util.time.TimeUtil; |
| import com.google.gerrit.testing.FakeEmailSender.Message; |
| import com.google.gerrit.testing.TestCommentHelper; |
| import com.google.gerrit.truth.NullAwareCorrespondence; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.LongSupplier; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| @NoHttpd |
| @UseClockStep(clockStepUnit = TimeUnit.MINUTES) |
| @VerifyNoPiiInChangeNotes(true) |
| public class AttentionSetIT extends AbstractDaemonTest { |
| |
| @Inject private ChangeOperations changeOperations; |
| @Inject private AccountOperations accountOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| |
| @Inject private TestCommentHelper testCommentHelper; |
| @Inject private Provider<InternalChangeQuery> changeQueryProvider; |
| @Inject private ProjectOperations projectOperations; |
| @Inject private GetAttentionSet getAttentionSet; |
| @Inject private ReaddOwnerUtil readdOwnerUtil; |
| |
| /** Simulates a fake clock. Uses second granularity. */ |
| private static class FakeClock implements LongSupplier { |
| Instant now = Instant.now(); |
| |
| @Override |
| public long getAsLong() { |
| return TimeUnit.SECONDS.toMillis(now.getEpochSecond()); |
| } |
| |
| Instant now() { |
| return Instant.ofEpochSecond(now.getEpochSecond()); |
| } |
| |
| void advance(Duration duration) { |
| now = now.plus(duration); |
| } |
| } |
| |
| private FakeClock fakeClock = new FakeClock(); |
| |
| @Before |
| public void setUp() { |
| TimeUtil.setCurrentMillisSupplier(fakeClock); |
| } |
| |
| @Test |
| public void emptyAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void addUser_updateReason() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| int accountId = |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "first"))._accountId; |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| AttentionSetUpdate expectedAttentionSetUpdate = |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "first"); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| |
| // Check that email was sent |
| String emailBody = Iterables.getOnlyElement(sender.getMessages()).body(); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "%s requires the attention of %s to this change.\n The reason is: first.", |
| user.fullName(), admin.fullName())); |
| |
| // Update the reason |
| sender.clear(); |
| accountId = |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "second"))._accountId; |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| expectedAttentionSetUpdate = |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "second"); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| |
| // Check that email was sent |
| emailBody = Iterables.getOnlyElement(sender.getMessages()).body(); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "%s requires the attention of %s to this change.\n The reason is: second.", |
| user.fullName(), admin.fullName())); |
| } |
| |
| @Test |
| public void addUser_addWithSameReasonIsIgnored() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| int accountId = |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason"))._accountId; |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| AttentionSetUpdate expectedAttentionSetUpdate = |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "reason"); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| |
| // Check that email was sent |
| String emailBody = Iterables.getOnlyElement(sender.getMessages()).body(); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "%s requires the attention of %s to this change.\n The reason is: reason.", |
| user.fullName(), admin.fullName())); |
| |
| // Second add with the same reason is ignored. |
| sender.clear(); |
| accountId = |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason"))._accountId; |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| |
| // Check that no email was sent |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void addUserWithTemplateReason() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| String manualReason = "Added by " + AccountTemplateUtil.getAccountTemplate(user.id()); |
| int accountId = |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), manualReason))._accountId; |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| AttentionSetUpdate expectedAttentionSetUpdate = |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, manualReason); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| AttentionSetInfo attentionSetInfo = |
| Iterables.getOnlyElement(change(r).get().attentionSet.values()); |
| assertThat(attentionSetInfo.reason).isEqualTo(manualReason); |
| assertThat(attentionSetInfo.reasonAccount).isEqualTo(getAccountInfo(user.id())); |
| assertThat(attentionSetInfo.account).isEqualTo(getAccountInfo(admin.id())); |
| |
| AttentionSetInfo getAttentionSetInfo = |
| Iterables.getOnlyElement( |
| getAttentionSet.apply(parseChangeResource(r.getChangeId())).value()); |
| assertThat(getAttentionSetInfo.reason).isEqualTo(manualReason); |
| assertThat(getAttentionSetInfo.reasonAccount).isEqualTo(getAccountInfo(user.id())); |
| assertThat(getAttentionSetInfo.account).isEqualTo(getAccountInfo(admin.id())); |
| } |
| |
| @Test |
| public void addUserWithTemplateReasonMultipleAccounts() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| String manualReason = |
| String.format( |
| "Added by %s with user %s", |
| AccountTemplateUtil.getAccountTemplate(user.id()), |
| AccountTemplateUtil.getAccountTemplate(admin.id())); |
| int accountId = |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), manualReason))._accountId; |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| AttentionSetUpdate expectedAttentionSetUpdate = |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, manualReason); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| AttentionSetInfo attentionSetInfo = |
| Iterables.getOnlyElement(change(r).get().attentionSet.values()); |
| assertThat(attentionSetInfo.reason).isEqualTo(manualReason); |
| assertThat(attentionSetInfo.reasonAccount).isNull(); |
| assertThat(attentionSetInfo.account).isEqualTo(getAccountInfo(admin.id())); |
| |
| AttentionSetInfo getAttentionSetInfo = |
| Iterables.getOnlyElement( |
| getAttentionSet.apply(parseChangeResource(r.getChangeId())).value()); |
| assertThat(getAttentionSetInfo.reason).isEqualTo(manualReason); |
| assertThat(getAttentionSetInfo.reasonAccount).isNull(); |
| assertThat(getAttentionSetInfo.account).isEqualTo(getAccountInfo(admin.id())); |
| } |
| |
| @Test |
| public void reviewWithManuallyAddedUserAndTemplateReason() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| String manualReason = "Review by " + AccountTemplateUtil.getAccountTemplate(user.id()); |
| ReviewInput reviewInput = |
| ReviewInput.create().addUserToAttentionSet(user.email(), manualReason); |
| |
| change(r).current().review(reviewInput); |
| AttentionSetInfo attentionSetInfo = change(r).get().attentionSet.get(user.id().get()); |
| assertThat(attentionSetInfo.reason).isEqualTo(manualReason); |
| assertThat(attentionSetInfo.reasonAccount).isEqualTo(getAccountInfo(user.id())); |
| assertThat(attentionSetInfo.account).isEqualTo(getAccountInfo(user.id())); |
| } |
| |
| @Test |
| public void addMultipleUsers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| Instant timestamp1 = fakeClock.now(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| fakeClock.advance(Duration.ofSeconds(42)); |
| Instant timestamp2 = fakeClock.now(); |
| int accountId2 = |
| change(r) |
| .addToAttentionSet(new AttentionSetInput(admin.id().toString(), "manual update")) |
| ._accountId; |
| assertThat(accountId2).isEqualTo(admin.id().get()); |
| |
| AttentionSetUpdate expectedAttentionSetUpdate1 = |
| AttentionSetUpdate.createFromRead( |
| timestamp1, user.id(), AttentionSetUpdate.Operation.ADD, "Reviewer was added"); |
| AttentionSetUpdate expectedAttentionSetUpdate2 = |
| AttentionSetUpdate.createFromRead( |
| timestamp2, admin.id(), AttentionSetUpdate.Operation.ADD, "manual update"); |
| assertThat(r.getChange().attentionSet()) |
| .containsExactly(expectedAttentionSetUpdate1, expectedAttentionSetUpdate2); |
| } |
| |
| @Test |
| public void removeUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| sender.clear(); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| fakeClock.advance(Duration.ofSeconds(42)); |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed")); |
| AttentionSetUpdate expectedAttentionSetUpdate = |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed"); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| |
| // The removal also shows up in AttentionSetInfo. |
| AttentionSetInfo attentionSetInfo = |
| Iterables.getOnlyElement(change(r).get().removedFromAttentionSet.values()); |
| assertThat(attentionSetInfo.reason).isEqualTo("removed"); |
| assertThat(attentionSetInfo.account).isEqualTo(getAccountInfo(user.id())); |
| |
| // Second removal is ignored. |
| fakeClock.advance(Duration.ofSeconds(42)); |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed again")); |
| assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate); |
| |
| // Only one email since the second remove was ignored. |
| String emailBody = Iterables.getOnlyElement(sender.getMessages()).body(); |
| assertThat(emailBody) |
| .contains( |
| user.fullName() |
| + " removed themselves from the attention set of this change.\n" |
| + " The reason is: removed."); |
| } |
| |
| @Test |
| public void removeUserWithInvalidUserInput() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| change(r) |
| .attention(user.id().toString()) |
| .remove(new AttentionSetInput("invalid user", "reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "invalid user doesn't exist or is not active on the change as an owner, " |
| + "uploader, reviewer, or cc so they can't be added to the attention set"); |
| |
| exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| change(r) |
| .attention(user.id().toString()) |
| .remove(new AttentionSetInput(admin.email(), "reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "The field \"user\" must be empty, or must match the user specified in the URL."); |
| } |
| |
| @Test |
| public void abandonRemovesUsers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "admin")); |
| |
| change(r).abandon(); |
| |
| AttentionSetUpdate userUpdate = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(userUpdate).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(userUpdate).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(userUpdate).hasReasonThat().isEqualTo("Change was abandoned"); |
| |
| AttentionSetUpdate adminUpdate = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(adminUpdate).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(adminUpdate).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(adminUpdate).hasReasonThat().isEqualTo("Change was abandoned"); |
| } |
| |
| @Test |
| public void workInProgressRemovesUsers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| change(r).setWorkInProgress(); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was marked work in progress"); |
| } |
| |
| @Test |
| public void submitRemovesUsersForAllSubmittedChanges() throws Exception { |
| PushOneCommit.Result r1 = createChange("refs/heads/master", "file1", "content"); |
| |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r1).current().review(ReviewInput.approve().reviewer(user.email())); |
| PushOneCommit.Result r2 = createChange("refs/heads/master", "file2", "content"); |
| change(r2).current().review(ReviewInput.approve().reviewer(user.email())); |
| |
| change(r2).current().submit(); |
| |
| // Attention set updates that relate to the admin (the person who replied) are filtered out. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r1, user)); |
| |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted"); |
| |
| // Attention set updates that relate to the admin (the person who replied) are filtered out. |
| attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r2, user)); |
| |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted"); |
| } |
| |
| @Test |
| public void robotSubmitsRemovesUsers() throws Exception { |
| PushOneCommit.Result r = createChange("refs/heads/master", "file1", "content"); |
| |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", |
| "robot2@example.com", |
| "Ro Bot", |
| "Ro", |
| ServiceUserClassifier.SERVICE_USERS, |
| "Administrators"); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).current().review(ReviewInput.approve()); |
| change(r).current().submit(); |
| |
| // Attention set updates that relate to the admin (the person who replied) are filtered out. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted"); |
| } |
| |
| @Test |
| public void addedReviewersAreAddedToAttentionSetOnMergedChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).current().review(ReviewInput.approve()); |
| change(r).current().submit(); |
| |
| change(r).addReviewer(user.email()); |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| } |
| |
| @Test |
| public void reviewersAddedAndRemovedFromAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| change(r).addReviewer(user.id().toString()); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| |
| change(r).reviewer(user.email()).remove(); |
| |
| attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer/Cc was removed"); |
| } |
| |
| @Test |
| public void removedCcRemovedFromAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add cc |
| ReviewerInput input = new ReviewerInput(); |
| input.reviewer = user.email(); |
| input.state = ReviewerState.CC; |
| change(r).addReviewer(input); |
| |
| // Add them to the attention set |
| AttentionSetInput attentionSetInput = new AttentionSetInput(); |
| attentionSetInput.user = user.email(); |
| attentionSetInput.reason = "reason"; |
| change(r).addToAttentionSet(attentionSetInput); |
| |
| // Remove them from cc |
| change(r).reviewer(user.email()).remove(); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer/Cc was removed"); |
| } |
| |
| @Test |
| public void reviewersAddedAndRemovedByEmailFromAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| change(r).addReviewer(user.email()); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| |
| change(r).reviewer(user.email()).remove(); |
| |
| attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer/Cc was removed"); |
| } |
| |
| @Test |
| public void reviewersInWorkProgressNotAddedToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| change(r).addReviewer(user.email()); |
| |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void addingReviewerWhileMarkingWorkInProgressDoesntAddToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput reviewInput = ReviewInput.create().setWorkInProgress(true); |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.state = ReviewerState.REVIEWER; |
| reviewerInput.reviewer = user.email(); |
| reviewInput.reviewers = ImmutableList.of(reviewerInput); |
| |
| change(r).current().review(reviewInput); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void reviewersAddedAsReviewersAgainAreNotAddedToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| change(r).addReviewer(user.id().toString()); |
| change(r) |
| .attention(user.id().toString()) |
| .remove(new AttentionSetInput("removed and not re-added when re-adding as reviewer")); |
| |
| change(r).addReviewer(user.id().toString()); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("removed and not re-added when re-adding as reviewer"); |
| } |
| |
| @Test |
| public void ccsAreIgnored() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.state = ReviewerState.CC; |
| reviewerInput.reviewer = user.email(); |
| |
| change(r).addReviewer(reviewerInput); |
| |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void ccsConsideredSameAsRemovedForExistingReviewers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addReviewer(user.email()); |
| |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.state = ReviewerState.CC; |
| reviewerInput.reviewer = user.email(); |
| change(r).addReviewer(reviewerInput); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer/Cc was removed"); |
| } |
| |
| @Test |
| public void robotReadyForReviewAddsAllReviewersToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| change(r).addReviewer(user.email()); |
| |
| TestAccount robot = |
| accountCreator.create( |
| "robot1", |
| "robot1@example.com", |
| "Ro Bot", |
| "Ro", |
| ServiceUserClassifier.SERVICE_USERS, |
| "Administrators"); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).setReadyForReview(); |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was marked ready for review"); |
| } |
| |
| @Test |
| public void readyForReviewAddsAllReviewersToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| change(r).addReviewer(user.email()); |
| |
| change(r).setReadyForReview(); |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was marked ready for review"); |
| } |
| |
| @Test |
| public void readyForReviewHasNoEffectOnReadyChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r) |
| .current() |
| .review(ReviewInput.create().reviewer(user.email()).blockAutomaticAttentionSetRules()); |
| |
| change(r).current().review(ReviewInput.create().setWorkInProgress(false)); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| change(r).current().review(ReviewInput.create().setReady(true)); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void workInProgressHasNoEffectOnWorkInProgressChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email()) |
| .setWorkInProgress(true) |
| .addUserToAttentionSet(user.email(), /* reason= */ "reason")); |
| |
| change(r).current().review(ReviewInput.create().setWorkInProgress(true)); |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| |
| change(r).current().review(ReviewInput.create().setReady(false)); |
| attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void rebaseDoesNotAddToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| change(r).addReviewer(user.email()); |
| |
| // create an unrelated change so that we can rebase |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result unrelated = createChange(); |
| gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(unrelated.getChangeId()).current().submit(); |
| |
| gApi.changes().id(r.getChangeId()).rebase(); |
| |
| // rebase has no impact on the attention set |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void readyForReviewWhileRemovingReviewerRemovesThemToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| change(r).addReviewer(user.email()); |
| |
| ReviewInput reviewInput = ReviewInput.create().setReady(true); |
| ReviewerInput reviewerInput = new ReviewerInput(); |
| reviewerInput.state = ReviewerState.CC; |
| reviewerInput.reviewer = user.email(); |
| reviewInput.reviewers = ImmutableList.of(reviewerInput); |
| change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason")); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer/Cc was removed"); |
| } |
| |
| @Test |
| public void readyForReviewWhileAddingReviewerAddsThemToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| |
| ReviewInput reviewInput = ReviewInput.create().setReady(true).reviewer(user.email()); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| } |
| |
| @Test |
| public void reviewersAreNotAddedForNoReasonBecauseOfAHashtagUpdate() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implicitly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed")); |
| |
| HashtagsInput hashtagsInput = new HashtagsInput(); |
| hashtagsInput.add = ImmutableSet.of("tag"); |
| change(r).setHashtags(hashtagsInput); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("removed"); |
| } |
| |
| @Test |
| public void reviewersAreNotAddedForNoReasonBecauseOfACustomKeyedValuesUpdate() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implicitly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed")); |
| |
| CustomKeyedValuesInput customKeyedValuesInput = new CustomKeyedValuesInput(); |
| customKeyedValuesInput.add = ImmutableMap.of("key1", "value1"); |
| change(r).setCustomKeyedValues(customKeyedValuesInput); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("removed"); |
| } |
| |
| @Test |
| public void reviewAddsManuallyAddedUserToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = ReviewInput.create().addUserToAttentionSet(user.email(), "reason"); |
| |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| |
| // No emails for adding to attention set were sent. |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void reviewRemovesManuallyRemovedUserFromAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| requestScopeOperations.setApiUser(user.id()); |
| sender.clear(); |
| |
| ReviewInput reviewInput = |
| ReviewInput.create().removeUserFromAttentionSet(user.email(), "reason"); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| |
| // No emails for removing from attention set were sent. |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void reviewWithManualAdditionToAttentionSetFailsWithoutReason() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| ReviewInput reviewInput = ReviewInput.create().addUserToAttentionSet(user.email(), ""); |
| |
| BadRequestException exception = |
| assertThrows(BadRequestException.class, () -> change(r).current().review(reviewInput)); |
| |
| assertThat(exception.getMessage()).isEqualTo("missing field: reason"); |
| } |
| |
| @Test |
| public void reviewWithManualAdditionToAttentionSetFailsWithoutUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput reviewInput = ReviewInput.create().addUserToAttentionSet("", "reason"); |
| |
| BadRequestException exception = |
| assertThrows(BadRequestException.class, () -> change(r).current().review(reviewInput)); |
| |
| assertThat(exception.getMessage()).isEqualTo("missing field: user"); |
| } |
| |
| @Test |
| public void reviewAddReviewerWhileRemovingFromAttentionSetJustRemovesUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| ReviewInput reviewInput = |
| ReviewInput.create() |
| .reviewer(user.email()) |
| .removeUserFromAttentionSet(user.email(), "reason"); |
| |
| change(r).current().review(reviewInput); |
| |
| // Attention set updates that relate to the admin (the person who replied) are filtered out. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void cantAddAndRemoveSameUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput reviewInput = |
| ReviewInput.create() |
| .removeUserFromAttentionSet(user.email(), "reason") |
| .addUserToAttentionSet(user.username(), "reason"); |
| |
| BadRequestException exception = |
| assertThrows(BadRequestException.class, () -> change(r).current().review(reviewInput)); |
| |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "user1 can not be added/removed twice, and can not be added and removed at the same" |
| + " time"); |
| } |
| |
| @Test |
| public void cantRemoveSameUserTwice() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput reviewInput = |
| ReviewInput.create() |
| .removeUserFromAttentionSet(user.email(), "reason1") |
| .removeUserFromAttentionSet(user.username(), "reason2"); |
| |
| BadRequestException exception = |
| assertThrows(BadRequestException.class, () -> change(r).current().review(reviewInput)); |
| |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "user1 can not be added/removed twice, and can not be added and removed at the same" |
| + " time"); |
| } |
| |
| @Test |
| public void reviewDoesNotAddReviewerWithoutAutomaticRules() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = ReviewInput.recommend().blockAutomaticAttentionSetRules(); |
| |
| change(r).current().review(reviewInput); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void reviewDoesNotAddReviewer() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = ReviewInput.recommend(); |
| |
| change(r).current().review(reviewInput); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void cantAddSameUserTwice() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ReviewInput reviewInput = |
| ReviewInput.create() |
| .addUserToAttentionSet(user.email(), "reason1") |
| .addUserToAttentionSet(user.username(), "reason2"); |
| |
| BadRequestException exception = |
| assertThrows(BadRequestException.class, () -> change(r).current().review(reviewInput)); |
| |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "user1 can not be added/removed twice, and can not be added and removed at the same" |
| + " time"); |
| } |
| |
| @Test |
| public void reviewRemoveFromAttentionSetWhileMarkingReadyForReviewJustRemovesUser() |
| throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| change(r).addReviewer(user.email()); |
| change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason")); |
| |
| ReviewInput reviewInput = |
| ReviewInput.create().setReady(true).removeUserFromAttentionSet(user.email(), "reason"); |
| |
| change(r).current().review(reviewInput); |
| |
| // Attention set updates that relate to the admin (the person who replied) are filtered out. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void reviewAddToAttentionSetWhileMarkingWorkInProgressJustAddsUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addReviewer(user.email()); |
| |
| ReviewInput reviewInput = |
| ReviewInput.create().setWorkInProgress(true).addUserToAttentionSet(user.email(), "reason"); |
| |
| change(r).attention(user.email()).remove(new AttentionSetInput("removal")); |
| change(r).current().review(reviewInput); |
| |
| // Attention set updates that relate to the admin (the person who replied) are filtered out. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void reviewRemovesUserFromAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason")); |
| |
| ReviewInput reviewInput = new ReviewInput(); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("removed on reply"); |
| } |
| |
| @Test |
| public void reviewAddUserToAttentionSetWhileReplyingJustAddsUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| ReviewInput reviewInput = ReviewInput.create().addUserToAttentionSet(admin.email(), "reason"); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void reviewWhileAddingThemselvesAsReviewerStillRemovesThem() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| |
| // add the user to the attention set. |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email(), ReviewerState.CC, true) |
| .addUserToAttentionSet(user.email(), "reason")); |
| |
| // add the user as reviewer but still be removed on reply. |
| ReviewInput reviewInput = ReviewInput.create().reviewer(user.email()); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("removed on reply"); |
| } |
| |
| @Test |
| public void reviewWhileAddingThemselvesAsReviewerDoesNotAddThem() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput reviewInput = ReviewInput.create().reviewer(user.email()); |
| change(r).current().review(reviewInput); |
| |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void repliesAddsOwner_voteAdded() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).current().review(ReviewInput.dislike()); |
| |
| // The change owner has been added to the attention set |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void repliesAddsOwner_voteCopied() throws Exception { |
| // Define a label with a copy condition that copies all votes. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder fooReview = |
| labelBuilder( |
| "Foo-Review", |
| value(1, "Looks good to me, but someone else must approve"), |
| value(0, "No score"), |
| value(-1, "I would prefer this is not submitted as is")) |
| .setCopyCondition("is:ANY"); |
| u.getConfig().upsertLabelType(fooReview.build()); |
| u.save(); |
| } |
| |
| // Allow voting on the label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("Foo-Review") |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Create a change with 2 patch sets. |
| PushOneCommit.Result r = createChange(); |
| PatchSet patchSet1 = r.getChange().currentPatchSet(); |
| r = amendChange(r.getChangeId()); |
| r.assertOkStatus(); |
| |
| // Apply a negative vote on the first patch set which is copied since the label has a copy |
| // condition that copies all votes |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1)); |
| |
| // The change owner has been added to the attention set |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void repliesDoesntAddOwner_voteNotCopied_userNotRemovedReasonUpdated() throws Exception { |
| // Define a label without any copy condition. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder fooReview = |
| labelBuilder( |
| "Foo-Review", |
| value(1, "Looks good to me, but someone else must approve"), |
| value(0, "No score"), |
| value(-1, "I would prefer this is not submitted as is")); |
| u.getConfig().upsertLabelType(fooReview.build()); |
| u.save(); |
| } |
| |
| // Allow voting on the label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("Foo-Review") |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Create a change with 2 patch sets. |
| PushOneCommit.Result r = createChange(); |
| PatchSet patchSet1 = r.getChange().currentPatchSet(); |
| r = amendChange(r.getChangeId()); |
| r.assertOkStatus(); |
| |
| // Add user as a reviewer so that the user is in the attention set |
| change(r).addReviewer(user.email()); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isNotEmpty(); |
| |
| // Apply a negative vote on the first patch set which is not copied since the label doesn't have |
| // a copy condition |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1)); |
| |
| // The change owner has not been added to the attention set |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| |
| // The reason for the user to be in the attention set has been updated |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("Some votes were not copied to the current patch set"); |
| } |
| |
| @Test |
| public void repliesDoesntAddOwner_voteNotCopied_userAdded() throws Exception { |
| // Define a label without any copy condition. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder fooReview = |
| labelBuilder( |
| "Foo-Review", |
| value(1, "Looks good to me, but someone else must approve"), |
| value(0, "No score"), |
| value(-1, "I would prefer this is not submitted as is")); |
| u.getConfig().upsertLabelType(fooReview.build()); |
| u.save(); |
| } |
| |
| // Allow voting on the label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("Foo-Review") |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Create a change with 2 patch sets. |
| PushOneCommit.Result r = createChange(); |
| PatchSet patchSet1 = r.getChange().currentPatchSet(); |
| r = amendChange(r.getChangeId()); |
| r.assertOkStatus(); |
| |
| // User is not in the attention set yet |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| |
| // Apply a negative vote on the first patch set which is not copied since the label doesn't have |
| // a copy condition |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1)); |
| |
| // The change owner has not been added to the attention set |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| |
| // The user has been added to the attention set because the vote was not copied |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("Some votes were not copied to the current patch set"); |
| } |
| |
| @Test |
| public void repliesAddsOwner_changeMessagePosted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.message = "A message"; |
| change(r).current().review(reviewInput); |
| |
| // The change owner has been added to the attention set |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void repliesAddsOwner_commentAdded() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = new ReviewInput(); |
| ReviewInput.CommentInput comment = new ReviewInput.CommentInput(); |
| comment.side = Side.REVISION; |
| comment.path = Patch.COMMIT_MSG; |
| comment.message = "comment"; |
| reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment)); |
| change(r).current().review(reviewInput); |
| |
| // The change owner has been added to the attention set |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void repliesAddsOwner_markedAsReady() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| |
| requestScopeOperations.setApiUser(accountCreator.admin2().id()); |
| ReviewInput reviewInput = ReviewInput.create().setReady(true); |
| change(r).current().review(reviewInput); |
| |
| // The change owner has been added to the attention set |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void noOpRepliesDontAddOwner() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // post a no-op reply (a reply that does neither add any vote, change message, comment nor |
| // changes the change to ready) |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).current().review(new ReviewInput()); |
| |
| // The change owner has not been added to the attention set |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void robotRepliesDoNotAddToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addReviewer(user.email()); |
| |
| TestAccount robot = |
| accountCreator.create( |
| "robot1", |
| "robot1@example.com", |
| "Ro Bot", |
| "Ro", |
| ServiceUserClassifier.SERVICE_USERS, |
| "Administrators"); |
| requestScopeOperations.setApiUser(robot.id()); |
| |
| ReviewInput reviewInput = new ReviewInput(); |
| change(r).current().review(reviewInput); |
| |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void repliesDoNotAddOwnerWhenChangeIsClosed() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).abandon(); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput reviewInput = new ReviewInput(); |
| change(r).current().review(reviewInput); |
| |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void repliesDoNotAddOwnerWhenChangeIsWorkInProgress() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| ReviewInput reviewInput = new ReviewInput(); |
| change(r).current().review(reviewInput); |
| |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void repliesDoNotAddOwnerWhenChangeIsBecomingWorkInProgress() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(accountCreator.admin2().id()); |
| |
| ReviewInput reviewInput = ReviewInput.create().setWorkInProgress(true); |
| change(r).current().review(reviewInput); |
| |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void repliesAddOwnerWhenChangeIsBecomingReadyForReview() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setWorkInProgress(); |
| requestScopeOperations.setApiUser(accountCreator.admin2().id()); |
| |
| ReviewInput reviewInput = ReviewInput.create().setReady(true); |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void repliesAddsOwnerAndUploader() throws Exception { |
| // Create change with admin as the owner and user as the uploader |
| PushOneCommit.Result r = createChange(); |
| r = amendChangeWithUploader(r, project, user); |
| change(r).attention(user.email()).remove(new AttentionSetInput("reason")); |
| |
| // Post a comment by another user |
| TestAccount user2 = accountCreator.user2(); |
| requestScopeOperations.setApiUser(user2.id()); |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.message = "A message"; |
| change(r).current().review(reviewInput); |
| |
| // Uploader added |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| |
| // Owner added |
| attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void noOpRepliesDontAddOwnerAndUploader() throws Exception { |
| // Create change with admin as the owner and user as the uploader |
| PushOneCommit.Result r = createChange(); |
| r = amendChangeWithUploader(r, project, user); |
| change(r).attention(user.email()).remove(new AttentionSetInput("reason")); |
| |
| // Post a no-op reply by another user (a reply that does neither add any vote, change message, |
| // comment nor changes the change to ready) |
| TestAccount user2 = accountCreator.user2(); |
| requestScopeOperations.setApiUser(user2.id()); |
| ReviewInput reviewInput = new ReviewInput(); |
| change(r).current().review(reviewInput); |
| |
| // Neither the change owner nor the uploader have been added to the attention set |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void reviewIgnoresRobotCommentsForAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| testCommentHelper.addRobotComment( |
| r.getChangeId(), |
| TestCommentHelper.createRobotCommentInputWithMandatoryFields(Patch.COMMIT_MSG)); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| change(r) |
| .current() |
| .review( |
| reviewInReplyToComment( |
| Iterables.getOnlyElement( |
| gApi.changes().id(r.getChangeId()).current().robotCommentsAsList()) |
| .id)); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void reviewAddsAllUsersInCommentThread() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).current().review(reviewWithComment()); |
| |
| TestAccount user2 = accountCreator.user2(); |
| |
| requestScopeOperations.setApiUser(user2.id()); |
| change(r) |
| .current() |
| .review( |
| reviewInReplyToComment( |
| Iterables.getOnlyElement( |
| gApi.changes().id(r.getChangeId()).current().commentsAsList()) |
| .id)); |
| |
| change(r).attention(user.email()).remove(new AttentionSetInput("removal")); |
| requestScopeOperations.setApiUser(admin.id()); |
| change(r) |
| .current() |
| .review( |
| reviewInReplyToComment( |
| gApi.changes().id(r.getChangeId()).current().commentsAsList().get(1).id)); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("Someone else replied on a comment you posted"); |
| |
| attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user2)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user2.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("Someone else replied on a comment you posted"); |
| } |
| |
| @Test |
| public void reviewAddsAllUsersInCommentThreadWhenOriginalCommentIsARobotComment() |
| throws Exception { |
| PushOneCommit.Result result = createChange(); |
| testCommentHelper.addRobotComment( |
| result.getChangeId(), |
| TestCommentHelper.createRobotCommentInputWithMandatoryFields(Patch.COMMIT_MSG)); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| // Reply to the robot comment. |
| change(result) |
| .current() |
| .review( |
| reviewInReplyToComment( |
| Iterables.getOnlyElement( |
| gApi.changes().id(result.getChangeId()).current().robotCommentsAsList()) |
| .id)); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| // Reply to the human comment. which was a reply to the robot comment. |
| change(result) |
| .current() |
| .review( |
| reviewInReplyToComment( |
| Iterables.getOnlyElement( |
| gApi.changes().id(result.getChangeId()).current().commentsAsList()) |
| .id)); |
| |
| // The user which replied to the robot comment was added to the attention set. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(result, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("Someone else replied on a comment you posted"); |
| } |
| |
| @Test |
| public void reviewAddsAllUsersInCommentThreadEvenOfDifferentChildBranch() throws Exception { |
| Account.Id changeOwner = accountOperations.newAccount().create(); |
| Change.Id changeId = changeOperations.newChange().owner(changeOwner).create(); |
| Account.Id user1 = accountOperations.newAccount().create(); |
| Account.Id user2 = accountOperations.newAccount().create(); |
| Account.Id user3 = accountOperations.newAccount().create(); |
| Account.Id user4 = accountOperations.newAccount().create(); |
| // Add users as reviewers. |
| gApi.changes().id(changeId.get()).addReviewer(user1.toString()); |
| gApi.changes().id(changeId.get()).addReviewer(user2.toString()); |
| gApi.changes().id(changeId.get()).addReviewer(user3.toString()); |
| gApi.changes().id(changeId.get()).addReviewer(user4.toString()); |
| // Add a comment thread with branches. Such threads occur if people reply in parallel without |
| // having seen/loaded the reply of another person. |
| String root = |
| changeOperations.change(changeId).currentPatchset().newComment().author(user1).create(); |
| String sibling1 = |
| changeOperations |
| .change(changeId) |
| .currentPatchset() |
| .newComment() |
| .author(user2) |
| .parentUuid(root) |
| .create(); |
| String sibling2 = |
| changeOperations |
| .change(changeId) |
| .currentPatchset() |
| .newComment() |
| .author(user3) |
| .parentUuid(root) |
| .create(); |
| changeOperations |
| .change(changeId) |
| .currentPatchset() |
| .newComment() |
| .author(user4) |
| .parentUuid(sibling2) |
| .create(); |
| // Clear the attention set. Necessary as we used Gerrit APIs above which affect the attention |
| // set. |
| AttentionSetInput clearAttention = new AttentionSetInput("clear attention set"); |
| gApi.changes().id(changeId.get()).attention(user1.toString()).remove(clearAttention); |
| gApi.changes().id(changeId.get()).attention(user2.toString()).remove(clearAttention); |
| gApi.changes().id(changeId.get()).attention(user3.toString()).remove(clearAttention); |
| gApi.changes().id(changeId.get()).attention(user4.toString()).remove(clearAttention); |
| |
| requestScopeOperations.setApiUser(changeOwner); |
| // Simulate that this reply is a child of sibling1 and thus parallel to sibling2 and its child. |
| gApi.changes().id(changeId.get()).current().review(reviewInReplyToComment(sibling1)); |
| |
| List<AttentionSetUpdate> attentionSetUpdates = getAttentionSetUpdates(changeId); |
| assertThat(attentionSetUpdates) |
| .comparingElementsUsing(hasAccount()) |
| .containsExactly(user1, user2, user3, user4); |
| } |
| |
| @Test |
| public void reviewAddsAllUsersInCommentThreadWhenPostedAsDraft() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).current().review(reviewWithComment()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| testCommentHelper.addDraft( |
| r.getChangeId(), |
| testCommentHelper.newDraft( |
| "message", |
| Iterables.getOnlyElement(gApi.changes().id(r.getChangeId()).current().commentsAsList()) |
| .id)); |
| |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.drafts = ReviewInput.DraftHandling.PUBLISH; |
| change(r).current().review(reviewInput); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet) |
| .hasReasonThat() |
| .isEqualTo("Someone else replied on a comment you posted"); |
| } |
| |
| @Test |
| public void reviewDoesNotAddUsersInACommentThreadThatAreNotActiveInTheChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| change(r).current().review(reviewWithComment()); |
| change(r).reviewer(user.id().toString()).remove(new DeleteReviewerInput()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| change(r) |
| .current() |
| .review( |
| reviewInReplyToComment( |
| Iterables.getOnlyElement( |
| gApi.changes().id(r.getChangeId()).current().commentsAsList()) |
| .id)); |
| |
| // The user was to be added, but was not added since that user is no longer a |
| // reviewer/cc/owner/uploader. |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void ownerRepliesWhileRemovingReviewerRemovesFromAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addReviewer(user.email()); |
| |
| ReviewInput reviewInput = ReviewInput.create().reviewer(user.email(), ReviewerState.CC, false); |
| change(r).current().review(reviewInput); |
| |
| // cc removed |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer/Cc was removed"); |
| } |
| |
| @Test |
| public void uploaderRepliesAddsOwner() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| r = amendChangeWithUploader(r, project, user); |
| |
| // Add reviewer and cc |
| TestAccount reviewer = accountCreator.user2(); |
| TestAccount cc = accountCreator.admin2(); |
| ReviewInput reviewInput = new ReviewInput().blockAutomaticAttentionSetRules(); |
| reviewInput = reviewInput.reviewer(reviewer.email()); |
| reviewInput.reviewer(cc.email(), ReviewerState.CC, false); |
| change(r).current().review(reviewInput); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| |
| reviewInput = new ReviewInput(); |
| reviewInput.message = "A message"; |
| change(r).current().review(reviewInput); |
| |
| // Reviewer and CC not added since the uploader didn't reply to their comments |
| assertThat(getAttentionSetUpdatesForUser(r, cc)).isEmpty(); |
| assertThat(getAttentionSetUpdatesForUser(r, reviewer)).isEmpty(); |
| |
| // Owner added |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| } |
| |
| @Test |
| public void repliesWhileAddingAsReviewerStillRemovesUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| change(r).addToAttentionSet(new AttentionSetInput(user.email(), "remove")); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = ReviewInput.recommend(); |
| change(r).current().review(reviewInput); |
| |
| // reviewer removed |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("removed on reply"); |
| } |
| |
| @Test |
| public void attentionSetUnchangedWithIgnoreAutomaticAttentionSetRules() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason")); |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(admin.email(), ReviewerState.CC, false) |
| .blockAutomaticAttentionSetRules()); |
| |
| // admin is still in the attention set, although replies remove from attention set, and removing |
| // from reviewer also should remove from attention set. |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void ownerNotAddedAsReviewerToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).current().review(ReviewInput.approve()); |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void ownerNotAddedAsReviewerToAttentionSetWithoutAutomaticRules() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).current().review(ReviewInput.approve().blockAutomaticAttentionSetRules()); |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void uploaderNotAddedAsReviewerToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| amendChangeWithUploader(r, project, user); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| change(r).current().review(ReviewInput.recommend()); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void uploaderNotAddedAsReviewerToAttentionSetWithoutAutomaticRules() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| amendChangeWithUploader(r, project, user); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| change(r).current().review(ReviewInput.recommend().blockAutomaticAttentionSetRules()); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void attentionSetStillChangesWithIgnoreAutomaticAttentionSetRulesWithInputList() |
| throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason")); |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create() |
| .removeUserFromAttentionSet(admin.email(), "removed") |
| .blockAutomaticAttentionSetRules()); |
| |
| // Admin is still removed although we block default attention set rules, since we remove |
| // the admin manually. |
| AttentionSetUpdate attentionSet = Iterables.getOnlyElement(r.getChange().attentionSet()); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("removed"); |
| } |
| |
| @Test |
| public void robotsNotAddedToAttentionSet() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot1", "robot1@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| // Make the robot active on the change. |
| change(r).addReviewer(robot.email()); |
| |
| // Throw an error when adding a robot explicitly. |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> change(r).addToAttentionSet(new AttentionSetInput(robot.email(), "reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "robot1@example.com is a robot, and robots can't be added to the attention set."); |
| |
| // Robots are not added implicitly. |
| change(r).addReviewer(robot.email()); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void robotAddingAReviewerChangeAttentionSet() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).addReviewer(user.id().toString()); |
| |
| // Bots can still change the attention set, just not when replying. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| } |
| |
| @Test |
| public void robotReviewDoesNotChangeAttentionSet() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).current().review(ReviewInput.recommend()); |
| |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void robotReviewWithNegativeLabelDoesNotAddOwnerOnWorkInProgressChanges() |
| throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).setWorkInProgress(); |
| |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).current().review(ReviewInput.dislike()); |
| |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void robotReviewWithNegativeLabelAddsOwner() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).current().review(ReviewInput.dislike()); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("A robot voted negatively on a label"); |
| } |
| |
| @Test |
| public void robotReviewWithNegativeLabelOnOutdatedPatchSetAddsOwnerIfVoteWasCopied() |
| throws Exception { |
| // Define a label with a copy condition that copies all votes. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder fooReview = |
| labelBuilder( |
| "Foo-Review", |
| value(1, "Looks good to me, but someone else must approve"), |
| value(0, "No score"), |
| value(-1, "I would prefer this is not submitted as is")) |
| .setCopyCondition("is:ANY"); |
| u.getConfig().upsertLabelType(fooReview.build()); |
| u.save(); |
| } |
| |
| // Allow voting on the label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("Foo-Review") |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Create a change with 2 patch sets. |
| PushOneCommit.Result r = createChange(); |
| PatchSet patchSet1 = r.getChange().currentPatchSet(); |
| r = amendChange(r.getChangeId()); |
| r.assertOkStatus(); |
| |
| // Create a service user and apply a negative vote on the first patch set which is copied since |
| // the label has a copy condition that copies all votes |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1)); |
| |
| // The change owner has been added to the attention set |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("A robot voted negatively on a label"); |
| } |
| |
| @Test |
| public void robotReviewWithNegativeLabelOnOutdatedPatchSetDoesntAddOwnerIfVoteWasNotCopied() |
| throws Exception { |
| // Define a label without any copy condition. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder fooReview = |
| labelBuilder( |
| "Foo-Review", |
| value(1, "Looks good to me, but someone else must approve"), |
| value(0, "No score"), |
| value(-1, "I would prefer this is not submitted as is")); |
| u.getConfig().upsertLabelType(fooReview.build()); |
| u.save(); |
| } |
| |
| // Allow voting on the label. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel("Foo-Review") |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| // Create a change with 2 patch sets. |
| PushOneCommit.Result r = createChange(); |
| PatchSet patchSet1 = r.getChange().currentPatchSet(); |
| r = amendChange(r.getChangeId()); |
| r.assertOkStatus(); |
| |
| // Create a service user and apply a negative vote on the first patch set which is not copied |
| // since the label doesn't have a copy condition |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).revision(patchSet1.number()).review(new ReviewInput().label("Foo-Review", -1)); |
| |
| // The change owner has not been added to the attention set |
| assertThat(getAttentionSetUpdatesForUser(r, admin)).isEmpty(); |
| } |
| |
| @Test |
| public void robotReviewWithNegativeLabelDoesntAddOwnerIfChangeIsMerged() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| |
| PushOneCommit.Result r = createChange(); |
| |
| // The robot votes with Code-Review-1 on patch set 1. |
| // Without this vote the robot cannot (re-)apply a negative vote on the change after it was |
| // merged change later. |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).revision(1).review(ReviewInput.dislike()); |
| |
| // Amend the change so that patch set 2 gets created. |
| requestScopeOperations.setApiUser(admin.id()); |
| amendChange(r.getChangeId()).assertOkStatus(); |
| |
| // Approve the change. |
| approve(r.getChangeId()); |
| |
| // User adds a comment so that the admin user is added to the attention set. |
| // This has to be a comment from a user, since comments from robots do not trigger attention set |
| // updates. |
| requestScopeOperations.setApiUser(user.id()); |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.message = "A comment"; |
| change(r).current().review(reviewInput); |
| |
| // Verify that the admin user was added to the attention set. |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Someone else replied on the change"); |
| |
| // Submit the change. |
| requestScopeOperations.setApiUser(admin.id()); |
| change(r).current().submit(); |
| |
| // Verify that the attention set was cleared on submit. |
| attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted"); |
| |
| // Re-apply the negative robot vote on patch set 1. |
| // Note it's possible to a apply a negative vote on merged changes if it wasn't already present |
| // since we disallow downgrading votes on merged changes (e.g. downgrade from not present aka 0 |
| // to -1 is not allowed). |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).revision(1).review(ReviewInput.dislike()); |
| |
| // Verify that re-applying the negative robot vote on patch set 1 didn't add the admin user |
| // back to the attention set. |
| attentionSet = Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Change was submitted"); |
| } |
| |
| @Test |
| public void robotCommentDoesNotAddOwnerOnClosedChanges() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).abandon(); |
| |
| requestScopeOperations.setApiUser(robot.id()); |
| ReviewInput reviewInput = new ReviewInput(); |
| ReviewInput.RobotCommentInput robotCommentInput = |
| TestCommentHelper.createRobotCommentInputWithMandatoryFields("a.txt"); |
| reviewInput.robotComments = ImmutableMap.of("a.txt", ImmutableList.of(robotCommentInput)); |
| change(r).current().review(reviewInput); |
| |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| } |
| |
| @Test |
| public void robotCanChangeAttentionSetExplicitly() throws Exception { |
| TestAccount robot = |
| accountCreator.create( |
| "robot2", "robot2@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS); |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(robot.id()); |
| change(r).current().review(new ReviewInput().addUserToAttentionSet(admin.email(), "reason")); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(admin.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("reason"); |
| } |
| |
| @Test |
| public void addUsersToAttentionSetInPrivateChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setPrivate(true); |
| |
| // implictly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| } |
| |
| @Test |
| public void addUsersAsReviewerAndAttentionSetInPrivateChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).setPrivate(true); |
| change(r).current().review(new ReviewInput().reviewer(user.email())); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added"); |
| } |
| |
| @Test |
| public void addToAttentionSetEmail_withTemplateReason() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| String templateReason = "Added by " + AccountTemplateUtil.getAccountTemplate(user.id()); |
| int accountId = |
| change(r) |
| .addToAttentionSet(new AttentionSetInput(admin.email(), templateReason)) |
| ._accountId; |
| |
| assertThat(accountId).isEqualTo(admin.id().get()); |
| String emailBody = Iterables.getOnlyElement(sender.getMessages()).body(); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "%s requires the attention of %s to this change.\n The reason is: Added by %s.", |
| user.fullName(), admin.fullName(), user.getNameEmail())); |
| } |
| |
| @Test |
| public void removeFromAttentionSetEmail_withTemplateReason() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| // implicitly adds the user to the attention set when adding as reviewer |
| change(r).addReviewer(user.email()); |
| sender.clear(); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| String templateReason = "Removed by " + AccountTemplateUtil.getAccountTemplate(user.id()); |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput(templateReason)); |
| |
| String emailBody = Iterables.getOnlyElement(sender.getMessages()).body(); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "%s removed themselves from the attention set of this change.\n" |
| + " The reason is: Removed by %s.", |
| user.fullName(), user.getNameEmail())); |
| } |
| |
| @Test |
| public void attentionSetEmailFooter() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add user to attention set. They receive an email with the attention footer. |
| change(r).addReviewer(user.id().toString()); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .contains("Gerrit-Attention: " + user.fullName()); |
| sender.clear(); |
| |
| // Irrelevant reply, User is still in the attention set. |
| change(r).current().review(ReviewInput.approve()); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .contains("Gerrit-Attention: " + user.fullName()); |
| sender.clear(); |
| |
| // Abandon the change which removes user from attention set; there is an email but without the |
| // attention footer. |
| change(r).abandon(); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .doesNotContain("Gerrit-Attention: " + user.fullName()); |
| sender.clear(); |
| } |
| |
| @Test |
| public void attentionSetEmailHeader() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| TestAccount user2 = accountCreator.user2(); |
| |
| // The pattern ensures the header mentions the attention set requirements in any order. |
| Pattern attentionSetHeaderPattern = |
| Pattern.compile( |
| String.format( |
| "Attention is currently required from: (%s|%s), (%s|%s).", |
| user2.fullName(), user.fullName(), user.fullName(), user2.fullName())); |
| // Add user and user2 to the attention set. |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create().reviewer(user.email()).reviewer(accountCreator.user2().email())); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .containsMatch(attentionSetHeaderPattern); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody()) |
| .containsMatch(attentionSetHeaderPattern); |
| sender.clear(); |
| |
| // Irrelevant reply, User and User2 are still in the attention set. |
| change(r).current().review(ReviewInput.approve()); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .containsMatch(attentionSetHeaderPattern); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody()) |
| .containsMatch(attentionSetHeaderPattern); |
| sender.clear(); |
| |
| // Abandon the change which removes user from attention set; there is an email but without the |
| // attention footer. |
| change(r).abandon(); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).body()) |
| .doesNotContain("Attention is currently required"); |
| assertThat(Iterables.getOnlyElement(sender.getMessages()).htmlBody()) |
| .doesNotContain("Attention is currently required"); |
| sender.clear(); |
| } |
| |
| @Test |
| public void attentionSetWithEmailFilter() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add preference for the user such that they only receive an email on changes that require |
| // their attention. |
| setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY); |
| |
| // Add user to attention set. They receive an email since they are in the attention set. |
| change(r).addReviewer(user.id().toString()); |
| assertThat(sender.getMessages()).isNotEmpty(); |
| sender.clear(); |
| |
| // Irrelevant reply, User is still in the attention set, thus got another email. |
| change(r).current().review(ReviewInput.approve()); |
| assertThat(sender.getMessages()).isNotEmpty(); |
| sender.clear(); |
| |
| // Abandon the change which removes user from attention set; the user doesn't receive an email |
| // since they are not in the attention set. |
| change(r).abandon(); |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void attentionSetWithEmailFilterFiltersNewPatchsets() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add preference for the user such that they only receive an email on changes that require |
| // their attention. |
| requestScopeOperations.setApiUser(user.id()); |
| GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences(); |
| prefs.emailStrategy = EmailStrategy.ATTENTION_SET_ONLY; |
| gApi.accounts().self().setPreferences(prefs); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Add user to reviewers but not to the attention set |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email()) |
| .removeUserFromAttentionSet(user.email(), "reason")); |
| sender.clear(); |
| |
| // amending a change doesn't send an email when user is not in the attention set. |
| amendChange(r.getChangeId()); |
| assertThat(sender.getMessages()).isEmpty(); |
| } |
| |
| @Test |
| public void attentionSetWithEmailFilterStillReceivesSubmitEmail() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| // Add preference for the user such that they only receive an email on changes that require |
| // their attention. |
| requestScopeOperations.setApiUser(user.id()); |
| GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences(); |
| prefs.emailStrategy = EmailStrategy.ATTENTION_SET_ONLY; |
| gApi.accounts().self().setPreferences(prefs); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Add user to reviewers but not to the attention set |
| change(r) |
| .current() |
| .review( |
| ReviewInput.approve() |
| .reviewer(user.email()) |
| .removeUserFromAttentionSet(user.email(), "reason")); |
| sender.clear(); |
| |
| // submitting the change sends an email even when user is not in the attention set. |
| change(r).current().submit(); |
| assertThat(sender.getMessages()).isNotEmpty(); |
| } |
| |
| @Test |
| public void attentionSetWithEmailFilterImpactingOnlyChangeEmails() throws Exception { |
| // Add preference for the user such that they only receive an email on changes that require |
| // their attention. |
| setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY); |
| |
| // Ensure emails that don't relate to changes are still sent. |
| @SuppressWarnings("unused") |
| var unused = gApi.accounts().id(user.id().get()).generateHttpPassword(); |
| assertThat(sender.getMessages()).isNotEmpty(); |
| } |
| |
| @Test |
| public void cannotAddIrrelevantUserToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| String.format( |
| "%s doesn't exist or is not active on the change as an owner, uploader, reviewer, " |
| + "or cc so they can't be added to the attention set", |
| user.email())); |
| } |
| |
| @Test |
| public void cannotAddNonExistingUserToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> change(r).addToAttentionSet(new AttentionSetInput("INVALID USER", "reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "INVALID USER doesn't exist or is not active on the change as an owner," |
| + " uploader, reviewer, or cc so they can't be added to the attention set"); |
| } |
| |
| @Test |
| public void cannotRemoveIrrelevantUserToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> change(r).attention(user.email()).remove(new AttentionSetInput("reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| String.format( |
| "%s doesn't exist or is not active on the change as an owner, uploader, reviewer, " |
| + "or cc so they can't be added to the attention set", |
| user.email())); |
| } |
| |
| @Test |
| public void cannotRemoveIrrelevantUserToAttentionSetWithUserInInput() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> |
| change(r) |
| .attention(user.email()) |
| .remove(new AttentionSetInput(user.email(), "reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| String.format( |
| "%s doesn't exist or is not active on the change as an owner, uploader, reviewer, " |
| + "or cc so they can't be added to the attention set", |
| user.email())); |
| } |
| |
| @Test |
| public void cannotRemoveNonExistingUser() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> change(r).attention("INVALID USER").remove(new AttentionSetInput("reason"))); |
| assertThat(exception.getMessage()) |
| .isEqualTo( |
| "INVALID USER doesn't exist or is not active on the change as an owner," |
| + " uploader, reviewer, or cc so they can't be added to the attention set"); |
| } |
| |
| @Test |
| public void irrelevantUsersAddedToAttentionSetAreIgnoredOnReply() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| change(r).current().review(ReviewInput.create().addUserToAttentionSet(user.email(), "reason")); |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void newReviewerCanBeAddedToTheAttentionSetManually() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r) |
| .current() |
| .review( |
| ReviewInput.create() |
| .reviewer(user.email()) |
| .addUserToAttentionSet(user.email(), "reason") |
| .blockAutomaticAttentionSetRules()); |
| assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)).operation()) |
| .isEqualTo(Operation.ADD); |
| } |
| |
| @Test |
| public void newReviewerCanBeAddedToTheAttentionSetAutomatically() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| change(r).current().review(ReviewInput.create().reviewer(user.email())); |
| assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)).operation()) |
| .isEqualTo(Operation.ADD); |
| } |
| |
| @GerritConfig(name = "accounts.visibility", value = "NONE") |
| public void onReplyCanAddInvisibleUsersToAttentionSetOnVisibleChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| // admin is invisible to the user, but they can still add them to the attention set since they |
| // see the change. |
| change(r).current().review(ReviewInput.create().addUserToAttentionSet(admin.email(), "reason")); |
| assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)).operation()) |
| .isEqualTo(Operation.ADD); |
| } |
| |
| @Test |
| public void onReplyNonExistingUsersAreSilentlyIgnored() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| change(r) |
| .current() |
| .review(ReviewInput.create().addUserToAttentionSet("INVALID USER", "reason")); |
| assertThat(getAttentionSetUpdates(r.getChange().getId())).isEmpty(); |
| } |
| |
| @Test |
| public void usersNotPartOfTheChangeAreNeverInTheAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).addReviewer(user.email()); |
| |
| AttentionSetUpdate attentionSetUpdate = |
| Iterables.getOnlyElement(getAttentionSetUpdates(r.getChange().getId())); |
| assertThat(attentionSetUpdate).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSetUpdate).hasOperationThat().isEqualTo(Operation.ADD); |
| |
| ReviewInput reviewInput = ReviewInput.create(); |
| reviewInput.reviewer(user.email(), ReviewerState.REMOVED, /* confirmed= */ true); |
| reviewInput.ignoreAutomaticAttentionSetRules = true; |
| change(r).current().review(reviewInput); |
| |
| // user removed from the attention set although we ignored automatic attention set rules. |
| attentionSetUpdate = Iterables.getOnlyElement(getAttentionSetUpdates(r.getChange().getId())); |
| assertThat(attentionSetUpdate).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSetUpdate).hasOperationThat().isEqualTo(Operation.REMOVE); |
| assertThat(attentionSetUpdate) |
| .hasReasonThat() |
| .isEqualTo("Only change owner, uploader, reviewers, and cc can be in the attention set"); |
| } |
| |
| @Test |
| @GerritConfig(name = "accounts.visibility", value = "NONE") |
| public void canModifyAttentionSetForInvisibleUsersOnVisibleChanges() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| requestScopeOperations.setApiUser(user.id()); |
| |
| // admin is invisible to the user, but they can still add them to the attention set since they |
| // see the change. |
| change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "reason")); |
| assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)).operation()) |
| .isEqualTo(Operation.ADD); |
| |
| // admin is invisible to the user, but they can still remove them to the attention set since |
| // they see the change. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, admin)).operation()) |
| .isEqualTo(Operation.REMOVE); |
| } |
| |
| @Test |
| public void deleteSelfVotesDoesNotAddToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| approve(r.getChangeId()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .reviewer(admin.id().toString()) |
| .deleteVote(LabelId.CODE_REVIEW); |
| |
| assertThat(getAttentionSetUpdates(r.getChange().getId())).isEmpty(); |
| } |
| |
| @Test |
| public void deleteVotesDoesNotAffectAttentionSetWhenIgnoreAutomaticRulesIsSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| DeleteVoteInput deleteVoteInput = new DeleteVoteInput(); |
| deleteVoteInput.label = LabelId.CODE_REVIEW; |
| |
| // set this to true to not change the attention set. |
| deleteVoteInput.ignoreAutomaticAttentionSetRules = true; |
| |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .reviewer(user.id().toString()) |
| .deleteVote(deleteVoteInput); |
| |
| assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty(); |
| } |
| |
| @Test |
| public void deleteVotesOfOthersAddThemToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .reviewer(user.id().toString()) |
| .deleteVote(LabelId.CODE_REVIEW); |
| |
| AttentionSetUpdate attentionSet = |
| Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)); |
| assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id()); |
| assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD); |
| assertThat(attentionSet).hasReasonThat().isEqualTo("Their vote was deleted"); |
| } |
| |
| @Test |
| public void accountsWithNoReadPermissionIgnoredOnReply() throws Exception { |
| // Create a group with user. |
| GroupInput groupInput = new GroupInput(); |
| groupInput.name = name("User"); |
| groupInput.members = ImmutableList.of(String.valueOf(user.id())); |
| GroupInfo group = gApi.groups().create(groupInput).get(); |
| |
| PushOneCommit.Result r = createChange(); |
| gApi.changes().id(r.getChangeId()).addReviewer(user.email()); |
| |
| // remove read permission for user. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(block(Permission.READ).ref("refs/*").group(AccountGroup.uuid(group.id))) |
| .update(); |
| |
| // removing user without permissions from attention set is allowed on reply. |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(new ReviewInput().removeUserFromAttentionSet(user.email(), "reason")); |
| |
| // Add user to attention throws an exception. |
| assertThrows( |
| AuthException.class, |
| () -> change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"))); |
| |
| // Add user to attention set is ignored on reply. |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(new ReviewInput().addUserToAttentionSet(user.email(), "reason")); |
| assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)).operation()) |
| .isEqualTo(Operation.REMOVE); |
| } |
| |
| @Test |
| public void outsideAttentionSet_watchProjectEmailReceived() throws Exception { |
| setEmailStrategyForUser(EmailStrategy.ATTENTION_SET_ONLY); |
| |
| requestScopeOperations.setApiUser(user.id()); |
| watch(project.get()); |
| |
| createChange(); |
| |
| assertThat(sender.getMessages()).isNotEmpty(); |
| sender.clear(); |
| } |
| |
| @Test |
| public void approverOfOutdatedApprovalAddedToAttentionSet() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Add an approval that gets outdated when a new patch set is created (i.e. an approval that is |
| // not copied). |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the admin user to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove admin user from attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Amend the change, this removes the vote from user, as it is not copied to the new patch set. |
| sender.clear(); |
| r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo); |
| r.assertOkStatus(); |
| |
| // Verify that the approval has been removed. |
| assertThat(r.getChange().currentApprovals()).isEmpty(); |
| |
| // User got added to the attention set because users approval got outdated and was removed and |
| // user now needs to re-review the change and renew the approval. |
| assertThat(r.getChange().attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Vote got outdated and was removed: Code-Review+1")); |
| |
| // Expect that the email notification contains the outdated vote. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Attention is currently required from: %s.\n" |
| + "\n" |
| + "Hello %s, \n" |
| + "\n" |
| + "I'd like you to reexamine a change." |
| + " Please visit", |
| user.fullName(), user.fullName())); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s\n", |
| user.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "<p> Attention is currently required from: %s. </p>\n" |
| + "<p>%s <strong>uploaded patch set #2</strong> to this change.</p>", |
| user.fullName(), admin.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "View Change</a></p>" |
| + "<p>The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s</p>", |
| user.fullName())); |
| } |
| |
| @Test |
| public void approverOfMultipleOutdatedApprovalsAddedToAttentionSet() throws Exception { |
| // Create a Verify and a Foo-Var 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()); |
| |
| LabelType.Builder fooBar = |
| labelBuilder("Foo-Bar", value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| u.getConfig().upsertLabelType(fooBar.build()); |
| |
| u.save(); |
| } |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(LabelId.VERIFIED) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .add( |
| allowLabel("Foo-Bar") |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Add multiple approvals from one user that gets outdated when a new patch set is created (i.e. |
| // approvals that are not copied). |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(new ReviewInput().label(LabelId.VERIFIED, 1)); |
| gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Foo-Bar", -1)); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the admin user to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove admin user from attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Amend the change, this removes the vote from user, as it is not copied to the new patch set. |
| sender.clear(); |
| r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo); |
| r.assertOkStatus(); |
| |
| // Verify that the approvals have been removed. |
| assertThat(r.getChange().currentApprovals()).isEmpty(); |
| |
| // User got added to the attention set because users approvals got outdated and were removed and |
| // user now needs to re-review the change and renew the approvals. |
| assertThat(r.getChange().attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Votes got outdated and were removed: Code-Review+1, Foo-Bar-1, Verified+1")); |
| |
| // Expect that the email notification contains the outdated votes. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Attention is currently required from: %s.\n" |
| + "\n" |
| + "Hello %s, \n" |
| + "\n" |
| + "I'd like you to reexamine a change." |
| + " Please visit", |
| user.fullName(), user.fullName())); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s, Foo-Bar-1 by %s, Verified+1 by %s\n", |
| user.fullName(), user.fullName(), user.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "<p> Attention is currently required from: %s. </p>\n" |
| + "<p>%s <strong>uploaded patch set #2</strong> to this change.</p>", |
| user.fullName(), admin.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "View Change</a></p>" |
| + "<p>The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s, Foo-Bar-1 by %s, Verified+1 by %s</p>", |
| user.fullName(), user.fullName(), user.fullName())); |
| } |
| |
| @Test |
| public void multipleApproverOfOutdatedApprovalsAddedToAttentionSet() 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(); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Add approvals from multiple users that gets outdated when a new patch set is created (i.e. |
| // approvals that are not copied). |
| requestScopeOperations.setApiUser(user.id()); |
| recommend(r.getChangeId()); |
| TestAccount user2 = accountCreator.user2(); |
| requestScopeOperations.setApiUser(user2.id()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(new ReviewInput().label(LabelId.VERIFIED, 1)); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the admin user to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove admin user from attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Amend the change, this removes the vote from user, as it is not copied to the new patch set. |
| sender.clear(); |
| r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo); |
| r.assertOkStatus(); |
| |
| // Verify that the approvals have been removed. |
| assertThat(r.getChange().currentApprovals()).isEmpty(); |
| |
| // User got added to the attention set because users approvals got outdated and were removed and |
| // user now needs to re-review the change and renew the approvals. |
| assertThat(r.getChange().attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Vote got outdated and was removed: Code-Review+1"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user2.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Vote got outdated and was removed: Verified+1")); |
| |
| // Expect that the email notification contains the outdated votes. |
| assertThat(sender.getMessages()).hasSize(1); |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Attention is currently required from: %s, %s.\n" |
| + "\n" |
| + "Hello %s, %s, \n" |
| + "\n" |
| + "I'd like you to reexamine a change." |
| + " Please visit", |
| user.fullName(), user2.fullName(), user.fullName(), user2.fullName())); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s, Verified+1 by %s\n", |
| user.fullName(), user2.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "<p> Attention is currently required from: %s, %s. </p>\n" |
| + "<p>%s <strong>uploaded patch set #2</strong> to this change.</p>", |
| user.fullName(), user2.fullName(), admin.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "View Change</a></p>" |
| + "<p>The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s, Verified+1 by %s</p>", |
| user.fullName(), user2.fullName())); |
| } |
| |
| @Test |
| public void robotApproverOfOutdatedApprovalIsNotAddedToAttentionSet() throws Exception { |
| // Create robot account |
| TestAccount robot = |
| accountCreator.create( |
| "robot-X", |
| "robot-x@example.com", |
| "Ro Bot X", |
| "RoX", |
| ServiceUserClassifier.SERVICE_USERS, |
| "Administrators"); |
| |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Add an approval by a robot that gets outdated when a new patch set is created (i.e. an |
| // approval that is not copied). |
| requestScopeOperations.setApiUser(robot.id()); |
| recommend(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // A robot vote doesn't add the admin user to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()).isEmpty(); |
| |
| // Amend the change, this removes the vote from the robot, as it is not copied to the new patch |
| // set. |
| sender.clear(); |
| r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo); |
| r.assertOkStatus(); |
| |
| // Verify that the approval has been removed. |
| assertThat(r.getChange().currentApprovals()).isEmpty(); |
| |
| // The robot was not added to the attention set because users service users are never added to |
| // the attention set. |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Verify the email for the new patch set. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| String emailBody = message.body(); |
| assertThat(emailBody) |
| .doesNotContain( |
| String.format("Attention is currently required from: %s", robot.fullName())); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "Hello %s, \n\nI'd like you to reexamine a change. Please visit", |
| robot.fullName())); |
| assertThat(emailBody) |
| .contains( |
| String.format( |
| "The following approvals got outdated and were removed:\nCode-Review+1 by %s", |
| robot.fullName())); |
| assertThat(message.htmlBody()) |
| .doesNotContain( |
| String.format("Attention is currently required from: %s", robot.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "<p>%s <strong>uploaded patch set #2</strong> to this change.</p>", |
| admin.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "View Change</a></p>" |
| + "<p>The following approvals got outdated and were removed:\n" |
| + "Code-Review+1 by %s</p>", |
| robot.fullName())); |
| } |
| |
| @Test |
| public void approverOfCopiedApprovelNotAddedToAttentionSet() throws Exception { |
| // Allow user to make veto votes. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(LabelId.CODE_REVIEW) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-2, 1)) |
| .update(); |
| |
| PushOneCommit.Result r = createChange(); |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Add a veto vote that will be copied over to a new patch set. |
| requestScopeOperations.setApiUser(user.id()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .current() |
| .review(new ReviewInput().label(LabelId.CODE_REVIEW, -2)); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the admin user to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove admin user from attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Amend the change, this copies the vote from user to the new patch set. |
| sender.clear(); |
| r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo); |
| r.assertOkStatus(); |
| |
| // Verify that the approval has been copied. |
| List<PatchSetApproval> approvalsPs2 = r.getChange().currentApprovals(); |
| assertThat(approvalsPs2).hasSize(1); |
| assertThat(Iterables.getOnlyElement(approvalsPs2).copied()).isTrue(); |
| |
| // Attention set wasn't changed. |
| assertThat(r.getChange().attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Verify the email for the new patch set. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .doesNotContain(String.format("Attention is currently required from: %s", user.fullName())); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Hello %s, \n\nI'd like you to reexamine a change. Please visit", user.fullName())); |
| assertThat(message.body()) |
| .doesNotContain("The following approvals got outdated and were removed:"); |
| assertThat(message.htmlBody()) |
| .doesNotContain(String.format("Attention is currently required from: %s", user.fullName())); |
| assertThat(message.htmlBody()) |
| .contains( |
| String.format( |
| "<p>%s <strong>uploaded patch set #2</strong> to this change.</p>", |
| admin.fullName())); |
| assertThat(message.htmlBody()) |
| .doesNotContain("The following approvals got outdated and were removed:"); |
| } |
| |
| @Test |
| public void ownerAndUploaderAreAddedToAttentionSetWhenChangeBecomesUnsubmittable_approvalRemoved() |
| throws Exception { |
| // Allow all users to approve. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(TestLabels.codeReview().getName()) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 2)) |
| .update(); |
| |
| // Create change with admin as the owner and upload a new patch set with user as uploader. |
| PushOneCommit.Result r = createChange(); |
| r = amendChangeWithUploader(r, project, user); |
| r.assertOkStatus(); |
| |
| // Attention set is empty. |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Approve the change. |
| TestAccount approver = accountCreator.user2(); |
| requestScopeOperations.setApiUser(approver.id()); |
| approve(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the owner (admin) and the uploader (user) to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove the owner (admin) and the uploader (user) from the attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Revoke the approval |
| requestScopeOperations.setApiUser(approver.id()); |
| sender.clear(); |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.noScore()); |
| |
| // Removing the approval added the owner (admin) and the uploader (user) to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Verify the email notification that has been sent for removing the approval. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Attention is currently required from: %s, %s.\n" |
| + "\n" |
| + "%s has posted comments on this change.", |
| admin.fullName(), user.fullName(), approver.fullName())); |
| assertThat(message.body()).doesNotContain("\nPatch Set 2: Code-Review+2\n"); |
| assertThat(message.body()) |
| .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n"); |
| } |
| |
| @Test |
| public void |
| ownerAndUploaderAreAddedToAttentionSetWhenChangeBecomesUnsubmittable_approvalDowngraded() |
| throws Exception { |
| // Allow all users to approve. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(TestLabels.codeReview().getName()) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-1, 2)) |
| .update(); |
| |
| // Create change with admin as the owner and upload a new patch set with user as uploader. |
| PushOneCommit.Result r = createChange(); |
| r = amendChangeWithUploader(r, project, user); |
| r.assertOkStatus(); |
| |
| // Attention set is empty. |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Approve the change. |
| TestAccount approver = accountCreator.user2(); |
| requestScopeOperations.setApiUser(approver.id()); |
| approve(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the owner (admin) and the uploader (user) to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove the owner (admin) and the uploader (user) from the attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Downgrade the approval |
| requestScopeOperations.setApiUser(approver.id()); |
| sender.clear(); |
| recommend(r.getChangeId()); |
| |
| // Changing the approval added the owner (admin) and the uploader (user) to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Verify the email notification that has been sent for downgrading the approval. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Attention is currently required from: %s, %s.\n" |
| + "\n" |
| + "%s has posted comments on this change.", |
| admin.fullName(), user.fullName(), approver.fullName())); |
| assertThat(message.body()).doesNotContain("\nPatch Set 2: Code-Review+2\n"); |
| assertThat(message.body()).contains("\nPatch Set 2: Code-Review+1\n"); |
| assertThat(message.body()) |
| .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n"); |
| } |
| |
| @Test |
| public void ownerAndUploaderAreAddedToAttentionSetWhenChangeBecomesUnsubmittable_vetoApplied() |
| throws Exception { |
| // Allow all users to approve and veto. |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(TestLabels.codeReview().getName()) |
| .ref(RefNames.REFS_HEADS + "*") |
| .group(REGISTERED_USERS) |
| .range(-2, 2)) |
| .update(); |
| |
| // Create change with admin as the owner and upload a new patch set with user as uploader. |
| PushOneCommit.Result r = createChange(); |
| r = amendChangeWithUploader(r, project, user); |
| r.assertOkStatus(); |
| |
| // Attention set is empty. |
| assertThat(r.getChange().attentionSet()).isEmpty(); |
| |
| // Approve the change. |
| TestAccount approver = accountCreator.user2(); |
| requestScopeOperations.setApiUser(approver.id()); |
| approve(r.getChangeId()); |
| requestScopeOperations.setApiUser(admin.id()); |
| |
| // Voting added the owner (admin) and the uploader (user) to the attention set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Remove the owner (admin) and the uploader (user) from the attention set. |
| change(r).attention(admin.id().toString()).remove(new AttentionSetInput("removed")); |
| change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed")); |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.REMOVE, "removed"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed")); |
| |
| // Apply veto by another user. |
| TestAccount approver2 = accountCreator.user2(); |
| sender.clear(); |
| requestScopeOperations.setApiUser(approver2.id()); |
| gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject()); |
| |
| // Adding the veto approval added the owner (admin) and the uploader (user) to the attention |
| // set. |
| assertThat(changeDataFactory.create(project, r.getChange().getId()).attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change"), |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| user.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Someone else replied on the change")); |
| |
| // Verify the email notification that has been sent for adding the veto. |
| Message message = Iterables.getOnlyElement(sender.getMessages()); |
| assertThat(message.body()) |
| .contains( |
| String.format( |
| "Attention is currently required from: %s, %s.\n" |
| + "\n" |
| + "%s has posted comments on this change.", |
| admin.fullName(), user.fullName(), approver.fullName())); |
| assertThat(message.body()).contains("\nPatch Set 2: Code-Review-2\n"); |
| assertThat(message.body()) |
| .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n"); |
| } |
| |
| @Test |
| @GerritConfig(name = "attentionSet.readdOwnerAfter", value = "1w") |
| @GerritConfig(name = "attentionSet.readdOwnerMessage", value = "Owner has been added") |
| public void readdOwnerForInactiveOpenChanges() throws Exception { |
| // create 2 changes where the owner will be added to the attention-set |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // ... because they are older than 1 week |
| fakeClock.advance(Duration.ofDays(7)); |
| |
| // create 1 change where the owner should not be added to the attention-set |
| PushOneCommit.Result r3 = createChange(); |
| |
| assertThat(r1.getChange().attentionSet()).isEmpty(); |
| assertThat(r2.getChange().attentionSet()).isEmpty(); |
| assertThat(r3.getChange().attentionSet()).isEmpty(); |
| |
| sender.clear(); |
| readdOwnerUtil.readdOwnerForInactiveOpenChanges(batchUpdateFactory); |
| assertThat(r1.getChange().attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Owner has been added")); |
| assertThat(r2.getChange().attentionSet()) |
| .containsExactly( |
| AttentionSetUpdate.createFromRead( |
| fakeClock.now(), |
| admin.id(), |
| AttentionSetUpdate.Operation.ADD, |
| "Owner has been added")); |
| assertThat(r3.getChange().attentionSet()).isEmpty(); |
| assertThat(sender.getMessages()).hasSize(2); |
| } |
| |
| private void setEmailStrategyForUser(EmailStrategy es) throws Exception { |
| requestScopeOperations.setApiUser(user.id()); |
| GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences(); |
| prefs.emailStrategy = es; |
| gApi.accounts().self().setPreferences(prefs); |
| requestScopeOperations.setApiUser(admin.id()); |
| } |
| |
| private List<AttentionSetUpdate> getAttentionSetUpdatesForUser( |
| PushOneCommit.Result r, TestAccount account) { |
| return getAttentionSetUpdates(r.getChange().getId()).stream() |
| .filter(a -> a.account().equals(account.id())) |
| .collect(Collectors.toList()); |
| } |
| |
| private List<AttentionSetUpdate> getAttentionSetUpdates(Change.Id changeId) { |
| List<ChangeData> changeData = changeQueryProvider.get().byLegacyChangeId(changeId); |
| if (changeData.size() != 1) { |
| throw new IllegalStateException( |
| String.format("Not exactly one change found for ID %s.", changeId)); |
| } |
| return new ArrayList<>(Iterables.getOnlyElement(changeData).attentionSet()); |
| } |
| |
| private ReviewInput reviewWithComment() { |
| return reviewInReplyToComment(null); |
| } |
| |
| private ReviewInput reviewInReplyToComment(@Nullable String id) { |
| ReviewInput.CommentInput comment = new ReviewInput.CommentInput(); |
| comment.side = Side.REVISION; |
| comment.path = Patch.COMMIT_MSG; |
| comment.message = "comment"; |
| comment.setUpdated(TimeUtil.now()); |
| comment.inReplyTo = id; |
| ReviewInput reviewInput = new ReviewInput(); |
| reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment)); |
| return reviewInput; |
| } |
| |
| private Correspondence<AttentionSetUpdate, Account.Id> hasAccount() { |
| return NullAwareCorrespondence.transforming(AttentionSetUpdate::account, "hasAccount"); |
| } |
| } |