| // Copyright (C) 2013 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.common.truth.TruthJUnit.assume; |
| import static com.google.gerrit.acceptance.GitUtil.assertPushOk; |
| import static com.google.gerrit.acceptance.GitUtil.pushHead; |
| import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; |
| import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; |
| import static com.google.gerrit.extensions.client.ReviewerState.CC; |
| import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; |
| import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; |
| import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; |
| import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER; |
| import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; |
| import static com.google.gerrit.server.project.Util.blockLabel; |
| import static com.google.gerrit.server.project.Util.category; |
| import static com.google.gerrit.server.project.Util.value; |
| import static java.util.concurrent.TimeUnit.SECONDS; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.base.Function; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.AcceptanceTestRequestScope; |
| import com.google.gerrit.acceptance.GerritConfig; |
| import com.google.gerrit.acceptance.GerritConfigs; |
| import com.google.gerrit.acceptance.GitUtil; |
| import com.google.gerrit.acceptance.NoHttpd; |
| import com.google.gerrit.acceptance.PushOneCommit; |
| import com.google.gerrit.acceptance.TestProjectInput; |
| import com.google.gerrit.common.data.LabelType; |
| import com.google.gerrit.common.data.Permission; |
| import com.google.gerrit.extensions.api.changes.AddReviewerInput; |
| import com.google.gerrit.extensions.api.changes.DeleteVoteInput; |
| import com.google.gerrit.extensions.api.changes.NotifyHandling; |
| import com.google.gerrit.extensions.api.changes.RebaseInput; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.api.changes.RevisionApi; |
| import com.google.gerrit.extensions.api.projects.BranchInput; |
| import com.google.gerrit.extensions.client.ChangeKind; |
| import com.google.gerrit.extensions.client.ChangeStatus; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.client.SubmitType; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.extensions.common.ApprovalInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.ChangeInput; |
| import com.google.gerrit.extensions.common.ChangeMessageInfo; |
| import com.google.gerrit.extensions.common.GitPerson; |
| import com.google.gerrit.extensions.common.LabelInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.AccountGroup; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.client.RefNames; |
| import com.google.gerrit.server.change.ChangeResource; |
| import com.google.gerrit.server.config.AnonymousCowardNameProvider; |
| import com.google.gerrit.server.git.ProjectConfig; |
| import com.google.gerrit.server.group.SystemGroupBackend; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gerrit.server.project.Util; |
| import com.google.gerrit.testutil.FakeEmailSender.Message; |
| import com.google.gerrit.testutil.NoteDbMode; |
| import com.google.gerrit.testutil.TestTimeUtil; |
| |
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.transport.PushResult; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.EnumSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| @NoHttpd |
| public class ChangeIT extends AbstractDaemonTest { |
| private String systemTimeZone; |
| |
| @Before |
| public void setTimeForTesting() { |
| systemTimeZone = System.setProperty("user.timezone", "US/Eastern"); |
| } |
| |
| @After |
| public void resetTime() { |
| TestTimeUtil.useSystemTime(); |
| System.setProperty("user.timezone", systemTimeZone); |
| } |
| |
| @Test |
| public void get() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| ChangeInfo c = info(triplet); |
| assertThat(c.id).isEqualTo(triplet); |
| assertThat(c.project).isEqualTo(project.get()); |
| assertThat(c.branch).isEqualTo("master"); |
| assertThat(c.status).isEqualTo(ChangeStatus.NEW); |
| assertThat(c.subject).isEqualTo("test commit"); |
| assertThat(c.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY); |
| assertThat(c.mergeable).isTrue(); |
| assertThat(c.changeId).isEqualTo(r.getChangeId()); |
| assertThat(c.created).isEqualTo(c.updated); |
| assertThat(c._number).isEqualTo(r.getChange().getId().get()); |
| |
| assertThat(c.owner._accountId).isEqualTo(admin.getId().get()); |
| assertThat(c.owner.name).isNull(); |
| assertThat(c.owner.email).isNull(); |
| assertThat(c.owner.username).isNull(); |
| assertThat(c.owner.avatars).isNull(); |
| } |
| |
| @Test |
| public void getAmbiguous() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| String changeId = r1.getChangeId(); |
| gApi.changes().id(changeId).get(); |
| |
| BranchInput b = new BranchInput(); |
| b.revision = repo().exactRef("HEAD").getObjectId().name(); |
| gApi.projects() |
| .name(project.get()) |
| .branch("other") |
| .create(b); |
| |
| PushOneCommit push2 = pushFactory.create(db, admin.getIdent(), testRepo, |
| PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, |
| PushOneCommit.FILE_CONTENT, changeId); |
| PushOneCommit.Result r2 = push2.to("refs/for/other"); |
| assertThat(r2.getChangeId()).isEqualTo(changeId); |
| |
| exception.expect(ResourceNotFoundException.class); |
| exception.expectMessage("Multiple changes found for " + changeId); |
| gApi.changes().id(changeId).get(); |
| } |
| |
| @Test |
| public void abandon() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| gApi.changes() |
| .id(changeId) |
| .abandon(); |
| ChangeInfo info = get(changeId); |
| assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); |
| assertThat(Iterables.getLast(info.messages).message.toLowerCase()) |
| .contains("abandoned"); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("change is abandoned"); |
| gApi.changes() |
| .id(changeId) |
| .abandon(); |
| } |
| |
| @Test |
| public void abandonDraft() throws Exception { |
| PushOneCommit.Result r = createDraftChange(); |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.DRAFT); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("draft changes cannot be abandoned"); |
| gApi.changes() |
| .id(changeId) |
| .abandon(); |
| } |
| |
| @Test |
| public void restore() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| gApi.changes() |
| .id(changeId) |
| .abandon(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED); |
| |
| gApi.changes() |
| .id(changeId) |
| .restore(); |
| ChangeInfo info = get(changeId); |
| assertThat(info.status).isEqualTo(ChangeStatus.NEW); |
| assertThat(Iterables.getLast(info.messages).message.toLowerCase()) |
| .contains("restored"); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("change is new"); |
| gApi.changes() |
| .id(changeId) |
| .restore(); |
| } |
| |
| @Test |
| public void revert() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .submit(); |
| ChangeInfo revertChange = |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revert().get(); |
| |
| // expected messages on source change: |
| // 1. Uploaded patch set 1. |
| // 2. Patch Set 1: Code-Review+2 |
| // 3. Change has been successfully merged by Administrator |
| // 4. Patch Set 1: Reverted |
| List<ChangeMessageInfo> sourceMessages = new ArrayList<>( |
| gApi.changes().id(r.getChangeId()).get().messages); |
| assertThat(sourceMessages).hasSize(4); |
| String expectedMessage = String.format( |
| "Created a revert of this change as %s", |
| revertChange.changeId); |
| assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage); |
| |
| assertThat(revertChange.messages).hasSize(1); |
| assertThat(revertChange.messages.iterator().next().message) |
| .isEqualTo("Uploaded patch set 1."); |
| } |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false) |
| public void revertInitialCommit() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .submit(); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("Cannot revert initial commit"); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revert(); |
| } |
| |
| @Test |
| public void rebase() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Approve and submit the first change |
| RevisionApi revision = gApi.changes() |
| .id(r.getChangeId()) |
| .current(); |
| revision.review(ReviewInput.approve()); |
| revision.submit(); |
| |
| String changeId = r2.getChangeId(); |
| // Rebase the second change |
| gApi.changes() |
| .id(changeId) |
| .current() |
| .rebase(); |
| |
| // Second change should have 2 patch sets |
| ChangeInfo c2 = gApi.changes().id(changeId).get(); |
| assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2); |
| |
| // ...and the committer should be correct |
| ChangeInfo info = gApi.changes() |
| .id(changeId).get(EnumSet.of( |
| ListChangesOption.CURRENT_REVISION, |
| ListChangesOption.CURRENT_COMMIT)); |
| GitPerson committer = info.revisions.get( |
| info.currentRevision).commit.committer; |
| assertThat(committer.name).isEqualTo(admin.fullName); |
| assertThat(committer.email).isEqualTo(admin.email); |
| |
| // Rebasing the second change again should fail |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("Change is already up to date"); |
| gApi.changes() |
| .id(changeId) |
| .current() |
| .rebase(); |
| } |
| |
| @Test |
| public void publish() throws Exception { |
| PushOneCommit.Result r = createChange("refs/drafts/master"); |
| assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .publish(); |
| assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.NEW); |
| } |
| |
| @Test |
| public void delete() throws Exception { |
| PushOneCommit.Result r = createChange("refs/drafts/master"); |
| assertThat(query(r.getChangeId())).hasSize(1); |
| assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .delete(); |
| assertThat(query(r.getChangeId())).isEmpty(); |
| } |
| |
| @Test |
| public void voteOnClosedChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| merge(r); |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("change is closed"); |
| revision(r).review(ReviewInput.reject()); |
| } |
| |
| @Test |
| public void voteOnBehalfOf() throws Exception { |
| ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); |
| LabelType codeReviewType = Util.codeReview(); |
| String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName()); |
| String heads = "refs/heads/*"; |
| AccountGroup.UUID owner = |
| SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID(); |
| Util.allow(cfg, forCodeReviewAs, -1, 1, owner, heads); |
| saveProjectConfig(project, cfg); |
| |
| PushOneCommit.Result r = createChange(); |
| RevisionApi revision = gApi.changes() |
| .id(r.getChangeId()) |
| .current(); |
| |
| ReviewInput in = ReviewInput.recommend(); |
| in.onBehalfOf = user.id.toString(); |
| revision.review(in); |
| |
| ChangeInfo c = gApi.changes() |
| .id(r.getChangeId()) |
| .get(); |
| |
| LabelInfo codeReview = c.labels.get("Code-Review"); |
| assertThat(codeReview.all).hasSize(1); |
| ApprovalInfo approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id.get()); |
| assertThat(approval.value).isEqualTo(1); |
| } |
| |
| @Test |
| public void rebaseUpToDateChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("Change is already up to date"); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .rebase(); |
| } |
| |
| @Test |
| public void rebaseConflict() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .submit(); |
| |
| PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, |
| PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, "other content", |
| "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5"); |
| r = push.to("refs/for/master"); |
| r.assertOkStatus(); |
| |
| exception.expect(ResourceConflictException.class); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .rebase(); |
| } |
| |
| @Test |
| public void rebaseChangeBase() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| PushOneCommit.Result r3 = createChange(); |
| RebaseInput ri = new RebaseInput(); |
| |
| // rebase r3 directly onto master (break dep. towards r2) |
| ri.base = ""; |
| gApi.changes() |
| .id(r3.getChangeId()) |
| .revision(r3.getCommit().name()) |
| .rebase(ri); |
| PatchSet ps3 = r3.getPatchSet(); |
| assertThat(ps3.getId().get()).isEqualTo(2); |
| |
| // rebase r2 onto r3 (referenced by ref) |
| ri.base = ps3.getId().toRefName(); |
| gApi.changes() |
| .id(r2.getChangeId()) |
| .revision(r2.getCommit().name()) |
| .rebase(ri); |
| PatchSet ps2 = r2.getPatchSet(); |
| assertThat(ps2.getId().get()).isEqualTo(2); |
| |
| // rebase r1 onto r2 (referenced by commit) |
| ri.base = ps2.getRevision().get(); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .rebase(ri); |
| PatchSet ps1 = r1.getPatchSet(); |
| assertThat(ps1.getId().get()).isEqualTo(2); |
| |
| // rebase r1 onto r3 (referenced by change number) |
| ri.base = String.valueOf(r3.getChange().getId().get()); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(ps1.getRevision().get()) |
| .rebase(ri); |
| assertThat(r1.getPatchSetId().get()).isEqualTo(3); |
| } |
| |
| @Test |
| public void rebaseChangeBaseRecursion() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| |
| RebaseInput ri = new RebaseInput(); |
| ri.base = r2.getCommit().name(); |
| String expectedMessage = "base change " + r2.getChangeId() |
| + " is a descendant of the current change - recursion not allowed"; |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage(expectedMessage); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .rebase(ri); |
| } |
| |
| @Test |
| public void rebaseAbandonedChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| gApi.changes() |
| .id(changeId) |
| .abandon(); |
| ChangeInfo info = get(changeId); |
| assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("change is abandoned"); |
| gApi.changes() |
| .id(changeId) |
| .revision(r.getCommit().name()) |
| .rebase(); |
| } |
| |
| @Test |
| public void rebaseOntoAbandonedChange() throws Exception { |
| // Create two changes both with the same parent |
| PushOneCommit.Result r = createChange(); |
| testRepo.reset("HEAD~1"); |
| PushOneCommit.Result r2 = createChange(); |
| |
| // Abandon the first change |
| String changeId = r.getChangeId(); |
| assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); |
| gApi.changes() |
| .id(changeId) |
| .abandon(); |
| ChangeInfo info = get(changeId); |
| assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); |
| |
| RebaseInput ri = new RebaseInput(); |
| ri.base = r.getCommit().name(); |
| |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("base change is abandoned: " + changeId); |
| gApi.changes() |
| .id(r2.getChangeId()) |
| .revision(r2.getCommit().name()) |
| .rebase(ri); |
| } |
| |
| @Test |
| public void rebaseOntoSelf() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| String commit = r.getCommit().name(); |
| RebaseInput ri = new RebaseInput(); |
| ri.base = commit; |
| exception.expect(ResourceConflictException.class); |
| exception.expectMessage("cannot rebase change onto itself"); |
| gApi.changes() |
| .id(changeId) |
| .revision(commit) |
| .rebase(ri); |
| } |
| |
| @Test |
| @TestProjectInput(createEmptyCommit = false) |
| public void changeNoParentToOneParent() throws Exception { |
| // create initial commit with no parent and push it as change, so that patch |
| // set 1 has no parent |
| RevCommit c = |
| testRepo.commit().message("Initial commit").insertChangeId().create(); |
| String id = GitUtil.getChangeId(testRepo, c).get(); |
| testRepo.reset(c); |
| |
| PushResult pr = pushHead(testRepo, "refs/for/master", false); |
| assertPushOk(pr, "refs/for/master"); |
| |
| ChangeInfo change = gApi.changes().id(id).get(); |
| assertThat(change.revisions.get(change.currentRevision).commit.parents) |
| .isEmpty(); |
| |
| // create another initial commit with no parent and push it directly into |
| // the remote repository |
| c = testRepo.amend(c.getId()).message("Initial Empty Commit").create(); |
| testRepo.reset(c); |
| pr = pushHead(testRepo, "refs/heads/master", false); |
| assertPushOk(pr, "refs/heads/master"); |
| |
| // create a successor commit and push it as second patch set to the change, |
| // so that patch set 2 has 1 parent |
| RevCommit c2 = testRepo.commit().message("Initial commit").parent(c) |
| .insertChangeId(id.substring(1)).create(); |
| testRepo.reset(c2); |
| |
| pr = pushHead(testRepo, "refs/for/master", false); |
| assertPushOk(pr, "refs/for/master"); |
| |
| change = gApi.changes().id(id).get(); |
| RevisionInfo rev = change.revisions.get(change.currentRevision); |
| assertThat(rev.commit.parents).hasSize(1); |
| assertThat(rev.commit.parents.get(0).commit).isEqualTo(c.name()); |
| |
| // check that change kind is correctly detected as REWORK |
| assertThat(rev.kind).isEqualTo(ChangeKind.REWORK); |
| } |
| |
| @Test |
| public void addReviewerThatCannotSeeChange() throws Exception { |
| // create hidden project that is only visible to administrators |
| Project.NameKey p = createProject("p"); |
| ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); |
| Util.allow(cfg, |
| Permission.READ, |
| groupCache.get(new AccountGroup.NameKey("Administrators")) |
| .getGroupUUID(), |
| "refs/*"); |
| Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); |
| saveProjectConfig(p, cfg); |
| |
| // create change |
| TestRepository<InMemoryRepository> repo = cloneProject(p, admin); |
| PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo); |
| PushOneCommit.Result result = push.to("refs/for/master"); |
| result.assertOkStatus(); |
| |
| // check the user cannot see the change |
| setApiUser(user); |
| try { |
| gApi.changes().id(result.getChangeId()).get(); |
| fail("Expected ResourceNotFoundException"); |
| } catch (ResourceNotFoundException e) { |
| // Expected. |
| } |
| |
| // try to add user as reviewer |
| setApiUser(admin); |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| exception.expect(UnprocessableEntityException.class); |
| exception.expectMessage("Change not visible to " + user.email); |
| gApi.changes() |
| .id(result.getChangeId()) |
| .addReviewer(in); |
| } |
| |
| @Test |
| public void addReviewer() throws Exception { |
| TestTimeUtil.resetWithClockStep(1, SECONDS); |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| Timestamp oldTs = rsrc.getChange().getLastUpdatedOn(); |
| |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(r.getChangeId()) |
| .addReviewer(in); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message m = messages.get(0); |
| assertThat(m.rcpt()).containsExactly(user.emailAddress); |
| assertThat(m.body()).contains("Hello " + user.fullName + ",\n"); |
| assertThat(m.body()).contains("I'd like you to do a code review."); |
| assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); |
| assertMailFrom(m, admin.email); |
| ChangeInfo c = gApi.changes() |
| .id(r.getChangeId()) |
| .get(); |
| |
| // When NoteDb is enabled adding a reviewer records that user as reviewer |
| // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0 |
| // approval on the change which is treated as CC when the ChangeInfo is |
| // created. |
| Collection<AccountInfo> reviewers = NoteDbMode.readWrite() |
| ? c.reviewers.get(REVIEWER) |
| : c.reviewers.get(CC); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId) |
| .isEqualTo(user.getId().get()); |
| |
| // Ensure ETag and lastUpdatedOn are updated. |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); |
| } |
| |
| @Test |
| public void addSelfAsReviewer() throws Exception { |
| TestTimeUtil.resetWithClockStep(1, SECONDS); |
| PushOneCommit.Result r = createChange(); |
| ChangeResource rsrc = parseResource(r); |
| String oldETag = rsrc.getETag(); |
| Timestamp oldTs = rsrc.getChange().getLastUpdatedOn(); |
| |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| setApiUser(user); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .addReviewer(in); |
| |
| // There should be no email notification when adding self |
| assertThat(sender.getMessages()).isEmpty(); |
| |
| // When NoteDb is enabled adding a reviewer records that user as reviewer |
| // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0 |
| // approval on the change which is treated as CC when the ChangeInfo is |
| // created. |
| ChangeInfo c = gApi.changes() |
| .id(r.getChangeId()) |
| .get(); |
| Collection<AccountInfo> reviewers = NoteDbMode.readWrite() |
| ? c.reviewers.get(REVIEWER) |
| : c.reviewers.get(CC); |
| assertThat(reviewers).isNotNull(); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId) |
| .isEqualTo(user.getId().get()); |
| |
| // Ensure ETag and lastUpdatedOn are updated. |
| rsrc = parseResource(r); |
| assertThat(rsrc.getETag()).isNotEqualTo(oldETag); |
| assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); |
| } |
| |
| @Test |
| public void addReviewerToClosedChange() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .submit(); |
| |
| ChangeInfo c = gApi.changes() |
| .id(r.getChangeId()) |
| .get(); |
| Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId) |
| .isEqualTo(admin.getId().get()); |
| assertThat(c.reviewers).doesNotContainKey(CC); |
| |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(r.getChangeId()) |
| .addReviewer(in); |
| |
| c = gApi.changes() |
| .id(r.getChangeId()) |
| .get(); |
| reviewers = c.reviewers.get(REVIEWER); |
| if (NoteDbMode.readWrite()) { |
| // When NoteDb is enabled adding a reviewer records that user as reviewer |
| // in NoteDb. |
| assertThat(reviewers).hasSize(2); |
| Iterator<AccountInfo> reviewerIt = reviewers.iterator(); |
| assertThat(reviewerIt.next()._accountId) |
| .isEqualTo(admin.getId().get()); |
| assertThat(reviewerIt.next()._accountId) |
| .isEqualTo(user.getId().get()); |
| assertThat(c.reviewers).doesNotContainKey(CC); |
| } else { |
| // When NoteDb is disabled adding a reviewer results in a dummy 0 approval |
| // on the change which is treated as CC when the ChangeInfo is created. |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId) |
| .isEqualTo(admin.getId().get()); |
| Collection<AccountInfo> ccs = c.reviewers.get(CC); |
| assertThat(ccs).hasSize(1); |
| assertThat(ccs.iterator().next()._accountId) |
| .isEqualTo(user.getId().get()); |
| } |
| } |
| |
| @Test |
| public void listVotes() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| Map<String, Short> m = gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(admin.getId().toString()) |
| .votes(); |
| |
| assertThat(m).hasSize(1); |
| assertThat(m).containsEntry("Code-Review", Short.valueOf((short)2)); |
| |
| setApiUser(user); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.dislike()); |
| |
| m = gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(user.getId().toString()) |
| .votes(); |
| |
| assertThat(m).hasSize(1); |
| assertThat(m).containsEntry("Code-Review", Short.valueOf((short)-1)); |
| } |
| |
| @Test |
| public void removeReviewerNoVotes() throws Exception { |
| ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); |
| |
| LabelType verified = category("Verified", value(1, "Passes"), |
| value(0, "No score"), value(-1, "Failed")); |
| cfg.getLabelSections().put(verified.getName(), verified); |
| |
| AccountGroup.UUID registeredUsers = |
| SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); |
| String heads = RefNames.REFS_HEADS + "*"; |
| Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1, |
| registeredUsers, heads); |
| saveProjectConfig(project, cfg); |
| |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes() |
| .id(changeId) |
| .addReviewer(user.getId().toString()); |
| |
| // ReviewerState will vary between ReviewDb and NoteDb; we just care that it |
| // shows up somewhere. |
| Iterable<AccountInfo> reviewers = Iterables.concat( |
| gApi.changes().id(changeId).get().reviewers.values()); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId) |
| .isEqualTo(user.getId().get()); |
| |
| gApi.changes() |
| .id(changeId) |
| .reviewer(user.getId().toString()) |
| .remove(); |
| assertThat(gApi.changes().id(changeId).get().reviewers.isEmpty()); |
| |
| // Make sure the reviewer can still be added again. |
| gApi.changes() |
| .id(changeId) |
| .addReviewer(user.getId().toString()); |
| reviewers = Iterables.concat(gApi.changes().id(changeId).get().reviewers.values()); |
| assertThat(reviewers).hasSize(1); |
| assertThat(reviewers.iterator().next()._accountId) |
| .isEqualTo(user.getId().get()); |
| |
| // Remove again, and then try to remove once more to verify 404 is |
| // returned. |
| gApi.changes() |
| .id(changeId) |
| .reviewer(user.getId().toString()) |
| .remove(); |
| exception.expect(ResourceNotFoundException.class); |
| gApi.changes() |
| .id(changeId) |
| .reviewer(user.getId().toString()) |
| .remove(); |
| } |
| |
| @Test |
| public void removeReviewer() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes() |
| .id(changeId) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| setApiUser(user); |
| gApi.changes() |
| .id(changeId) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.recommend()); |
| |
| Collection<AccountInfo> reviewers = gApi.changes() |
| .id(changeId) |
| .get() |
| .reviewers.get(REVIEWER); |
| |
| assertThat(reviewers).hasSize(2); |
| Iterator<AccountInfo> reviewerIt = reviewers.iterator(); |
| assertThat(reviewerIt.next()._accountId) |
| .isEqualTo(admin.getId().get()); |
| assertThat(reviewerIt.next()._accountId) |
| .isEqualTo(user.getId().get()); |
| |
| setApiUser(admin); |
| gApi.changes() |
| .id(changeId) |
| .reviewer(user.getId().toString()) |
| .remove(); |
| |
| reviewers = gApi.changes() |
| .id(changeId) |
| .get() |
| .reviewers.get(REVIEWER); |
| assertThat(reviewers).hasSize(1); |
| reviewerIt = reviewers.iterator(); |
| assertThat(reviewerIt.next()._accountId) |
| .isEqualTo(admin.getId().get()); |
| |
| eventRecorder.assertReviewerDeletedEvents(changeId, user.email); |
| } |
| |
| @Test |
| public void removeReviewerNotPermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| gApi.changes() |
| .id(changeId) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| setApiUser(user); |
| exception.expect(AuthException.class); |
| exception.expectMessage("delete reviewer not permitted"); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(admin.getId().toString()) |
| .remove(); |
| } |
| |
| @Test |
| public void deleteVote() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| setApiUser(user); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.recommend()); |
| |
| setApiUser(admin); |
| sender.clear(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(user.getId().toString()) |
| .deleteVote("Code-Review"); |
| |
| List<Message> messages = sender.getMessages(); |
| assertThat(messages).hasSize(1); |
| Message msg = messages.get(0); |
| assertThat(msg.rcpt()).containsExactly(user.emailAddress); |
| assertThat(msg.body()).contains( |
| admin.fullName + " has removed a vote on this change.\n"); |
| assertThat(msg.body()).contains( |
| "Removed Code-Review+1 by " |
| + user.fullName + " <" + user.email + ">" + "\n"); |
| |
| Map<String, Short> m = gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(user.getId().toString()) |
| .votes(); |
| |
| if (NoteDbMode.readWrite()) { |
| // When NoteDb is enabled each reviewer is explicitly recorded in the |
| // NoteDb and this record stays even when all votes of that user have been |
| // deleted, hence there is no dummy 0 approval left when a vote is |
| // deleted. |
| assertThat(m).isEmpty(); |
| } else { |
| // When NoteDb is disabled there is a dummy 0 approval on the change so |
| // that the user is still returned as CC when all votes of that user have |
| // been deleted. |
| assertThat(m).containsEntry("Code-Review", Short.valueOf((short)0)); |
| } |
| |
| ChangeInfo c = gApi.changes() |
| .id(r.getChangeId()) |
| .get(); |
| |
| ChangeMessageInfo message = Iterables.getLast(c.messages); |
| assertThat(message.author._accountId).isEqualTo(admin.getId().get()); |
| assertThat(message.message).isEqualTo( |
| "Removed Code-Review+1 by User <user@example.com>\n"); |
| if (NoteDbMode.readWrite()) { |
| // When NoteDb is enabled each reviewer is explicitly recorded in the |
| // NoteDb and this record stays even when all votes of that user have been |
| // deleted. |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn( |
| ImmutableSet.of(admin.getId(), user.getId())); |
| } else { |
| // When NoteDb is disabled users that have only dummy 0 approvals on the |
| // change are returned as CC and not as REVIEWER. |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.getId())); |
| assertThat(getReviewers(c.reviewers.get(CC))) |
| .containsExactlyElementsIn(ImmutableSet.of(user.getId())); |
| } |
| } |
| |
| @Test |
| public void deleteVoteNotifyNone() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| setApiUser(user); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.recommend()); |
| |
| setApiUser(admin); |
| sender.clear(); |
| DeleteVoteInput in = new DeleteVoteInput(); |
| in.label = "Code-Review"; |
| in.notify = NotifyHandling.NONE; |
| gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(user.getId().toString()) |
| .deleteVote(in); |
| assertThat(sender.getMessages()).hasSize(0); |
| } |
| |
| @Test |
| public void deleteVoteNotPermitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| |
| setApiUser(user); |
| exception.expect(AuthException.class); |
| exception.expectMessage("delete vote not permitted"); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .reviewer(admin.getId().toString()) |
| .deleteVote("Code-Review"); |
| } |
| |
| @Test |
| public void nonVotingReviewerStaysAfterSubmit() throws Exception { |
| LabelType verified = category("Verified", |
| value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); |
| cfg.getLabelSections().put(verified.getName(), verified); |
| String heads = "refs/heads/*"; |
| AccountGroup.UUID owners = |
| SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID(); |
| AccountGroup.UUID registered = |
| SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); |
| Util.allow(cfg, |
| Permission.forLabel(verified.getName()), -1, 1, owners, heads); |
| Util.allow(cfg, |
| Permission.forLabel("Code-Review"), -2, +2, registered, heads); |
| saveProjectConfig(project, cfg); |
| |
| // Set Code-Review+2 and Verified+1 as admin (change owner) |
| PushOneCommit.Result r = createChange(); |
| String changeId = r.getChangeId(); |
| String commit = r.getCommit().name(); |
| ReviewInput input = ReviewInput.approve(); |
| input.label(verified.getName(), 1); |
| gApi.changes() |
| .id(changeId) |
| .revision(commit) |
| .review(input); |
| |
| // Reviewers should only be "admin" |
| ChangeInfo c = gApi.changes().id(changeId).get(); |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.getId())); |
| assertThat(c.reviewers.get(CC)).isNull(); |
| |
| // Add the user as reviewer |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(changeId) |
| .addReviewer(in); |
| c = gApi.changes().id(changeId).get(); |
| if (NoteDbMode.readWrite()) { |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of( |
| admin.getId(), user.getId())); |
| } else { |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.getId())); |
| assertThat(getReviewers(c.reviewers.get(CC))) |
| .containsExactlyElementsIn(ImmutableSet.of(user.getId())); |
| } |
| |
| // Approve the change as user, then remove the approval |
| // (only to confirm that the user does have Code-Review+2 permission) |
| setApiUser(user); |
| gApi.changes() |
| .id(changeId) |
| .revision(commit) |
| .review(ReviewInput.approve()); |
| gApi.changes() |
| .id(changeId) |
| .revision(commit) |
| .review(ReviewInput.noScore()); |
| |
| // Submit the change |
| setApiUser(admin); |
| gApi.changes() |
| .id(changeId) |
| .revision(commit) |
| .submit(); |
| |
| // User should still be on the change |
| c = gApi.changes().id(changeId).get(); |
| if (NoteDbMode.readWrite()) { |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of( |
| admin.getId(), user.getId())); |
| } else { |
| assertThat(getReviewers(c.reviewers.get(REVIEWER))) |
| .containsExactlyElementsIn(ImmutableSet.of(admin.getId())); |
| assertThat(getReviewers(c.reviewers.get(CC))) |
| .containsExactlyElementsIn(ImmutableSet.of(user.getId())); |
| } |
| } |
| |
| @Test |
| public void createEmptyChange() throws Exception { |
| ChangeInput in = new ChangeInput(); |
| in.branch = Constants.MASTER; |
| in.subject = "Create a change from the API"; |
| in.project = project.get(); |
| ChangeInfo info = gApi |
| .changes() |
| .create(in) |
| .get(); |
| assertThat(info.project).isEqualTo(in.project); |
| assertThat(info.branch).isEqualTo(in.branch); |
| assertThat(info.subject).isEqualTo(in.subject); |
| assertThat(Iterables.getOnlyElement(info.messages).message) |
| .isEqualTo("Uploaded patch set 1."); |
| } |
| |
| @Test |
| public void queryChangesNoQuery() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| List<ChangeInfo> results = gApi.changes().query().get(); |
| assertThat(results.size()).isAtLeast(1); |
| List<Integer> ids = new ArrayList<>(results.size()); |
| for (int i = 0; i < results.size(); i++) { |
| ChangeInfo info = results.get(i); |
| if (i == 0) { |
| assertThat(info._number).isEqualTo(r.getChange().getId().get()); |
| } |
| assertThat(Change.Status.forChangeStatus(info.status).isOpen()).isTrue(); |
| ids.add(info._number); |
| } |
| assertThat(ids).contains(r.getChange().getId().get()); |
| } |
| |
| @Test |
| public void queryChangesNoResults() throws Exception { |
| createChange(); |
| assertThat(query("message:test")).isNotEmpty(); |
| assertThat(query("message:{" + getClass().getName() + "fhqwhgads}")) |
| .isEmpty(); |
| } |
| |
| @Test |
| public void queryChanges() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| createChange(); |
| List<ChangeInfo> results = |
| query("project:{" + project.get() + "} " + r1.getChangeId()); |
| assertThat(Iterables.getOnlyElement(results).changeId) |
| .isEqualTo(r1.getChangeId()); |
| } |
| |
| @Test |
| public void queryChangesLimit() throws Exception { |
| createChange(); |
| PushOneCommit.Result r2 = createChange(); |
| List<ChangeInfo> results = gApi.changes().query().withLimit(1).get(); |
| assertThat(results).hasSize(1); |
| assertThat(Iterables.getOnlyElement(results).changeId) |
| .isEqualTo(r2.getChangeId()); |
| } |
| |
| @Test |
| public void queryChangesStart() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| createChange(); |
| List<ChangeInfo> results = gApi.changes() |
| .query("project:{" + project.get() + "}").withStart(1).get(); |
| assertThat(Iterables.getOnlyElement(results).changeId) |
| .isEqualTo(r1.getChangeId()); |
| } |
| |
| @Test |
| public void queryChangesNoOptions() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId())); |
| assertThat(result.labels).isNull(); |
| assertThat(result.messages).isNull(); |
| assertThat(result.revisions).isNull(); |
| assertThat(result.actions).isNull(); |
| } |
| |
| @Test |
| public void queryChangesOptions() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| |
| ChangeInfo result = Iterables.getOnlyElement(gApi.changes() |
| .query(r.getChangeId()) |
| .get()); |
| assertThat(result.labels).isNull(); |
| assertThat(result.messages).isNull(); |
| assertThat(result.actions).isNull(); |
| assertThat(result.revisions).isNull(); |
| |
| EnumSet<ListChangesOption> options = EnumSet.of( |
| ListChangesOption.ALL_REVISIONS, |
| ListChangesOption.CHANGE_ACTIONS, |
| ListChangesOption.CURRENT_ACTIONS, |
| ListChangesOption.DETAILED_LABELS, |
| ListChangesOption.MESSAGES); |
| result = Iterables.getOnlyElement(gApi.changes() |
| .query(r.getChangeId()) |
| .withOptions(options) |
| .get()); |
| assertThat(Iterables.getOnlyElement(result.labels.keySet())) |
| .isEqualTo("Code-Review"); |
| assertThat(result.messages).hasSize(1); |
| assertThat(result.actions).isNotEmpty(); |
| |
| RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values()); |
| assertThat(rev._number).isEqualTo(r.getPatchSetId().get()); |
| assertThat(rev.created).isNotNull(); |
| assertThat(rev.uploader._accountId).isEqualTo(admin.getId().get()); |
| assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName()); |
| assertThat(rev.actions).isNotEmpty(); |
| } |
| |
| @Test |
| public void queryChangesOwnerWithDifferentUsers() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(Iterables.getOnlyElement( |
| query("project:{" + project.get() + "} owner:self")).changeId) |
| .isEqualTo(r.getChangeId()); |
| setApiUser(user); |
| assertThat(query("owner:self")).isEmpty(); |
| } |
| |
| @Test |
| public void checkReviewedFlagBeforeAndAfterReview() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(r.getChangeId()) |
| .addReviewer(in); |
| |
| setApiUser(user); |
| assertThat(get(r.getChangeId()).reviewed).isNull(); |
| |
| revision(r).review(ReviewInput.recommend()); |
| assertThat(get(r.getChangeId()).reviewed).isTrue(); |
| } |
| |
| @Test |
| public void topic() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .topic()).isEqualTo(""); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .topic("mytopic"); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .topic()).isEqualTo("mytopic"); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .topic(""); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .topic()).isEqualTo(""); |
| } |
| |
| @Test |
| public void submitted() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .review(ReviewInput.approve()); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .info().submitted).isNull(); |
| gApi.changes() |
| .id(r.getChangeId()) |
| .revision(r.getCommit().name()) |
| .submit(); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .info().submitted).isNotNull(); |
| } |
| |
| @Test |
| public void check() throws Exception { |
| // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb. |
| assume().that(notesMigration.enabled()).isFalse(); |
| PushOneCommit.Result r = createChange(); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .get() |
| .problems).isNull(); |
| assertThat(gApi.changes() |
| .id(r.getChangeId()) |
| .get(EnumSet.of(ListChangesOption.CHECK)) |
| .problems).isEmpty(); |
| } |
| |
| @Test |
| public void commitFooters() throws Exception { |
| LabelType verified = category("Verified", |
| value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); |
| LabelType custom1 = category("Custom1", |
| value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); |
| LabelType custom2 = category("Custom2", |
| value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); |
| ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); |
| cfg.getLabelSections().put(verified.getName(), verified); |
| cfg.getLabelSections().put(custom1.getName(), custom1); |
| cfg.getLabelSections().put(custom2.getName(), custom2); |
| String heads = "refs/heads/*"; |
| AccountGroup.UUID anon = |
| SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); |
| Util.allow(cfg, Permission.forLabel("Verified"), -1, 1, anon, heads); |
| Util.allow(cfg, Permission.forLabel("Custom1"), -1, 1, anon, heads); |
| Util.allow(cfg, Permission.forLabel("Custom2"), -1, 1, anon, heads); |
| saveProjectConfig(project, cfg); |
| |
| PushOneCommit.Result r1 = createChange(); |
| r1.assertOkStatus(); |
| PushOneCommit.Result r2 = pushFactory.create( |
| db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", |
| r1.getChangeId()) |
| .to("refs/for/master"); |
| r2.assertOkStatus(); |
| |
| ReviewInput in = new ReviewInput(); |
| in.label("Code-Review", 1); |
| in.label("Verified", 1); |
| in.label("Custom1", -1); |
| in.label("Custom2", 1); |
| gApi.changes().id(r2.getChangeId()).current().review(in); |
| |
| EnumSet<ListChangesOption> options = EnumSet.of( |
| ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS); |
| ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(options); |
| assertThat(actual.revisions).hasSize(2); |
| |
| // No footers except on latest patch set. |
| assertThat(actual.revisions.get(r1.getCommit().getName()).commitWithFooters) |
| .isNull(); |
| |
| List<String> footers = |
| new ArrayList<>(Arrays.asList( |
| actual.revisions.get(r2.getCommit().getName()) |
| .commitWithFooters.split("\\n"))); |
| // remove subject + blank line |
| footers.remove(0); |
| footers.remove(0); |
| |
| List<String> expectedFooters = Arrays.asList( |
| "Change-Id: " + r2.getChangeId(), |
| "Reviewed-on: " |
| + canonicalWebUrl.get() + r2.getChange().getId(), |
| "Reviewed-by: Administrator <admin@example.com>", |
| "Custom2: Administrator <admin@example.com>", |
| "Tested-by: Administrator <admin@example.com>"); |
| |
| assertThat(footers).containsExactlyElementsIn(expectedFooters); |
| } |
| |
| @Test |
| public void defaultSearchDoesNotTouchDatabase() throws Exception { |
| setApiUser(admin); |
| PushOneCommit.Result r1 = createChange(); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .review(ReviewInput.approve()); |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .revision(r1.getCommit().name()) |
| .submit(); |
| |
| createChange(); |
| createDraftChange(); |
| |
| setApiUser(user); |
| AcceptanceTestRequestScope.Context ctx = disableDb(); |
| try { |
| assertThat(gApi.changes().query() |
| .withQuery( |
| "project:{" + project.get() + "} (status:open OR status:closed)") |
| // Options should match defaults in AccountDashboardScreen. |
| .withOption(ListChangesOption.LABELS) |
| .withOption(ListChangesOption.DETAILED_ACCOUNTS) |
| .withOption(ListChangesOption.REVIEWED) |
| .get()) |
| .hasSize(2); |
| } finally { |
| enableDb(ctx); |
| } |
| } |
| |
| @Test |
| public void votable() throws Exception { |
| PushOneCommit.Result r = createChange(); |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| gApi.changes().id(triplet).addReviewer(user.username); |
| ChangeInfo c = gApi.changes().id(triplet).get(EnumSet.of( |
| ListChangesOption.DETAILED_LABELS)); |
| LabelInfo codeReview = c.labels.get("Code-Review"); |
| assertThat(codeReview.all).hasSize(1); |
| ApprovalInfo approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id.get()); |
| assertThat(approval.value).isEqualTo(0); |
| |
| ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); |
| blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*"); |
| saveProjectConfig(project, cfg); |
| c = gApi.changes().id(triplet).get(EnumSet.of( |
| ListChangesOption.DETAILED_LABELS)); |
| codeReview = c.labels.get("Code-Review"); |
| assertThat(codeReview.all).hasSize(1); |
| approval = codeReview.all.get(0); |
| assertThat(approval._accountId).isEqualTo(user.id.get()); |
| assertThat(approval.value).isNull(); |
| } |
| |
| @Test |
| @GerritConfigs({ |
| @GerritConfig(name = "gerrit.editGpgKeys", value = "true"), |
| @GerritConfig(name = "receive.enableSignedPush", value = "true"), |
| }) |
| public void pushCertificates() throws Exception { |
| PushOneCommit.Result r1 = createChange(); |
| PushOneCommit.Result r2 = amendChange(r1.getChangeId()); |
| |
| ChangeInfo info = gApi.changes() |
| .id(r1.getChangeId()) |
| .get(EnumSet.of( |
| ListChangesOption.ALL_REVISIONS, |
| ListChangesOption.PUSH_CERTIFICATES)); |
| |
| RevisionInfo rev1 = info.revisions.get(r1.getCommit().name()); |
| assertThat(rev1).isNotNull(); |
| assertThat(rev1.pushCertificate).isNotNull(); |
| assertThat(rev1.pushCertificate.certificate).isNull(); |
| assertThat(rev1.pushCertificate.key).isNull(); |
| |
| RevisionInfo rev2 = info.revisions.get(r2.getCommit().name()); |
| assertThat(rev2).isNotNull(); |
| assertThat(rev2.pushCertificate).isNotNull(); |
| assertThat(rev2.pushCertificate.certificate).isNull(); |
| assertThat(rev2.pushCertificate.key).isNull(); |
| } |
| |
| @Test |
| public void anonymousRestApi() throws Exception { |
| setApiUserAnonymous(); |
| PushOneCommit.Result r = createChange(); |
| |
| ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); |
| assertThat(info.changeId).isEqualTo(r.getChangeId()); |
| |
| String triplet = project.get() + "~master~" + r.getChangeId(); |
| info = gApi.changes().id(triplet).get(); |
| assertThat(info.changeId).isEqualTo(r.getChangeId()); |
| |
| info = gApi.changes().id(info._number).get(); |
| assertThat(info.changeId).isEqualTo(r.getChangeId()); |
| |
| exception.expect(AuthException.class); |
| gApi.changes() |
| .id(triplet) |
| .current() |
| .review(ReviewInput.approve()); |
| } |
| |
| @Test |
| public void noteDbCommitsOnPatchSetCreation() throws Exception { |
| assume().that(notesMigration.readChanges()).isTrue(); |
| |
| PushOneCommit.Result r = createChange(); |
| pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, |
| "b.txt", "4711", r.getChangeId()).to("refs/for/master").assertOkStatus(); |
| ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); |
| try (Repository repo = repoManager.openRepository(project); |
| RevWalk rw = new RevWalk(repo)) { |
| RevCommit commitPatchSetCreation = rw.parseCommit( |
| repo.exactRef(changeMetaRef(new Change.Id(c._number))).getObjectId()); |
| |
| assertThat(commitPatchSetCreation.getShortMessage()) |
| .isEqualTo("Create patch set 2"); |
| PersonIdent expectedAuthor = changeNoteUtil.newIdent( |
| accountCache.get(admin.id).getAccount(), c.updated, |
| serverIdent.get(), AnonymousCowardNameProvider.DEFAULT); |
| assertThat(commitPatchSetCreation.getAuthorIdent()) |
| .isEqualTo(expectedAuthor); |
| assertThat(commitPatchSetCreation.getCommitterIdent()) |
| .isEqualTo(new PersonIdent(serverIdent.get(), c.updated)); |
| assertThat(commitPatchSetCreation.getParentCount()).isEqualTo(1); |
| |
| RevCommit commitChangeCreation = |
| rw.parseCommit(commitPatchSetCreation.getParent(0)); |
| assertThat(commitChangeCreation.getShortMessage()) |
| .isEqualTo("Create change"); |
| expectedAuthor = changeNoteUtil.newIdent( |
| accountCache.get(admin.id).getAccount(), c.created, serverIdent.get(), |
| AnonymousCowardNameProvider.DEFAULT); |
| assertThat(commitChangeCreation.getAuthorIdent()) |
| .isEqualTo(expectedAuthor); |
| assertThat(commitChangeCreation.getCommitterIdent()) |
| .isEqualTo(new PersonIdent(serverIdent.get(), c.created)); |
| assertThat(commitChangeCreation.getParentCount()).isEqualTo(0); |
| } |
| } |
| |
| @Test |
| public void createEmptyChangeOnNonExistingBranch() throws Exception { |
| ChangeInput in = new ChangeInput(); |
| in.branch = "foo"; |
| in.subject = "Create a change on new branch from the API"; |
| in.project = project.get(); |
| in.newBranch = true; |
| ChangeInfo info = gApi |
| .changes() |
| .create(in) |
| .get(); |
| assertThat(info.project).isEqualTo(in.project); |
| assertThat(info.branch).isEqualTo(in.branch); |
| assertThat(info.subject).isEqualTo(in.subject); |
| assertThat(Iterables.getOnlyElement(info.messages).message) |
| .isEqualTo("Uploaded patch set 1."); |
| } |
| |
| @Test |
| public void createEmptyChangeOnExistingBranchWithNewBranch() throws Exception { |
| ChangeInput in = new ChangeInput(); |
| in.branch = Constants.MASTER; |
| in.subject = "Create a change on new branch from the API"; |
| in.project = project.get(); |
| in.newBranch = true; |
| |
| exception.expect(ResourceConflictException.class); |
| gApi.changes() |
| .create(in) |
| .get(); |
| } |
| |
| @Test |
| public void createNewPatchSetOnVisibleDraftPatchSet() throws Exception { |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<InMemoryRepository> adminTestRepo = |
| cloneProject(project, admin); |
| TestRepository<InMemoryRepository> userTestRepo = |
| cloneProject(project, user); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create( |
| db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Amend draft as admin |
| PushOneCommit.Result r2 = amendChange( |
| r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo); |
| r2.assertOkStatus(); |
| |
| // Add user as reviewer to make this patch set visible |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .addReviewer(in); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r2.getPatchSet().getRefName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r3 = amendChange( |
| r2.getChangeId(), "refs/drafts/master", user, userTestRepo); |
| r3.assertOkStatus(); |
| } |
| |
| @Test |
| public void createNewPatchSetOnInvisibleDraftPatchSet() throws Exception { |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<InMemoryRepository> adminTestRepo = |
| cloneProject(project, admin); |
| TestRepository<InMemoryRepository> userTestRepo = |
| cloneProject(project, user); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create( |
| db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Amend draft as admin |
| PushOneCommit.Result r2 = amendChange( |
| r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo); |
| r2.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r3 = amendChange( |
| r1.getChangeId(), "refs/for/master", user, userTestRepo); |
| r3.assertErrorStatus("cannot add patch set to " |
| + r3.getChange().change().getChangeId() + "."); |
| } |
| |
| @Test |
| public void createNewPatchSetWithoutPermission() throws Exception { |
| // Create new project with clean permissions |
| Project.NameKey p = createProject("addPatchSet1"); |
| |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<InMemoryRepository> adminTestRepo = |
| cloneProject(p, admin); |
| TestRepository<InMemoryRepository> userTestRepo = |
| cloneProject(p, user); |
| |
| // Block default permission |
| block(Permission.ADD_PATCH_SET, |
| REGISTERED_USERS, "refs/for/*", p); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create( |
| db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r2 = |
| amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo); |
| r2.assertErrorStatus("cannot add patch set to " |
| + r1.getChange().getId().id + "."); |
| } |
| |
| @Test |
| public void createNewSetPatchWithPermission() throws Exception { |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<?> adminTestRepo = cloneProject(project, admin); |
| TestRepository<?> userTestRepo = cloneProject(project, user); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create( |
| db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r2 = amendChange( |
| r1.getChangeId(), "refs/for/master", user, userTestRepo); |
| r2.assertOkStatus(); |
| } |
| |
| @Test |
| public void createNewPatchSetAsOwnerWithoutPermission() throws Exception { |
| // Create new project with clean permissions |
| Project.NameKey p = createProject("addPatchSet2"); |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<?> adminTestRepo = cloneProject(project, admin); |
| |
| // Block default permission |
| block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p); |
| |
| // Create change as admin |
| PushOneCommit push = |
| pushFactory.create(db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/for/master"); |
| r1.assertOkStatus(); |
| |
| // Fetch change |
| GitUtil.fetch(adminTestRepo, r1.getPatchSet().getRefName() + ":ps"); |
| adminTestRepo.reset("ps"); |
| |
| // Amend change as admin |
| PushOneCommit.Result r2 = amendChange( |
| r1.getChangeId(), "refs/for/master", admin, adminTestRepo); |
| r2.assertOkStatus(); |
| } |
| |
| @Test |
| public void createNewPatchSetAsReviewerOnDraftChange() throws Exception { |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<?> adminTestRepo = cloneProject(project, admin); |
| TestRepository<?> userTestRepo = cloneProject(project, user); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create( |
| db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/drafts/master"); |
| r1.assertOkStatus(); |
| |
| // Add user as reviewer |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .addReviewer(in); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r2 = amendChange( |
| r1.getChangeId(), "refs/for/master", user, userTestRepo); |
| r2.assertOkStatus(); |
| } |
| |
| @Test |
| public void createNewDraftPatchSetOnDraftChange() throws Exception { |
| // Create new project with clean permissions |
| Project.NameKey p = createProject("addPatchSet4"); |
| // Clone separate repositories of the same project as admin and as user |
| TestRepository<?> adminTestRepo = cloneProject(p, admin); |
| TestRepository<?> userTestRepo = cloneProject(p, user); |
| |
| // Block default permission |
| block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p); |
| |
| // Create change as admin |
| PushOneCommit push = pushFactory.create( |
| db, admin.getIdent(), adminTestRepo); |
| PushOneCommit.Result r1 = push.to("refs/drafts/master"); |
| r1.assertOkStatus(); |
| |
| // Add user as reviewer |
| AddReviewerInput in = new AddReviewerInput(); |
| in.reviewer = user.email; |
| gApi.changes() |
| .id(r1.getChangeId()) |
| .addReviewer(in); |
| |
| // Fetch change |
| GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps"); |
| userTestRepo.reset("ps"); |
| |
| // Amend change as user |
| PushOneCommit.Result r2 = amendChange( |
| r1.getChangeId(), "refs/drafts/master", user, userTestRepo); |
| r2.assertErrorStatus("cannot add patch set to " |
| + r1.getChange().getId().id + "."); |
| } |
| |
| private static Iterable<Account.Id> getReviewers( |
| Collection<AccountInfo> r) { |
| return Iterables.transform(r, new Function<AccountInfo, Account.Id>() { |
| @Override |
| public Account.Id apply(AccountInfo account) { |
| return new Account.Id(account._accountId); |
| } |
| }); |
| } |
| |
| private ChangeResource parseResource(PushOneCommit.Result r) |
| throws Exception { |
| List<ChangeControl> ctls = changeFinder.find( |
| r.getChangeId(), atrScope.get().getUser()); |
| assertThat(ctls).hasSize(1); |
| return changeResourceFactory.create(ctls.get(0)); |
| } |
| } |