| // Copyright (C) 2023 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.acceptance.api.change; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; |
| 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.server.group.SystemGroupBackend.REGISTERED_USERS; |
| import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER; |
| 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.Iterables; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.ExtensionRegistry; |
| import com.google.gerrit.acceptance.ExtensionRegistry.Registration; |
| import com.google.gerrit.acceptance.TestMetricMaker; |
| import com.google.gerrit.acceptance.UseLocalDisk; |
| import com.google.gerrit.acceptance.testsuite.account.AccountOperations; |
| import com.google.gerrit.acceptance.testsuite.change.ChangeOperations; |
| import com.google.gerrit.acceptance.testsuite.group.GroupOperations; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.common.data.GlobalCapability; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.LabelId; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.SubmitRequirement; |
| import com.google.gerrit.entities.SubmitRequirementExpression; |
| import com.google.gerrit.extensions.api.changes.RebaseInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.common.ActionInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.ChangeMessageInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.extensions.events.RevisionCreatedListener; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.server.notedb.ChangeNoteUtil; |
| import com.google.gerrit.server.project.testing.TestLabels; |
| import com.google.gerrit.server.util.AccountTemplateUtil; |
| import com.google.inject.Inject; |
| import java.util.Collection; |
| import java.util.Optional; |
| import org.eclipse.jgit.lib.ReflogEntry; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.FooterLine; |
| import org.junit.Test; |
| |
| /** |
| * Tests for the {@link com.google.gerrit.server.restapi.change.Rebase} REST endpoint with the |
| * {@link RebaseInput#onBehalfOfUploader} option being set. |
| * |
| * <p>Rebasing a chain on behalf of the uploader is covered by {@link |
| * RebaseChainOnBehalfOfUploaderIT}. |
| */ |
| public class RebaseOnBehalfOfUploaderIT extends AbstractDaemonTest { |
| @Inject private AccountOperations accountOperations; |
| @Inject private ChangeOperations changeOperations; |
| @Inject private GroupOperations groupOperations; |
| @Inject private ProjectOperations projectOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| @Inject private ExtensionRegistry extensionRegistry; |
| @Inject private TestMetricMaker testMetricMaker; |
| |
| @Test |
| public void cannotRebaseOnBehalfOfUploaderWithAllowConflicts() throws Exception { |
| Account.Id uploader = accountOperations.newAccount().create(); |
| Change.Id changeId = changeOperations.newChange().owner(uploader).create(); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| rebaseInput.allowConflicts = true; |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, () -> gApi.changes().id(changeId.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo("allow_conflicts and on_behalf_of_uploader are mutually exclusive"); |
| } |
| |
| @Test |
| public void rebaseOnBehalfOfUploaderWithCommitterEmail() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderPreferredEmail = "uploader.preferred@example.com"; |
| String uploaderSecondaryEmail = "uploader.secondary@example.com"; |
| Account.Id uploader = |
| accountOperations |
| .newAccount() |
| .preferredEmail(uploaderPreferredEmail) |
| .addSecondaryEmail(uploaderSecondaryEmail) |
| .create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| projectOperations |
| .allProjectsForUpdate() |
| .add(allowCapability(GlobalCapability.VIEW_SECONDARY_EMAILS).group(REGISTERED_USERS)) |
| .update(); |
| |
| // Create two changes both with the same parent. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| rebaseInput.committerEmail = uploaderSecondaryEmail; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| assertThat( |
| gApi.changes() |
| .id(changeToBeRebased.get()) |
| .get() |
| .getCurrentRevision() |
| .commit |
| .committer |
| .email) |
| .isEqualTo(uploaderSecondaryEmail); |
| } |
| |
| @Test |
| public void cannotRebaseOnBehalfOfUploaderWithCommitterEmailWithoutViewSecondaryEmails() |
| throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderPreferredEmail = "uploader.preferred@example.com"; |
| String uploaderSecondaryEmail = "uploader.secondary@example.com"; |
| Account.Id uploader = |
| accountOperations |
| .newAccount() |
| .preferredEmail(uploaderPreferredEmail) |
| .addSecondaryEmail(uploaderSecondaryEmail) |
| .create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| rebaseInput.committerEmail = uploaderSecondaryEmail; |
| |
| ResourceConflictException exception = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "Cannot rebase using committer email '%s'. It can only be done using " |
| + "the preferred email or the committer email of the uploader", |
| uploaderSecondaryEmail)); |
| } |
| |
| @Test |
| public void cannotRebaseNonCurrentPatchSetOnBehalfOfUploader() throws Exception { |
| Account.Id uploader = accountOperations.newAccount().create(); |
| Change.Id changeId = changeOperations.newChange().owner(uploader).create(); |
| changeOperations.change(changeId).newPatchset().create(); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| BadRequestException exception = |
| assertThrows( |
| BadRequestException.class, |
| () -> gApi.changes().id(changeId.get()).revision(1).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "change %s: non-current patch set cannot be rebased on behalf of the uploader", |
| changeId)); |
| } |
| |
| @Test |
| public void rebaseChangeOnBehalfOfUploader_withRebasePermission() throws Exception { |
| testRebaseChangeOnBehalfOfUploader( |
| Permission.REBASE, |
| (changeId, rebaseInput) -> gApi.changes().id(changeId.get()).rebase(rebaseInput)); |
| } |
| |
| @Test |
| public void rebaseCurrentPatchSetOnBehalfOfUploader_withRebasePermission() throws Exception { |
| testRebaseChangeOnBehalfOfUploader( |
| Permission.REBASE, |
| (changeId, rebaseInput) -> gApi.changes().id(changeId.get()).current().rebase(rebaseInput)); |
| } |
| |
| @Test |
| public void rebaseChangeOnBehalfOfUploader_withSubmitPermission() throws Exception { |
| testRebaseChangeOnBehalfOfUploader( |
| Permission.SUBMIT, |
| (changeId, rebaseInput) -> gApi.changes().id(changeId.get()).rebase(rebaseInput)); |
| } |
| |
| @Test |
| public void rebaseCurrentPatchSetOnBehalfOfUploader_withSubmitPermission() throws Exception { |
| testRebaseChangeOnBehalfOfUploader( |
| Permission.SUBMIT, |
| (changeId, rebaseInput) -> gApi.changes().id(changeId.get()).current().rebase(rebaseInput)); |
| } |
| |
| private void testRebaseChangeOnBehalfOfUploader(String permissionToAllow, RebaseCall rebaseCall) |
| throws Exception { |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id changeOwner = accountOperations.newAccount().create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Grant permission to rebaser that is required to rebase on behalf of the uploader. |
| AccountGroup.UUID allowedGroup = |
| groupOperations.newGroup().name("can-" + permissionToAllow).addMember(rebaser).create(); |
| allowPermission(permissionToAllow, allowedGroup); |
| |
| // Block rebase and submit permission for uploader. For rebase on behalf of the uploader only |
| // the rebaser needs to have this permission, but not the uploader on whom's behalf the rebase |
| // is done. |
| AccountGroup.UUID cannotRebaseAndSubmitGroup = |
| groupOperations.newGroup().name("cannot-rebase").addMember(uploader).create(); |
| blockPermission(Permission.REBASE, cannotRebaseAndSubmitGroup); |
| blockPermission(Permission.SUBMIT, cannotRebaseAndSubmitGroup); |
| |
| // Block push permission for rebaser, as in contrast to rebase, rebase on behalf of the uploader |
| // doesn't require the rebaser to have the push permission. |
| AccountGroup.UUID cannotUploadGroup = |
| groupOperations.newGroup().name("cannot-upload").addMember(rebaser).create(); |
| blockPermission(Permission.PUSH, cannotUploadGroup); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(changeOwner); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| |
| // Create a second patch set for the change that will be rebased so that the uploader is |
| // different to the change owner. This is to verify that being change owner doesn't matter for |
| // the user on whom's behalf the rebase is done. |
| // Set author and committer to the uploader so that rebasing on behalf of the uploader doesn't |
| // require the Forge Author and Forge Committer permission. |
| changeOperations |
| .change(changeToBeRebased) |
| .newPatchset() |
| .uploader(uploader) |
| .author(uploader) |
| .committer(uploader) |
| .create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| |
| TestRevisionCreatedListener testRevisionCreatedListener = new TestRevisionCreatedListener(); |
| try (Registration registration = |
| extensionRegistry.newRegistration().add(testRevisionCreatedListener)) { |
| rebaseCall.call(changeToBeRebased, rebaseInput); |
| |
| assertThat(testRevisionCreatedListener.revisionInfo.uploader._accountId) |
| .isEqualTo(uploader.get()); |
| assertThat(testRevisionCreatedListener.revisionInfo.realUploader._accountId) |
| .isEqualTo(rebaser.get()); |
| } |
| |
| ChangeInfo changeInfo2 = gApi.changes().id(changeToBeRebased.get()).get(); |
| RevisionInfo currentRevisionInfo = changeInfo2.getCurrentRevision(); |
| // The change had 2 patch sets before the rebase, now it should be 3 |
| assertThat(currentRevisionInfo._number).isEqualTo(3); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| assertThat(currentRevisionInfo.commit.author.email).isEqualTo(uploaderEmail); |
| assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail); |
| |
| // Verify that the rebaser was recorded as realUser in NoteDb. |
| Optional<FooterLine> realUserFooter = |
| projectOperations.project(project).getHead(RefNames.changeMetaRef(changeToBeRebased)) |
| .getFooterLines().stream() |
| .filter(footerLine -> footerLine.matches(FOOTER_REAL_USER)) |
| .findFirst(); |
| assertThat(realUserFooter.map(FooterLine::getValue)) |
| .hasValue( |
| String.format( |
| "%s <%s>", |
| ChangeNoteUtil.getAccountIdAsUsername(rebaser), |
| changeNoteUtil.getAccountIdAsEmailAddress(rebaser))); |
| |
| // Verify the message that has been posted on the change. |
| Collection<ChangeMessageInfo> changeMessages = changeInfo2.messages; |
| // Before the rebase the change had 2 messages for the upload of the 2 patch sets. Rebase is |
| // expected to add another message. |
| assertThat(changeMessages).hasSize(3); |
| ChangeMessageInfo changeMessage = Iterables.getLast(changeMessages); |
| assertThat(changeMessage.message) |
| .isEqualTo( |
| "Patch Set 3: Patch Set 2 was rebased on behalf of " |
| + AccountTemplateUtil.getAccountTemplate(uploader)); |
| assertThat(changeMessage.author._accountId).isEqualTo(uploader.get()); |
| assertThat(changeMessage.realAuthor._accountId).isEqualTo(rebaser.get()); |
| } |
| |
| @Test |
| public void rebaseChangeOnBehalfOfUploaderAfterUpdatingPreferredEmailForUploader() |
| throws Exception { |
| String uploaderEmailOne = "uploader1@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmailOne).create(); |
| |
| // Create two changes both with the same parent |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Change preferred email for the uploader |
| String uploaderEmailTwo = "uploader2@example.com"; |
| accountOperations.account(uploader).forUpdate().preferredEmail(uploaderEmailTwo).update(); |
| |
| // Rebase the second change on behalf of the uploader |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| assertThat( |
| gApi.changes() |
| .id(changeToBeRebased.get()) |
| .get() |
| .getCurrentRevision() |
| .commit |
| .committer |
| .email) |
| .isEqualTo(uploaderEmailOne); |
| } |
| |
| @Test |
| public void rebaseChangeOnBehalfOfUploaderMultipleTimesInARow() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| ChangeInfo changeInfo2 = gApi.changes().id(changeToBeRebased.get()).get(); |
| RevisionInfo currentRevisionInfo = changeInfo2.getCurrentRevision(); |
| // The change had 1 patch set before the rebase, now it should be 2 |
| assertThat(currentRevisionInfo._number).isEqualTo(2); |
| assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| |
| // Create and submit another change so that we can rebase the change once again. |
| requestScopeOperations.setApiUser(approver); |
| Change.Id changeToBeTheNewBase2 = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase2.get()).current().submit(); |
| |
| // Rebase the change once again on behalf of the uploader. |
| requestScopeOperations.setApiUser(rebaser); |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| changeInfo2 = gApi.changes().id(changeToBeRebased.get()).get(); |
| currentRevisionInfo = changeInfo2.getCurrentRevision(); |
| // The change had 2 patch sets before the rebase, now it should be 3 |
| assertThat(currentRevisionInfo._number).isEqualTo(3); |
| assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| |
| // Create and submit another change so that we can rebase the change once again. |
| requestScopeOperations.setApiUser(approver); |
| Change.Id changeToBeTheNewBase3 = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| gApi.changes().id(changeToBeTheNewBase3.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase3.get()).current().submit(); |
| |
| // Rebase the change once again on behalf of the uploader, this time by another rebaser. |
| Account.Id rebaser2 = accountOperations.newAccount().create(); |
| requestScopeOperations.setApiUser(rebaser2); |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| changeInfo2 = gApi.changes().id(changeToBeRebased.get()).get(); |
| currentRevisionInfo = changeInfo2.getCurrentRevision(); |
| // The change had 3 patch sets before the rebase, now it should be 4 |
| assertThat(currentRevisionInfo._number).isEqualTo(4); |
| assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser2.get()); |
| } |
| |
| @Test |
| public void nonChangeOwnerWithoutSubmitAndRebasePermissionCannotRebaseOnBehalfOfUploader() |
| throws Exception { |
| Change.Id changeId = changeOperations.newChange().project(project).create(); |
| |
| blockPermissionForAllUsers(Permission.REBASE); |
| blockPermissionForAllUsers(Permission.SUBMIT); |
| |
| Account.Id rebaserId = accountOperations.newAccount().create(); |
| requestScopeOperations.setApiUser(rebaserId); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| AuthException exception = |
| assertThrows( |
| AuthException.class, () -> gApi.changes().id(changeId.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| "rebase on behalf of uploader not permitted (change owners and users with the 'Submit'" |
| + " or 'Rebase' permission can rebase on behalf of the uploader)"); |
| } |
| |
| @Test |
| public void cannotRebaseChangeOnBehalfOfUploaderIfTheUploaderHasNoReadPermission() |
| throws Exception { |
| String uploaderEmail = "uploader@example.com"; |
| testCannotRebaseChangeOnBehalfOfUploaderIfTheUploaderHasNoPermission( |
| uploaderEmail, |
| Permission.READ, |
| String.format("uploader %s cannot read change", uploaderEmail)); |
| } |
| |
| @Test |
| public void cannotRebaseChangeOnBehalfOfUploaderIfTheUploaderHasNoPushPermission() |
| throws Exception { |
| String uploaderEmail = "uploader@example.com"; |
| testCannotRebaseChangeOnBehalfOfUploaderIfTheUploaderHasNoPermission( |
| uploaderEmail, |
| Permission.PUSH, |
| String.format("uploader %s cannot add patch set", uploaderEmail)); |
| } |
| |
| private void testCannotRebaseChangeOnBehalfOfUploaderIfTheUploaderHasNoPermission( |
| String uploaderEmail, String permissionToBlock, String expectedErrorMessage) |
| throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Block the required permission for uploader. Without this permission it should not be possible |
| // to rebase the change on behalf of the uploader. |
| AccountGroup.UUID blockedGroup = |
| groupOperations.newGroup().name("cannot-" + permissionToBlock).addMember(uploader).create(); |
| blockPermission(permissionToBlock, blockedGroup); |
| |
| // Try to rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| ResourceConflictException exception = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo(String.format("change %s: %s", changeToBeRebased, expectedErrorMessage)); |
| } |
| |
| @Test |
| public void rebaseChangeOnBehalfOfYourself() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| Account.Id uploader = |
| accountOperations.newAccount().preferredEmail("uploader@example.com").create(); |
| Account.Id approver = admin.id(); |
| |
| // Create two changes both with the same parent. Forge the author of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase the second change as uploader on behalf of the uploader |
| requestScopeOperations.setApiUser(uploader); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| RevisionInfo currentRevisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| // The change had 1 patch set before the rebase, now it should be 2 |
| assertThat(currentRevisionInfo._number).isEqualTo(2); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader).isNull(); |
| } |
| |
| @Test |
| public void cannotRebaseChangeOnBehalfOfYourselfWithoutPushPermission() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| Account.Id uploader = |
| accountOperations.newAccount().preferredEmail("uploader@example.com").create(); |
| Account.Id approver = admin.id(); |
| |
| // Create two changes both with the same parent. Forge the author of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Block push for uploader. For rebase on behalf of the uploader only |
| // the rebaser needs to have this permission, but not the uploader on whom's behalf the rebase |
| // is done. |
| AccountGroup.UUID cannotPushGroup = |
| groupOperations.newGroup().name("cannot-push").addMember(uploader).create(); |
| blockPermission(Permission.PUSH, cannotPushGroup); |
| |
| // Rebase the second change as uploader on behalf of the uploader |
| requestScopeOperations.setApiUser(uploader); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| AuthException exception = |
| assertThrows( |
| AuthException.class, |
| () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'" |
| + " permission can rebase if they have the 'Push' permission)"); |
| } |
| |
| @Test |
| public void rebaseChangeOnBehalfOfUploaderWhenUploaderIsNotTheChangeOwner() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| Account.Id changeOwner = accountOperations.newAccount().create(); |
| Account.Id uploader = |
| accountOperations.newAccount().preferredEmail("uploader@example.com").create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Forge the author of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(changeOwner); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| |
| // Create a second patch set for the change that will be rebased so that the uploader is |
| // different to the change owner. |
| // Set author and committer to the uploader so that rebasing on behalf of the uploader doesn't |
| // require the Forge Author and Forge Committer permission. |
| changeOperations |
| .change(changeToBeRebased) |
| .newPatchset() |
| .uploader(uploader) |
| .author(uploader) |
| .committer(uploader) |
| .create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Grant add patch set permission for uploader. Without the add patch set permission it is not |
| // possible to rebase the change on behalf of the uploader since the uploader cannot add a |
| // patch set to a change that is owned by another user. |
| AccountGroup.UUID canAddPatchSet = |
| groupOperations.newGroup().name("can-add-patch-set").addMember(uploader).create(); |
| allowPermission(Permission.ADD_PATCH_SET, canAddPatchSet); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| RevisionInfo currentRevisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| // The change had 2 patch set before the rebase, now it should be 3 |
| assertThat(currentRevisionInfo._number).isEqualTo(3); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| } |
| |
| @Test |
| public void |
| cannotRebaseChangeOnBehalfOfUploaderWhenUploaderIsNotTheChangeOwnerAndDoesntHaveAddPatchSetPermission() |
| throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id changeOwner = accountOperations.newAccount().create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Forge the author of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(changeOwner); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| |
| // Create a second patch set for the change that will be rebased so that the uploader is |
| // different to the change owner. |
| // Set author and committer to the uploader so that rebasing on behalf of the uploader doesn't |
| // require the Forge Author and Forge Committer permission. |
| changeOperations |
| .change(changeToBeRebased) |
| .newPatchset() |
| .uploader(uploader) |
| .author(uploader) |
| .committer(uploader) |
| .create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Block add patch set permission for uploader. Without the add patch set permission it should |
| // not possible to rebase the change on behalf of the uploader since the uploader cannot add a |
| // patch set to a change that is owned by another user. |
| AccountGroup.UUID cannotAddPatchSet = |
| groupOperations.newGroup().name("cannot-add-patch-set").addMember(uploader).create(); |
| blockPermission(Permission.ADD_PATCH_SET, cannotAddPatchSet); |
| |
| // Try to rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| ResourceConflictException exception = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "change %s: uploader %s cannot add patch set", changeToBeRebased, uploaderEmail)); |
| } |
| |
| @Test |
| public void rebaseChangeWithForgedAuthorOnBehalfOfUploader() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String authorEmail = "author@example.com"; |
| Account.Id author = accountOperations.newAccount().preferredEmail(authorEmail).create(); |
| Account.Id uploader = |
| accountOperations.newAccount().preferredEmail("uploader@example.com").create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Forge the author of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).author(author).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Grant forge author permission for uploader. Without the forge author permission it is not |
| // possible to rebase the change on behalf of the uploader. |
| AccountGroup.UUID canForgeAuthor = |
| groupOperations.newGroup().name("can-forge-author").addMember(uploader).create(); |
| allowPermission(Permission.FORGE_AUTHOR, canForgeAuthor); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| RevisionInfo currentRevisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| // The change had 1 patch set before the rebase, now it should be 2 |
| assertThat(currentRevisionInfo._number).isEqualTo(2); |
| assertThat(currentRevisionInfo.commit.author.email).isEqualTo(authorEmail); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| } |
| |
| @Test |
| public void |
| cannotRebaseChangeWithForgedAuthorOnBehalfOfUploaderIfTheUploaderHasNoForgeAuthorPermission() |
| throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id author = accountOperations.newAccount().create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Forge the author of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).author(author).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Block forge author permission for uploader. Without the forge author permission it should not |
| // be possible to rebase the change on behalf of the uploader. |
| AccountGroup.UUID cannotForgeAuthor = |
| groupOperations.newGroup().name("cannot-forge-author").addMember(uploader).create(); |
| blockPermission(Permission.FORGE_AUTHOR, cannotForgeAuthor); |
| |
| // Try to rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| ResourceConflictException exception = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "change %s: author of patch set 1 is forged and the uploader %s cannot forge author", |
| changeToBeRebased, uploaderEmail)); |
| } |
| |
| @Test |
| public void |
| rebaseChangeWithForgedCommitterOnBehalfOfUploaderDoesntRequireForgeCommitterPermission() |
| throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id committer = |
| accountOperations.newAccount().preferredEmail("committer@example.com").create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Forge the committer of the change that will be |
| // rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).committer(committer).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| RevisionInfo currentRevisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| // The change had 1 patch set before the rebase, now it should be 2 |
| assertThat(currentRevisionInfo._number).isEqualTo(2); |
| assertThat(currentRevisionInfo.commit.committer.email).isEqualTo(uploaderEmail); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| } |
| |
| @Test |
| public void rebaseChangeWithServerIdentOnBehalfOfUploader() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Use the server identity as the author of the |
| // change that will be rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations |
| .newChange() |
| .project(project) |
| .owner(uploader) |
| .authorIdent(serverIdent.get()) |
| .create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Grant forge author and forge server permission for uploader. Without these permissions it is |
| // not possible to rebase the change on behalf of the uploader. |
| AccountGroup.UUID canForgeAuthorAndForgeServer = |
| groupOperations |
| .newGroup() |
| .name("can-forge-author-and-forge-server") |
| .addMember(uploader) |
| .create(); |
| allowPermission(Permission.FORGE_AUTHOR, canForgeAuthorAndForgeServer); |
| allowPermission(Permission.FORGE_SERVER, canForgeAuthorAndForgeServer); |
| |
| // Rebase the second change on behalf of the uploader. |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| RevisionInfo currentRevisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| // The change had 1 patch set before the rebase, now it should be 2 |
| assertThat(currentRevisionInfo._number).isEqualTo(2); |
| assertThat(currentRevisionInfo.commit.author.email) |
| .isEqualTo(serverIdent.get().getEmailAddress()); |
| assertThat(currentRevisionInfo.uploader._accountId).isEqualTo(uploader.get()); |
| assertThat(currentRevisionInfo.realUploader._accountId).isEqualTo(rebaser.get()); |
| } |
| |
| @Test |
| public void |
| cannotRebaseChangeWithServerIdentOnBehalfOfUploaderIfTheUploaderHasNoForgeServerPermission() |
| throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent. Use the server identity as the author of the |
| // change that will be rebased. |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations |
| .newChange() |
| .project(project) |
| .owner(uploader) |
| .authorIdent(serverIdent.get()) |
| .create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Grant forge author permission for uploader, but not the forge server permission. Without the |
| // forge server permission it is not possible to rebase the change on behalf of the uploader. |
| AccountGroup.UUID canForgeAuthor = |
| groupOperations.newGroup().name("can-forge-author").addMember(uploader).create(); |
| allowPermission(Permission.FORGE_AUTHOR, canForgeAuthor); |
| |
| // Try to rebase the second change on behalf of the uploader. |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| ResourceConflictException exception = |
| assertThrows( |
| ResourceConflictException.class, |
| () -> gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput)); |
| assertThat(exception) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "change %s: author of patch set 1 is the server identity and the uploader %s cannot forge" |
| + " the server identity", |
| changeToBeRebased, uploaderEmail)); |
| } |
| |
| @Test |
| public void rebaseActionEnabled_withRebasePermission() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| testRebaseActionEnabled(); |
| } |
| |
| @Test |
| public void rebaseActionEnabled_withSubmitPermission() throws Exception { |
| allowPermissionToAllUsers(Permission.SUBMIT); |
| testRebaseActionEnabled(); |
| } |
| |
| private void testRebaseActionEnabled() throws Exception { |
| Account.Id uploader = accountOperations.newAccount().create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Block push permission for rebaser, as in contrast to rebase, rebase on behalf of the uploader |
| // doesn't require the rebaser to have the push permission. |
| AccountGroup.UUID cannotUploadGroup = |
| groupOperations.newGroup().name("cannot-upload").addMember(rebaser).create(); |
| blockPermission(Permission.PUSH, cannotUploadGroup); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| requestScopeOperations.setApiUser(rebaser); |
| RevisionInfo revisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| assertThat(revisionInfo.actions).containsKey("rebase"); |
| ActionInfo rebaseActionInfo = revisionInfo.actions.get("rebase"); |
| assertThat(rebaseActionInfo.enabled).isTrue(); |
| |
| // rebase is disabled because rebaser doesn't have the 'Push' permission and hence cannot create |
| // new patch sets |
| assertThat(rebaseActionInfo.enabledOptions).containsExactly("rebase_on_behalf_of_uploader"); |
| } |
| |
| @Test |
| public void rebaseActionEnabled_forChangeOwner() throws Exception { |
| Account.Id changeOwner = accountOperations.newAccount().create(); |
| Account.Id approver = admin.id(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(changeOwner); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(changeOwner).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| requestScopeOperations.setApiUser(changeOwner); |
| RevisionInfo revisionInfo = |
| gApi.changes().id(changeToBeRebased.get()).get().getCurrentRevision(); |
| assertThat(revisionInfo.actions).containsKey("rebase"); |
| ActionInfo rebaseActionInfo = revisionInfo.actions.get("rebase"); |
| assertThat(rebaseActionInfo.enabled).isTrue(); |
| |
| // rebase is enabled because change owner has the 'Push' permission and hence can create new |
| // patch sets |
| assertThat(rebaseActionInfo.enabledOptions) |
| .containsExactly("rebase", "rebase_on_behalf_of_uploader"); |
| } |
| |
| @UseLocalDisk |
| @Test |
| public void rebaseChangeOnBehalfOfUploaderRecordsUploaderInRefLog() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| try (Repository repo = repoManager.openRepository(project)) { |
| String changeMetaRef = RefNames.changeMetaRef(changeToBeRebased); |
| String patchSetRef = RefNames.patchSetRef(PatchSet.id(changeToBeRebased, 2)); |
| createRefLogFileIfMissing(repo, changeMetaRef); |
| createRefLogFileIfMissing(repo, patchSetRef); |
| |
| // Rebase the second change on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| // The ref log for the patch set ref records the impersonated user aka the uploader. |
| ReflogEntry patchSetRefLogEntry = repo.getReflogReader(patchSetRef).getLastEntry(); |
| assertThat(patchSetRefLogEntry.getWho().getEmailAddress()).isEqualTo(uploaderEmail); |
| |
| // The ref log for the change meta ref records the impersonated user aka the uploader. |
| ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry(); |
| assertThat(changeMetaRefLogEntry.getWho().getEmailAddress()).isEqualTo(uploaderEmail); |
| } |
| } |
| |
| @Test |
| public void rebaserCanApproveChangeAfterRebasingOnBehalfOfUploader() throws Exception { |
| // Require a Code-Review approval from a non-uploader for submit. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig() |
| .upsertSubmitRequirement( |
| SubmitRequirement.builder() |
| .setName(TestLabels.codeReview().getName()) |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| String.format( |
| "label:%s=MAX,user=non_uploader", TestLabels.codeReview().getName()))) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| u.save(); |
| } |
| |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase it on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| // Approve the change as the rebaser. |
| allowVotingOnCodeReviewToAllUsers(); |
| gApi.changes().id(changeToBeRebased.get()).current().review(ReviewInput.approve()); |
| |
| // The change is submittable because the approval is from a user (the rebaser) that is not the |
| // uploader. |
| assertThat(gApi.changes().id(changeToBeRebased.get()).get().submittable).isTrue(); |
| |
| // Create and submit another change so that we can rebase the change once again. |
| requestScopeOperations.setApiUser(approver); |
| Change.Id changeToBeTheNewBase2 = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase2.get()).current().submit(); |
| |
| // Doing a normal rebase (not on behalf of the uploader) makes the rebaser the uploader. This |
| // makse the change non-submittable since the approval of the rebaser is ignored now (due to |
| // using 'user=non_uploader' in the submit requirement expression). |
| requestScopeOperations.setApiUser(rebaser); |
| rebaseInput.onBehalfOfUploader = false; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| gApi.changes().id(changeToBeRebased.get()).current().review(ReviewInput.approve()); |
| assertThat(gApi.changes().id(changeToBeRebased.get()).get().submittable).isFalse(); |
| } |
| |
| @Test |
| public void testSubmittedWithRebaserApprovalMetric() throws Exception { |
| allowVotingOnCodeReviewToAllUsers(); |
| |
| createVerifiedLabel(); |
| allowVotingOnVerifiedToAllUsers(); |
| |
| // Require a Code-Review approval from a non-uploader for submit. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig() |
| .upsertSubmitRequirement( |
| SubmitRequirement.builder() |
| .setName(TestLabels.verified().getName()) |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| String.format("label:%s=MAX", TestLabels.verified().getName()))) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| u.getConfig() |
| .upsertSubmitRequirement( |
| SubmitRequirement.builder() |
| .setName(TestLabels.codeReview().getName()) |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| String.format( |
| "label:%s=MAX,user=non_uploader", TestLabels.codeReview().getName()))) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| u.save(); |
| } |
| |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes() |
| .id(changeToBeTheNewBase.get()) |
| .current() |
| .review(ReviewInput.approve().label(TestLabels.verified().getName(), 1)); |
| testMetricMaker.reset(); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(0); |
| |
| // Rebase it on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| // Approve the change as the rebaser. |
| gApi.changes() |
| .id(changeToBeRebased.get()) |
| .current() |
| .review(ReviewInput.approve().label(TestLabels.verified().getName(), 1)); |
| |
| // The change is submittable because the approval is from a user (the rebaser) that is not the |
| // uploader. |
| allowPermissionToAllUsers(Permission.SUBMIT); |
| testMetricMaker.reset(); |
| gApi.changes().id(changeToBeRebased.get()).current().submit(); |
| assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(1); |
| } |
| |
| @Test |
| public void submittedWithRebaserApprovalMetricIsNotIncreasedIfANonRebaserApprovalIsPresent() |
| throws Exception { |
| allowVotingOnCodeReviewToAllUsers(); |
| |
| createVerifiedLabel(); |
| allowVotingOnVerifiedToAllUsers(); |
| |
| // Require a Code-Review approval from a non-uploader for submit. |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig() |
| .upsertSubmitRequirement( |
| SubmitRequirement.builder() |
| .setName(TestLabels.verified().getName()) |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| String.format("label:%s=MAX", TestLabels.verified().getName()))) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| u.getConfig() |
| .upsertSubmitRequirement( |
| SubmitRequirement.builder() |
| .setName(TestLabels.codeReview().getName()) |
| .setSubmittabilityExpression( |
| SubmitRequirementExpression.create( |
| String.format( |
| "label:%s=MAX,user=non_uploader", TestLabels.codeReview().getName()))) |
| .setAllowOverrideInChildProjects(false) |
| .build()); |
| u.save(); |
| } |
| |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| String uploaderEmail = "uploader@example.com"; |
| Account.Id uploader = accountOperations.newAccount().preferredEmail(uploaderEmail).create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes() |
| .id(changeToBeTheNewBase.get()) |
| .current() |
| .review(ReviewInput.approve().label(TestLabels.verified().getName(), 1)); |
| testMetricMaker.reset(); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(0); |
| |
| // Rebase it on behalf of the uploader |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| |
| // Approve the change as the rebaser. |
| gApi.changes() |
| .id(changeToBeRebased.get()) |
| .current() |
| .review(ReviewInput.approve().label(TestLabels.verified().getName(), 1)); |
| |
| // Approve the change as another user. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeRebased.get()).current().review(ReviewInput.approve()); |
| |
| // Due to the second approval the change would also be submittable if the approval of the |
| // rebaser would be ignored due to the rebaser being the uploader. |
| allowPermissionToAllUsers(Permission.SUBMIT); |
| testMetricMaker.reset(); |
| gApi.changes().id(changeToBeRebased.get()).current().submit(); |
| assertThat(testMetricMaker.getCount("change/submitted_with_rebaser_approval")).isEqualTo(0); |
| } |
| |
| @Test |
| public void testCountRebasesMetric() throws Exception { |
| allowPermissionToAllUsers(Permission.REBASE); |
| |
| Account.Id uploader = accountOperations.newAccount().create(); |
| Account.Id approver = admin.id(); |
| Account.Id rebaser = accountOperations.newAccount().create(); |
| |
| // Create two changes both with the same parent |
| requestScopeOperations.setApiUser(uploader); |
| Change.Id changeToBeTheNewBase = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| Change.Id changeToBeRebased = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| |
| // Approve and submit the change that will be the new base for the change that will be rebased. |
| requestScopeOperations.setApiUser(approver); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase.get()).current().submit(); |
| |
| // Rebase it on behalf of the uploader |
| testMetricMaker.reset(); |
| requestScopeOperations.setApiUser(rebaser); |
| RebaseInput rebaseInput = new RebaseInput(); |
| rebaseInput.onBehalfOfUploader = true; |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(1); |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(0); |
| |
| // Create and submit another change so that we can rebase the change once again. |
| requestScopeOperations.setApiUser(approver); |
| Change.Id changeToBeTheNewBase2 = |
| changeOperations.newChange().project(project).owner(uploader).create(); |
| gApi.changes().id(changeToBeTheNewBase2.get()).current().review(ReviewInput.approve()); |
| gApi.changes().id(changeToBeTheNewBase2.get()).current().submit(); |
| |
| // Rebase the change once again, this time as the uploader. |
| // If the uploader sets on_behalf_of_uploader = true, the flag is ignored and a normal rebase is |
| // done, hence the metric should count this as a a rebase with on_behalf_of_uploader = false. |
| requestScopeOperations.setApiUser(uploader); |
| testMetricMaker.reset(); |
| gApi.changes().id(changeToBeRebased.get()).rebase(rebaseInput); |
| // field1 is on_behalf_of_uploader, field2 is rebase_chain, field3 is allow_conflicts |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, false, false)).isEqualTo(1); |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, false, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", false, true, false)).isEqualTo(0); |
| assertThat(testMetricMaker.getCount("change/count_rebases", true, true, false)).isEqualTo(0); |
| } |
| |
| private void allowPermissionToAllUsers(String permission) { |
| allowPermission(permission, REGISTERED_USERS); |
| } |
| |
| private void allowPermission(String permission, AccountGroup.UUID groupUuid) { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(permission).ref("refs/*").group(groupUuid)) |
| .update(); |
| } |
| |
| private void allowVotingOnCodeReviewToAllUsers() { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(TestLabels.codeReview().getName()) |
| .ref("refs/*") |
| .group(REGISTERED_USERS) |
| .range(-2, 2)) |
| .update(); |
| } |
| |
| private void createVerifiedLabel() throws Exception { |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| LabelType.Builder verified = |
| labelBuilder( |
| LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed")) |
| .setCopyCondition("is:MIN"); |
| u.getConfig().upsertLabelType(verified.build()); |
| u.save(); |
| } |
| } |
| |
| private void allowVotingOnVerifiedToAllUsers() { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add( |
| allowLabel(TestLabels.verified().getName()) |
| .ref("refs/*") |
| .group(REGISTERED_USERS) |
| .range(-1, 1)) |
| .update(); |
| } |
| |
| private void blockPermissionForAllUsers(String permission) { |
| blockPermission(permission, REGISTERED_USERS); |
| } |
| |
| private void blockPermission(String permission, AccountGroup.UUID groupUuid) { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(block(permission).ref("refs/*").group(groupUuid)) |
| .update(); |
| } |
| |
| @FunctionalInterface |
| private interface RebaseCall { |
| void call(Change.Id changeId, RebaseInput rebaseInput) throws RestApiException; |
| } |
| |
| private static class TestRevisionCreatedListener implements RevisionCreatedListener { |
| public RevisionInfo revisionInfo; |
| |
| @Override |
| public void onRevisionCreated(Event event) { |
| revisionInfo = event.getRevision(); |
| } |
| } |
| } |