blob: a36da32fe84d9a6bfa482e9c2d1ad418707ac301 [file] [log] [blame]
// Copyright (C) 2021 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.plugins.codeowners.acceptance.api;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.server.util.AccountTemplateUtil;
import java.time.Duration;
import java.util.Collection;
import java.util.concurrent.Callable;
import org.junit.Test;
/**
* Acceptance test for {@code com.google.gerrit.plugins.codeowners.backend.CodeOwnersOnAddReviewer}.
*
* <p>For tests the change message that is posted when a code owner is added as a reviewer, is added
* synchronously by default (see {@link AbstractCodeOwnersIT #defaultConfig()}). Tests that want to
* verify the asynchronous posting of this change message need to set {@code
* plugin.code-owners.enableAsyncMessageOnAddReviewer=true} in {@code gerrit.config} explicitly (by
* using the {@link GerritConfig} annotation).
*/
public class CodeOwnersOnAddReviewerIT extends AbstractCodeOwnersIT {
private static String TEST_PATH = "foo/bar.baz";
private static String TEST_PATH_ESCAPED = "foo/bar\\.baz";
@Test
@GerritConfig(name = "plugin.code-owners.disabled", value = "true")
public void noChangeMessageAddedIfCodeOwnersFuctionalityIsDisabled() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void noChangeMessageAddedIfReviewerIsNotACodeOwner() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(admin.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void noChangeMessageAddedIfInvalidCodeOwnerConfigFilesExist() throws Exception {
createNonParseableCodeOwnerConfig(getCodeOwnerConfigFileName());
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void changeMessageListsOwnedPaths() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED));
}
@Test
public void changeMessageListsOnlyOwnedPaths() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String testPath1 = "foo/bar.baz";
String testPath1Escaped = "foo/bar\\.baz";
String testPath2 = "foo/baz.bar";
String testPath2Escaped = "foo/baz\\.bar";
String changeId =
createChange(
"Test Change",
ImmutableMap.of(
testPath1,
"file content",
testPath2,
"file content",
"bar/foo.baz",
"file content",
"bar/baz.foo",
"file content"))
.getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()),
testPath1Escaped,
testPath2Escaped));
}
@Test
public void noChangeMessageAddedIfSameCodeOwnerIsAddedAsReviewerAgain() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
int messageCount = gApi.changes().id(changeId).get().messages.size();
// Add the same code owner as reviewer again.
gApi.changes().id(changeId).addReviewer(user.email());
// Check that no new change message was added.
assertThat(gApi.changes().id(changeId).get().messages.size()).isEqualTo(messageCount);
}
@Test
@GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "4")
public void pathsInChangeMessageAreLimited_limitNotReached() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
String testPath1 = "foo/bar.baz";
String testPath1Escaped = "foo/bar\\.baz";
String testPath2 = "foo/baz.bar";
String testPath2Escaped = "foo/baz\\.bar";
String testPath3 = "bar/foo.baz";
String testPath3Escaped = "bar/foo\\.baz";
String testPath4 = "bar/baz.foo";
String testPath4Escaped = "bar/baz\\.foo";
String changeId =
createChange(
"Test Change",
ImmutableMap.of(
testPath1,
"file content",
testPath2,
"file content",
testPath3,
"file content",
testPath4,
"file content"))
.getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n"
+ "* %s\n"
+ "* %s\n"
+ "* %s\n"
+ "* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()),
testPath4Escaped,
testPath3Escaped,
testPath1Escaped,
testPath2Escaped));
}
@Test
@GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "3")
public void pathsInChangeMessageAreLimited_limitReached() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
String testPath1 = "foo/bar.baz";
String testPath2 = "foo/baz.bar";
String testPath3 = "bar/foo.baz";
String testPath3Escaped = "bar/foo\\.baz";
String testPath4 = "bar/baz.foo";
String testPath4Escaped = "bar/baz\\.foo";
String testPath5 = "baz/foo.bar";
String testPath5Escaped = "baz/foo\\.bar";
String changeId =
createChange(
"Test Change",
ImmutableMap.of(
testPath1,
"file content",
testPath2,
"file content",
testPath3,
"file content",
testPath4,
"file content",
testPath5,
"file content"))
.getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n"
+ "* %s\n"
+ "* %s\n"
+ "* %s\n"
+ "(more files)\n",
AccountTemplateUtil.getAccountTemplate(user.id()),
testPath4Escaped,
testPath3Escaped,
testPath5Escaped));
}
@Test
@GerritConfig(name = "plugin.code-owners.maxPathsInChangeMessages", value = "0")
public void pathsInChangeMessagesDisabled() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.create();
String changeId =
createChange(
"Test Change",
ImmutableMap.of(
"foo/bar.baz",
"file content",
"foo/baz.bar",
"file content",
"bar/foo.baz",
"file content",
"bar/baz.foo",
"file content"))
.getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void noChangeMessageAddedIfDestinationBranchWasDeleted() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
String branchName = "tempBranch";
createBranch(BranchNameKey.create(project, branchName));
String changeId = createChange("refs/for/" + branchName).getChangeId();
DeleteBranchesInput input = new DeleteBranchesInput();
input.branches = ImmutableList.of(branchName);
gApi.projects().name(project.get()).deleteBranches(input);
gApi.changes().id(changeId).addReviewer(user.email());
// If the destination branch of the change no longer exits, the owned paths cannot be computed.
// Hence no change message is added in this case.
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void changeMessageListsOwnedPathsIfReviewerIsAddedViaPostReview() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
// Add reviewer via PostReview.
gApi.changes().id(changeId).current().review(ReviewInput.create().reviewer(user.email()));
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED));
}
@Test
public void multipleCodeOwnerAddedAsReviewersAtTheSameTime() throws Exception {
TestAccount user2 = accountCreator.user2();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.addCodeOwnerEmail(user2.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
// Add code owners 'user' and 'user2' as reviewers.
gApi.changes()
.id(changeId)
.current()
.review(ReviewInput.create().reviewer(user.email()).reviewer(user2.email()));
// We expect that 1 change message is added that lists the path owned for each of the new
// reviewers ('user' and 'user2').
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n\n"
+ "%s, who was added as reviewer owns the following files:\n* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()),
TEST_PATH_ESCAPED,
AccountTemplateUtil.getAccountTemplate(user2.id()),
TEST_PATH_ESCAPED));
}
@Test
public void reviewerAndCodeOwnerApprovalAddedAtTheSameTime() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(admin.email())
.addCodeOwnerEmail(user.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
// 'admin' grants a code owner approval (Code-Review+1) and adds 'user' as reviewer.
gApi.changes().id(changeId).current().review(ReviewInput.recommend().reviewer(user.email()));
// We expect that 2 changes messages are added:
// 1. change message listing the paths that were approved by voting Code-Review+1
// 2. change message listing the paths owned by the new reviewer
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.get(messages, messages.size() - 2).message)
.isEqualTo(
String.format(
"Patch Set 1: Code-Review+1\n\n"
+ "By voting Code-Review+1 the following files are now code-owner approved by"
+ " %s:\n"
+ "* %s\n",
AccountTemplateUtil.getAccountTemplate(admin.id()), TEST_PATH_ESCAPED));
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED));
}
@Test
public void markdownCharactersInPathsAreEscaped() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
testMarkdownCharactersInPathsAreEscaped('\\', user);
testMarkdownCharactersInPathsAreEscaped('`', user);
testMarkdownCharactersInPathsAreEscaped('*', user);
testMarkdownCharactersInPathsAreEscaped('_', user);
testMarkdownCharactersInPathsAreEscaped('{', user);
testMarkdownCharactersInPathsAreEscaped('}', user);
testMarkdownCharactersInPathsAreEscaped('[', user);
testMarkdownCharactersInPathsAreEscaped(']', user);
testMarkdownCharactersInPathsAreEscaped('<', user);
testMarkdownCharactersInPathsAreEscaped('>', user);
testMarkdownCharactersInPathsAreEscaped('(', user);
testMarkdownCharactersInPathsAreEscaped(')', user);
testMarkdownCharactersInPathsAreEscaped('#', user);
testMarkdownCharactersInPathsAreEscaped('+', user);
testMarkdownCharactersInPathsAreEscaped('-', user);
testMarkdownCharactersInPathsAreEscaped('.', user);
testMarkdownCharactersInPathsAreEscaped('!', user);
testMarkdownCharactersInPathsAreEscaped('|', user);
}
private void testMarkdownCharactersInPathsAreEscaped(
char markdownCharacter, TestAccount codeOwner) throws Exception {
String testPath = markdownCharacter + "foo" + markdownCharacter + ".bar";
String testPathEscaped = "\\" + markdownCharacter + "foo\\" + markdownCharacter + "\\.bar";
String changeId = createChange("Test Change", testPath, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(codeOwner.email());
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n",
AccountTemplateUtil.getAccountTemplate(codeOwner.id()), testPathEscaped));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableAsyncMessageOnAddReviewer", value = "true")
public void asyncChangeMessageThatListsOwnedPaths() throws Exception {
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
String changeId = createChange("Test Change", TEST_PATH, "file content").getChangeId();
gApi.changes().id(changeId).addReviewer(user.email());
assertAsyncChangeMessage(
changeId,
String.format(
"%s, who was added as reviewer owns the following files:\n* %s\n",
AccountTemplateUtil.getAccountTemplate(user.id()), TEST_PATH_ESCAPED));
}
private void assertAsyncChangeMessage(String changeId, String expectedChangeMessage)
throws Exception {
assertAsync(
() -> {
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
assertThat(Iterables.getLast(messages).message).isEqualTo(expectedChangeMessage);
return null;
});
}
@CanIgnoreReturnValue
private <T> T assertAsync(Callable<T> assertion) throws Exception {
return RetryerBuilder.<T>newBuilder()
.retryIfException(t -> true)
.withStopStrategy(
StopStrategies.stopAfterDelay(Duration.ofSeconds(1).toMillis(), MILLISECONDS))
.build()
.call(() -> assertion.call());
}
}