| // 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()); |
| } |
| } |