blob: 51b52da3898aa8df8e287f951744446c23f4aa44 [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.plugins.codeowners.acceptance.api;
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.assertPushRejected;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportType;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerSet;
import com.google.gerrit.plugins.codeowners.backend.config.BackendConfig;
import com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig;
import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersCodeOwnerConfigParser;
import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
import com.google.gerrit.plugins.codeowners.backend.proto.ProtoCodeOwnerConfigParser;
import com.google.gerrit.plugins.codeowners.common.CodeOwnerConfigValidationPolicy;
import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationCapability;
import com.google.gerrit.plugins.codeowners.validation.SkipCodeOwnerConfigValidationPushOption;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.submit.IntegrationConflictException;
import com.google.inject.Inject;
import java.nio.file.Path;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.PushResult;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for {@code com.google.gerrit.plugins.codeowners.validation.CodeOwnerConfigValidator}.
* {@link CodeOwnerConfigValidatorOnSubmitIT} and {@link CodeOwnerConfigValidatorErrorHandlingIT}
* contain further tests for {@code
* com.google.gerrit.plugins.codeowners.validation.CodeOwnerConfigValidator}.
*/
public class CodeOwnerConfigValidatorIT extends AbstractCodeOwnersIT {
private static final ObjectId TEST_REVISION =
ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ProjectOperations projectOperations;
@Inject private DynamicItem<UrlFormatter> urlFormatter;
private FindOwnersCodeOwnerConfigParser findOwnersCodeOwnerConfigParser;
private ProtoCodeOwnerConfigParser protoCodeOwnerConfigParser;
@Before
public void setUpCodeOwnersPlugin() throws Exception {
backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
findOwnersCodeOwnerConfigParser =
plugin.getSysInjector().getInstance(FindOwnersCodeOwnerConfigParser.class);
protoCodeOwnerConfigParser =
plugin.getSysInjector().getInstance(ProtoCodeOwnerConfigParser.class);
}
@Test
public void nonCodeOwnerConfigFileIsNotValidated() throws Exception {
PushOneCommit.Result r = createChange("Add arbitrary file", "arbitrary-file.txt", "INVALID");
assertOkWithoutMessages(r);
}
@Test
public void codeOwnerConfigFileWithNonMatchingFileExtensionIsNotValidated() throws Exception {
PushOneCommit.Result r =
createChange(
"Add code owner config with file extension",
getCodeOwnerConfigFileName() + ".foo",
"INVALID");
assertOkWithoutMessages(r);
}
@Test
@GerritConfig(name = "plugin.code-owners.fileExtension", value = "foo")
public void codeOwnerConfigFileWithMatchingFileExtensionIsValidated() throws Exception {
PushOneCommit.Result r =
createChange(
"Add code owner config with file extension",
getCodeOwnerConfigFileName() + ".foo",
"INVALID");
String abbreviatedCommit = abbreviateName(r.getCommit());
r.assertErrorStatus(
String.format(
"commit %s: [code-owners] %s", abbreviatedCommit, "invalid code owner config files"));
}
@Test
@GerritConfig(
name = "plugin.code-owners.enableCodeOwnerConfigFilesWithFileExtensions",
value = "true")
public void codeOwnerConfigFileWithFileExtensionIsValidatedIfFileExtensionsAreEnabled()
throws Exception {
PushOneCommit.Result r =
createChange(
"Add code owner config with file extension",
getCodeOwnerConfigFileName() + ".foo",
"INVALID");
String abbreviatedCommit = abbreviateName(r.getCommit());
r.assertErrorStatus(
String.format(
"commit %s: [code-owners] %s", abbreviatedCommit, "invalid code owner config files"));
}
@Test
public void canUploadConfigWithoutIssues() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config without issues.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void canUploadConfigWithoutIssuesInInitialCommit() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config without issues.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
public void canUploadConfigWhichAssignsCodeOwnershipToAllUsers() throws Exception {
testCanUploadConfigWhichAssignsCodeOwnershipToAllUsers();
}
@Test
@GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.com")
public void canUploadConfigWhichAssignsCodeOwnershipToAllUsers_restrictedAllowedEmailDomain()
throws Exception {
testCanUploadConfigWhichAssignsCodeOwnershipToAllUsers();
}
private void testCanUploadConfigWhichAssignsCodeOwnershipToAllUsers() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config without issues that assigns code ownership to all users.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(
CodeOwnerResolver.ALL_USERS_WILDCARD))
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
public void canUploadConfigWithoutIssues_withImport() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config to be imported.
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
// Fetch the commit that created the imported code owner config into the local repository so
// that the commit that creates the importing code owner config becomes a successor of this
// commit.
GitUtil.fetch(testRepo, "refs/*:refs/*");
testRepo.reset(projectOperations.project(project).getHead("master"));
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
// Create a code owner config with import and without issues.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addCodeOwnerEmail(admin.email())
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build())
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
public void canUploadConfigWithoutIssues_withImportFromOtherProject() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config to be imported.
Project.NameKey otherProject = projectOperations.newProject().create();
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(otherProject)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.setProject(otherProject)
.build();
// Create a code owner config with import from other project, and without issues.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addCodeOwnerEmail(admin.email())
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build())
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
public void canUploadConfigWithoutIssues_withImportFromOtherProjectAndBranch() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config to be imported.
String otherBranch = "foo";
Project.NameKey otherProject = projectOperations.newProject().branches(otherBranch).create();
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(otherProject)
.branch(otherBranch)
.folderPath("/foo/")
.addCodeOwnerEmail(user.email())
.create();
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.setProject(otherProject)
.setBranch(otherBranch)
.build();
// Create a code owner config with import from other project and branch, and without issues.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addCodeOwnerEmail(admin.email())
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build())
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
public void
canUploadConfigWithImportOfConfigThatIsAddedInSameCommit_importModeGlobalCodeOwnersOnly()
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = createCodeOwnerConfigKey("/foo/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
PushOneCommit.Result r =
createChange(
"Add code owners",
ImmutableMap.of(
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addCodeOwnerEmail(admin.email())
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build())
.build()),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getJGitFilePath(),
format(
CodeOwnerConfig.builder(keyOfImportedCodeOwnerConfig, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.builder().addCodeOwnerEmail(user.email()).build())
.build())));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
public void canUploadConfigWithImportOfConfigThatIsAddedInSameCommit_importModeAll()
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig = createCodeOwnerConfigKey("/foo/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.ALL,
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
PushOneCommit.Result r =
createChange(
"Add code owners",
ImmutableMap.of(
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(
CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
.build()),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getJGitFilePath(),
format(
CodeOwnerConfig.builder(keyOfImportedCodeOwnerConfig, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.builder().addCodeOwnerEmail(user.email()).build())
.build())));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
@GerritConfig(name = "plugin.code-owners.backend", value = "non-existing-backend")
public void canUploadNonParseableConfigIfCodeOwnersPluginConfigurationIsInvalid()
throws Exception {
PushOneCommit.Result r = createChange("Add code owners", "OWNERS", "INVALID");
assertOkWithWarnings(
r,
"skipping validation of code owner config files",
"code-owners plugin configuration is invalid, cannot validate code owner config files");
}
@Test
public void canUploadNonParseableConfigIfCodeOwnersFunctionalityIsDisabled() throws Exception {
disableCodeOwnersForProject(project);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(createCodeOwnerConfigKey("/"))
.getJGitFilePath(),
"INVALID");
assertOkWithHints(
r,
"skipping validation of code owner config files",
"code-owners functionality is disabled");
}
@Test
public void userCannotSkipCodeOwnerConfigValidationWithoutCapability() throws Exception {
PushOneCommit.Result r =
uploadNonParseableConfigWithPushOption(
user, String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME));
assertErrorWithMessages(
r,
"skipping code owner config validation not allowed",
String.format(
"%s for plugin code-owners not permitted", SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
public void adminCanSkipCodeOwnerConfigValidation() throws Exception {
PushOneCommit.Result r =
uploadNonParseableConfigWithPushOption(
admin, String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME));
assertOkWithHints(
r,
"skipping validation of code owner config files",
String.format(
"the validation is skipped due to the --code-owners~%s push option",
SkipCodeOwnerConfigValidationPushOption.NAME));
}
@Test
public void canUploadNonParseableConfigWithSkipOption() throws Exception {
allowRegisteredUsersToSkipValidation();
// with --code-owners~skip-validation
PushOneCommit.Result r =
uploadNonParseableConfigWithPushOption(
user, String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME));
assertOkWithHints(
r,
"skipping validation of code owner config files",
String.format(
"the validation is skipped due to the --code-owners~%s push option",
SkipCodeOwnerConfigValidationPushOption.NAME));
// with --code-owners~skip-validation=true
r =
uploadNonParseableConfigWithPushOption(
user,
String.format("code-owners~%s=true", SkipCodeOwnerConfigValidationPushOption.NAME));
assertOkWithHints(
r,
"skipping validation of code owner config files",
String.format(
"the validation is skipped due to the --code-owners~%s push option",
SkipCodeOwnerConfigValidationPushOption.NAME));
// with --code-owners~skip-validation=TRUE
r =
uploadNonParseableConfigWithPushOption(
user,
String.format("code-owners~%s=TRUE", SkipCodeOwnerConfigValidationPushOption.NAME));
assertOkWithHints(
r,
"skipping validation of code owner config files",
String.format(
"the validation is skipped due to the --code-owners~%s push option",
SkipCodeOwnerConfigValidationPushOption.NAME));
}
@Test
public void cannotUploadNonParseableConfigIfSkipOptionIsFalse() throws Exception {
allowRegisteredUsersToSkipValidation();
// with --code-owners~skip-validation=false
PushOneCommit.Result r =
uploadNonParseableConfigWithPushOption(
user,
String.format("code-owners~%s=false", SkipCodeOwnerConfigValidationPushOption.NAME));
String abbreviatedCommit = abbreviateName(r.getCommit());
r.assertErrorStatus(
String.format(
"commit %s: [code-owners] %s", abbreviatedCommit, "invalid code owner config files"));
}
@Test
public void cannotUploadNonParseableConfigWithInvalidSkipOption() throws Exception {
allowRegisteredUsersToSkipValidation();
PushOneCommit.Result r =
uploadNonParseableConfigWithPushOption(
user,
String.format("code-owners~%s=INVALID", SkipCodeOwnerConfigValidationPushOption.NAME));
assertErrorWithMessages(
r,
"invalid push option",
String.format(
"Invalid value for --code-owners~%s push option: INVALID",
SkipCodeOwnerConfigValidationPushOption.NAME));
}
@Test
public void cannotUploadNonParseableConfigIfSkipOptionIsSetMultipleTimes() throws Exception {
allowRegisteredUsersToSkipValidation();
PushOneCommit.Result r =
uploadNonParseableConfigWithPushOption(
user,
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME),
String.format("code-owners~%s=false", SkipCodeOwnerConfigValidationPushOption.NAME));
assertErrorWithMessages(
r,
"invalid push option",
String.format(
"--code-owners~%s push option can be specified only once, received multiple values: [, false]",
SkipCodeOwnerConfigValidationPushOption.NAME));
}
private PushOneCommit.Result uploadNonParseableConfigWithPushOption(
TestAccount testAccount, String... pushOptions) throws Exception {
TestRepository<InMemoryRepository> userRepo = cloneProject(project, testAccount);
PushOneCommit push =
pushFactory.create(
testAccount.newIdent(),
userRepo,
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(createCodeOwnerConfigKey("/"))
.getJGitFilePath(),
"INVALID");
push.setPushOptions(ImmutableList.copyOf(pushOptions));
return push.to("refs/for/master");
}
private void allowRegisteredUsersToSkipValidation() {
// grant the global capability that is required to use the
// --code-owners~skip-validation push option to registered users
projectOperations
.allProjectsForUpdate()
.add(
allowCapability("code-owners-" + SkipCodeOwnerConfigValidationCapability.ID)
.group(REGISTERED_USERS))
.update();
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnCommitReceived", value = "forced")
public void
cannotUploadNonParseableConfigIfCodeOwnersFunctionalityIsDisabledButValidationIsEnforced()
throws Exception {
disableCodeOwnersForProject(project);
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: expected \"{\""))));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnCommitReceived", value = "dry_run")
public void canUploadNonParseableConfigIfValidationIsDoneAsDryRun() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
assertOkWithFatals(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: expected \"{\""))));
}
@Test
@GerritConfig(
name = "plugin.code-owners.enableValidationOnCommitReceived",
value = "forced_dry_run")
public void
canUploadNonParseableConfigIfCodeOwnersFunctionalityIsDisabledButDryRunValidationIsEnforced()
throws Exception {
disableCodeOwnersForProject(project);
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
assertOkWithFatals(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: expected \"{\""))));
}
@Test
@GerritConfig(name = "plugin.code-owners.readOnly", value = "true")
public void cannotUploadConfigIfConfigsAreConfiguredToBeReadOnly() throws Exception {
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(createCodeOwnerConfigKey("/"))
.getJGitFilePath(),
format(
CodeOwnerConfig.builder(createCodeOwnerConfigKey("/"), TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
assertErrorWithMessages(
r,
"modifying code owner config files not allowed",
"code owner config files are configured to be read-only");
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnCommitReceived", value = "false")
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "true")
public void onReceiveCommitValidationDisabled() throws Exception {
setAsDefaultCodeOwners(admin);
// upload a change with a code owner config that has issues (non-resolvable code owners)
String unknownEmail = "non-existing-email@example.com";
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
.build()));
assertOkWithHints(
r,
"skipping validation of code owner config files",
"code owners config validation is disabled");
// approve the change
approve(r.getChangeId());
// try to submit the change, we expect that this fails since the validation on submit is enabled
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).current().submit());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Failed to submit 1 change due to the following problems:\n"
+ "Change %d: [code-owners] invalid code owner config files"
+ " (see %s for help):\n"
+ " ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
r.getChange().getId().get(),
getHelpPage(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
public void noValidationOnDeletionOfConfig() throws Exception {
// Disable the code owners functionality so that we can upload an invalid config that we can
// delete afterwards.
disableCodeOwnersForProject(project);
String path =
codeOwnerConfigOperations.codeOwnerConfig(createCodeOwnerConfigKey("/")).getJGitFilePath();
PushOneCommit.Result r = createChange("Add code owners", path, "INVALID");
r.assertOkStatus();
// re-enable the code owners functionality for the project
enableCodeOwnersForProject(project);
// delete the invalid code owner config file
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "Delete code owner config", path, "");
r = push.rm("refs/for/master");
assertOkWithoutMessages(r);
}
@Test
public void canUploadNonParseableConfigIfItWasAlreadyNonParseable() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// disable the code owners functionality so that we can upload an initial code owner config that
// is not parseable
disableCodeOwnersForProject(project);
// upload an initial code owner config that is not parseable
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
r.assertOkStatus();
// re-enable the code owners functionality for the project
enableCodeOwnersForProject(project);
// update the code owner config so that it is still not parseable
r =
createChange(
"Update code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"STILL INVALID");
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: STILL INVALID",
ProtoBackend.class,
"1:7: expected \"{\""))));
}
@Test
public void canUploadConfigWithIssuesIfItWasNonParseableBefore() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// disable the code owners functionality so that we can upload an initial code owner config that
// is not parseable
disableCodeOwnersForProject(project);
// upload an initial code owner config that is not parseable
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
r.assertOkStatus();
// re-enable the code owners functionality for the project
enableCodeOwnersForProject(project);
// update the code owner config so that it is parseable now, but has validation issues
String unknownEmail1 = "non-existing-email@example.com";
String unknownEmail2 = "another-unknown-email@example.com";
r =
createChange(
"Update code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(
unknownEmail1, admin.email(), unknownEmail2))
.build()));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail1,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()),
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail2,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
public void canUploadConfigWithIssuesIfTheyExistedBefore() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// disable the code owners functionality so that we can upload an initial code owner config that
// has issues
disableCodeOwnersForProject(project);
// upload an initial code owner config that has issues (non-resolvable code owners)
String unknownEmail = "non-existing-email@example.com";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
.build()));
r.assertOkStatus();
// re-enable the code owners functionality for the project
enableCodeOwnersForProject(project);
// update the code owner config so that the validation issue still exists, but no new issue is
// introduced
r =
createChange(
"Update code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(unknownEmail, admin.email()))
.build()));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
public void cannotUploadNonParseableConfig() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: expected \"{\""))));
}
@Test
public void cannotUpdateConfigToBeNonParseable() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// Create a code owner config without issues.
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
r.assertOkStatus();
r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
"INVALID");
assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: expected \"{\""))));
}
@Test
public void issuesAreReportedForAllInvalidConfigs() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey1 = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key codeOwnerConfigKey2 = createCodeOwnerConfigKey("/foo/bar/");
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
"Add code owners",
ImmutableMap.of(
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getJGitFilePath(),
"INVALID",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getJGitFilePath(),
"ALSO-INVALID"));
PushOneCommit.Result r = push.to("refs/for/master");
assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: expected \"{\""))),
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath(),
project,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: ALSO-INVALID",
ProtoBackend.class,
"1:1: expected identifier. found 'ALSO-INVALID'"))));
}
@Test
public void cannotUploadConfigWithNonResolvableCodeOwners() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
String unknownEmail1 = "non-existing-email@example.com";
String unknownEmail2 = "another-unknown-email@example.com";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(
unknownEmail1, admin.email(), unknownEmail2))
.build()));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail1,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()),
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail2,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
@GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.com")
public void canUploadConfigThatAssignsCodeOwnershipToAnEmailWithAnAllowedEmailDomain()
throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
assertThat(admin.email()).endsWith("@example.com");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
}
@Test
@GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.com")
public void cannotUploadConfigThatAssignsCodeOwnershipToAnEmailWithANonAllowedEmailDomain()
throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
String emailWithNonAllowedDomain = "foo@example.net";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(
emailWithNonAllowedDomain, admin.email()))
.build()));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"the domain of the code owner email '%s' in '%s' is not allowed for" + " code owners",
emailWithNonAllowedDomain,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath()));
}
@Test
public void cannotUploadConfigWithNewIssues() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// disable the code owners functionality so that we can upload an initial code owner config that
// has issues
disableCodeOwnersForProject(project);
// upload an initial code owner config that has issues (non-resolvable code owners)
String unknownEmail1 = "non-existing-email@example.com";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail1))
.build()));
r.assertOkStatus();
// re-enable the code owners functionality for the project
enableCodeOwnersForProject(project);
// update the code owner config so that the validation issue still exists and a new issue is
// introduced
String unknownEmail2 = "another-unknown-email@example.com";
r =
createChange(
"Update code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(unknownEmail1, unknownEmail2))
.build()));
String abbreviatedCommit = abbreviateName(r.getCommit());
r.assertErrorStatus(
String.format(
"commit %s: [code-owners] %s", abbreviatedCommit, "invalid code owner config files"));
r.assertMessage(
String.format(
"error: commit %s: [code-owners] %s",
abbreviatedCommit,
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail2,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName())));
// the pre-existing issue is returned as warning
r.assertMessage(
String.format(
"warning: commit %s: [code-owners] code owner email '%s' in '%s' cannot be resolved for %s",
abbreviatedCommit,
unknownEmail1,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
r.assertNotMessage("hint");
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnCommitReceived", value = "dry_run")
public void canUploadConfigWithNewIssuesIfValidationIsDoneAsDryRun() throws Exception {
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// upload an initial code owner config that has issues (non-resolvable code owners)
String unknownEmail1 = "non-existing-email@example.com";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail1))
.build()));
assertOkWithErrors(
r,
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail1,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
r.assertOkStatus();
// update the code owner config so that the validation issue still exists and a new issue is
// introduced
String unknownEmail2 = "another-unknown-email@example.com";
r =
createChange(
"Update code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.createWithoutPathExpressions(unknownEmail1, unknownEmail2))
.build()));
String abbreviatedCommit = abbreviateName(r.getCommit());
r.assertOkStatus();
r.assertMessage(
String.format(
"error: commit %s: [code-owners] %s",
abbreviatedCommit,
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail2,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName())));
// the pre-existing issue is returned as warning
r.assertMessage(
String.format(
"warning: commit %s: [code-owners] code owner email '%s' in '%s' cannot be resolved"
+ " for %s",
abbreviatedCommit,
unknownEmail1,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
r.assertNotMessage("hint");
}
@Test
public void uploadConfigWithGlobalSelfImportReportsAWarning() throws Exception {
testUploadConfigWithSelfImport(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void uploadConfigWithPerFileSelfImportReportsAWarning() throws Exception {
testUploadConfigWithSelfImport(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithSelfImport(CodeOwnerConfigImportType importType)
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports itself
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getFilePath())
.setProject(project)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': code owner config imports itself",
importType.getType(),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getFilePath()));
}
@Test
public void canUploadConfigWithGlobalImportOfFileWithExtensionFromSameFolder() throws Exception {
testUploadConfigWithImportOfFileWithExtensionFromSameFolder(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void canUploadConfigWithPerFileImportOfFileWithExtensionFromSameFolder() throws Exception {
testUploadConfigWithImportOfFileWithExtensionFromSameFolder(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportOfFileWithExtensionFromSameFolder(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a code owner config from the same folder but with an
// extension in the file name
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.fileName(getCodeOwnerConfigFileName() + "_extension")
.addCodeOwnerEmail(user.email())
.create();
GitUtil.fetch(testRepo, "refs/*:refs/*");
testRepo.reset(projectOperations.project(project).getHead("master"));
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.setProject(project)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
r.assertOkStatus();
}
@Test
public void cannotUploadConfigWithGlobalImportOfFileWithFileExtension() throws Exception {
testCannotUploadConfigWithImportOfFileWithFileExtension(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportOfFileWithFileExtension() throws Exception {
testCannotUploadConfigWithImportOfFileWithFileExtension(CodeOwnerConfigImportType.PER_FILE);
}
private void testCannotUploadConfigWithImportOfFileWithFileExtension(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a code owner config from the same folder but with a
// file extension in the file name
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.fileName(getCodeOwnerConfigFileName() + ".extension")
.addCodeOwnerEmail(user.email())
.create();
GitUtil.fetch(testRepo, "refs/*:refs/*");
testRepo.reset(projectOperations.project(project).getHead("master"));
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.setProject(project)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s':" + " '%s' is not a code owner config file",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath()));
}
@Test
@GerritConfig(
name = "plugin.code-owners.enableCodeOwnerConfigFilesWithFileExtensions",
value = "true")
public void canUploadConfigWithGlobalImportOfFileWithFileExtensionIfFileExtensionsAreEnabled()
throws Exception {
testUploadConfigWithImportOfFileWithFileExtension(CodeOwnerConfigImportType.GLOBAL);
}
@Test
@GerritConfig(
name = "plugin.code-owners.enableCodeOwnerConfigFilesWithFileExtensions",
value = "true")
public void canUploadConfigWithPerFileImportOfFileWithFileExtensionIfFileExtensionsAreEnabled()
throws Exception {
testUploadConfigWithImportOfFileWithFileExtension(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportOfFileWithFileExtension(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a code owner config from the same folder but with a
// file extension in the file name
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.fileName(getCodeOwnerConfigFileName() + ".extension")
.addCodeOwnerEmail(user.email())
.create();
GitUtil.fetch(testRepo, "refs/*:refs/*");
testRepo.reset(projectOperations.project(project).getHead("master"));
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.setProject(project)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
r.assertOkStatus();
}
@Test
public void cannotUploadConfigWithGlobalImportFromNonExistingProject() throws Exception {
testUploadConfigWithImportFromNonExistingProject(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportFromNonExistingProject() throws Exception {
testUploadConfigWithImportFromNonExistingProject(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportFromNonExistingProject(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a code owner config from a non-existing project
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
Project.NameKey nonExistingProject = Project.nameKey("non-existing");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(nonExistingProject, "master", "/"))
.getFilePath())
.setProject(nonExistingProject)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': project '%s' not found",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
nonExistingProject.get()));
}
@Test
public void cannotUploadConfigWithGlobalImportFromNonVisibleProject() throws Exception {
testUploadConfigWithImportFromNonVisibleProject(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportFromNonVisibleProject() throws Exception {
testUploadConfigWithImportFromNonVisibleProject(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportFromNonVisibleProject(CodeOwnerConfigImportType importType)
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a non-visible project with a code owner config file that we try to import
Project.NameKey nonVisibleProject =
projectOperations.newProject().name(name("non-visible-project")).create();
projectOperations
.project(nonVisibleProject)
.forUpdate()
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(nonVisibleProject)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
// create a code owner config that imports a code owner config from a non-visible project
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(nonVisibleProject, "master", "/"))
.getFilePath())
.setProject(nonVisibleProject)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
user,
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': project '%s' not found",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
nonVisibleProject.get()));
}
@Test
public void cannotUploadConfigWithGlobalImportFromHiddenProject() throws Exception {
testUploadConfigWithImportFromHiddenProject(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportFromHiddenProject() throws Exception {
testUploadConfigWithImportFromHiddenProject(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportFromHiddenProject(CodeOwnerConfigImportType importType)
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a hidden project with a code owner config file
Project.NameKey hiddenProject =
projectOperations.newProject().name(name("hidden-project")).create();
ConfigInput configInput = new ConfigInput();
configInput.state = ProjectState.HIDDEN;
gApi.projects().name(hiddenProject.get()).config(configInput);
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(hiddenProject)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
// create a code owner config that imports a code owner config from a hidden project
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(hiddenProject, "master", "/"))
.getFilePath())
.setProject(hiddenProject)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': project '%s' has state 'hidden' that doesn't permit read",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
hiddenProject.get()));
}
@Test
public void cannotUploadConfigWithGlobalImportFromNonExistingBranch() throws Exception {
testUploadConfigWithImportFromNonExistingBranch(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportFromNonExistingBranch() throws Exception {
testUploadConfigWithImportFromNonExistingBranch(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportFromNonExistingBranch(CodeOwnerConfigImportType importType)
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a code owner config from a non-existing branch
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
Project.NameKey otherProject = projectOperations.newProject().name(name("other")).create();
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(otherProject, "non-existing", "/"))
.getFilePath())
.setProject(otherProject)
.setBranch("non-existing")
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': branch 'non-existing' not found in project '%s'",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
otherProject.get()));
}
@Test
public void cannotUploadConfigWithGlobalImportFromNonVisibleBranch() throws Exception {
testUploadConfigWithImportFromNonVisibleBranch(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportFromNonVisibleBranch() throws Exception {
testUploadConfigWithImportFromNonVisibleBranch(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportFromNonVisibleBranch(CodeOwnerConfigImportType importType)
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
// create a project with a non-visible branch that contains a code owner config file
Project.NameKey otherProject =
projectOperations.newProject().name(name("non-visible-project")).create();
projectOperations
.project(otherProject)
.forUpdate()
.add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(otherProject)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
// create a code owner config that imports a code owner config from a non-visible branch
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(otherProject, "master", "/"))
.getFilePath())
.setProject(otherProject)
.setBranch("master")
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
user,
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': branch 'master' not found in project '%s'",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
otherProject.get()));
}
@Test
public void cannotUploadConfigWithGlobalImportOfNonCodeOwnerConfigFile() throws Exception {
testUploadConfigWithImportOfNonCodeOwnerConfigFile(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportOfNonCodeOwnerConfigFile() throws Exception {
testUploadConfigWithImportOfNonCodeOwnerConfigFile(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportOfNonCodeOwnerConfigFile(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a non code owner config file
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, "non-code-owner-config.txt")
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
user,
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s':"
+ " 'non-code-owner-config.txt' is not a code owner config file",
importType.getType(),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getFilePath()));
}
@Test
public void cannotUploadConfigWithGlobalImportOfNonExistingCodeOwnerConfig() throws Exception {
testUploadConfigWithImportOfNonExistingCodeOwnerConfig(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportOfNonExistingCodeOwnerConfig() throws Exception {
testUploadConfigWithImportOfNonExistingCodeOwnerConfig(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportOfNonExistingCodeOwnerConfig(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// create a code owner config that imports a non-existing code owner config
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfig.Key keyOfNonExistingCodeOwnerConfig =
CodeOwnerConfig.Key.create(project, "master", "/foo/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfNonExistingCodeOwnerConfig)
.getFilePath())
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
user,
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': '%s' does not exist (project = %s, branch = master,"
+ " revision = %s)",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfNonExistingCodeOwnerConfig)
.getFilePath(),
project.get(),
r.getCommit().name()));
}
@Test
public void cannotUploadConfigWithGlobalImportOfRootFolder() throws Exception {
testUploadConfigWithImportOfRootFolder(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportOfRootFolder() throws Exception {
testUploadConfigWithImportOfRootFolder(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportOfRootFolder(CodeOwnerConfigImportType importType)
throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// Create a code owner config that wrongly imports the root folder instead of the '/OWNERS'
// file.
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/foo/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY, /* filePath= */ "/")
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
user,
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': '/' is not a code owner config file",
importType.getType(),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getFilePath()));
}
@Test
public void
forMergeCommitsNonResolvableGlobalImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch()
throws Exception {
testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch(
CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportWithImportModeAll() throws Exception {
assume().that(backendConfig.getDefaultBackend()).isInstanceOf(FindOwnersBackend.class);
// create a code owner config that can be imported
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
GitUtil.fetch(testRepo, "refs/*:refs/*");
// codeOwnerConfigOperations cannot create a code owner config that has a per file import with
// import mode ALL, hence we just hard-code the contents of the OWNERS file here.
String codeOwnerConfig =
"per-file foo=include "
+ codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath();
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
codeOwnerConfig);
assertFatalWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid code owner config file '%s' (project = %s, branch = master):\n %s",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
project,
"keyword 'include' is not supported for per file imports: " + codeOwnerConfig));
}
@Test
public void
forMergeCommitsNonResolvablePerFileImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch()
throws Exception {
testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch(
CodeOwnerConfigImportType.PER_FILE);
}
private void
testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsWarningsIfImportsDontSpecifyBranch(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// Create a second project from which we will import a code owner config.
Project.NameKey otherProject = projectOperations.newProject().create();
// Create a target branch for into which we will merge later.
String targetBranchName = "target";
BranchInput branchInput = new BranchInput();
branchInput.ref = targetBranchName;
branchInput.revision = projectOperations.project(project).getHead("master").name();
gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
branchInput.revision = projectOperations.project(otherProject).getHead("master").name();
gApi.projects().name(otherProject.get()).branch(branchInput.ref).create(branchInput);
// Create the code owner config file in the second project that we will import.
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(otherProject)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(admin.email())
.create();
// Create a code owner config that imports the code owner config from the other project, without
// specifying the branch for the import (if the branch is not specified the code owner config is
// imported from the same branch that contains the importing code owner config).
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.setProject(otherProject)
.build();
TestCodeOwnerConfigCreation.Builder codeOwnerConfigBuilder =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/");
switch (importType) {
case GLOBAL:
codeOwnerConfigBuilder.addImport(codeOwnerConfigReference);
break;
case PER_FILE:
codeOwnerConfigBuilder.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build());
break;
default:
throw new IllegalStateException("unknown import type: " + importType);
}
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = codeOwnerConfigBuilder.create();
GitUtil.fetch(testRepo, "refs/*:refs/*");
// Create the merge commit.
RevCommit parent1 = projectOperations.project(project).getHead(targetBranchName);
RevCommit parent2 = projectOperations.project(project).getHead("master");
PushOneCommit m =
pushFactory.create(
admin.newIdent(),
testRepo,
"merge",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get()));
m.setParents(ImmutableList.of(parent1, parent2));
PushOneCommit.Result r = m.to("refs/for/" + targetBranchName);
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': '%s' does not exist (project = %s, branch = %s,"
+ " revision = %s)",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath(),
otherProject.get(),
targetBranchName,
projectOperations.project(otherProject).getHead(targetBranchName).getName()));
}
@Test
public void
forMergeCommitsNonResolvableGlobalImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch()
throws Exception {
testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch(
CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void
forMergeCommitsNonResolvablePerFileImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch()
throws Exception {
testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch(
CodeOwnerConfigImportType.PER_FILE);
}
private void
testForMergeCommitsThatNonResolvableImportsFromOtherProjectsAreReportedAsErrorsIfImportsSpecifyBranch(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// Create a second project from which we will import a non-existing code owner config.
Project.NameKey otherProject = projectOperations.newProject().create();
// Create a target branch for into which we will merge later.
String targetBranchName = "target";
BranchInput branchInput = new BranchInput();
branchInput.ref = targetBranchName;
branchInput.revision = projectOperations.project(project).getHead("master").name();
gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
// Create a code owner config that imports a non-existing code owner config from the other
// project, with specifying the branch for the import. When this code owner config is merged
// into another branch later we expect that it is rejected by the validation.
CodeOwnerConfig.Key keyOfNonExistingCodeOwnerConfig =
CodeOwnerConfig.Key.create(project, "master", "/foo/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfNonExistingCodeOwnerConfig)
.getFilePath())
.setProject(otherProject)
.setBranch("master")
.build();
TestCodeOwnerConfigCreation.Builder codeOwnerConfigBuilder =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/");
switch (importType) {
case GLOBAL:
codeOwnerConfigBuilder.addImport(codeOwnerConfigReference);
break;
case PER_FILE:
codeOwnerConfigBuilder.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build());
break;
default:
throw new IllegalStateException("unknown import type: " + importType);
}
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = codeOwnerConfigBuilder.create();
GitUtil.fetch(testRepo, "refs/*:refs/*");
// Create the merge commit.
RevCommit parent1 = projectOperations.project(project).getHead(targetBranchName);
RevCommit parent2 = projectOperations.project(project).getHead("master");
PushOneCommit m =
pushFactory.create(
admin.newIdent(),
testRepo,
"merge",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).get()));
m.setParents(ImmutableList.of(parent1, parent2));
PushOneCommit.Result r = m.to("refs/for/" + targetBranchName);
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': '%s' does not exist (project = %s, branch = master,"
+ " revision = %s)",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
codeOwnerConfigOperations
.codeOwnerConfig(keyOfNonExistingCodeOwnerConfig)
.getFilePath(),
otherProject.get(),
projectOperations.project(otherProject).getHead("master").getName()));
}
@Test
public void cannotUploadConfigWithGlobalImportOfNonParseableCodeOwnerConfig() throws Exception {
testUploadConfigWithImportOfNonParseableCodeOwnerConfig(CodeOwnerConfigImportType.GLOBAL);
}
@Test
public void cannotUploadConfigWithPerFileImportOfNonParseableCodeOwnerConfig() throws Exception {
testUploadConfigWithImportOfNonParseableCodeOwnerConfig(CodeOwnerConfigImportType.PER_FILE);
}
private void testUploadConfigWithImportOfNonParseableCodeOwnerConfig(
CodeOwnerConfigImportType importType) throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
CodeOwnerConfig.Key.create(project, "master", "/foo/");
// disable the code owners functionality so that we can upload a non-parseable code owner config
// that we then try to import
disableCodeOwnersForProject(project);
// upload a non-parseable code owner config that we then try to import
PushOneCommit.Result r =
createChange(
"Add invalid code owner config",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getJGitFilePath(),
"INVALID");
r.assertOkStatus();
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
// re-enable the code owners functionality for the project
enableCodeOwnersForProject(project);
// create a code owner config that imports a non-parseable code owner config
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportedCodeOwnerConfig)
.getFilePath())
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig, importType, codeOwnerConfigReference);
r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertErrorWithMessages(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': '%s' is not parseable (project = %s, branch = master)",
importType.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath(),
project.get()));
}
@Test
public void validateMergeCommitCreatedViaTheCreateChangeRestApi() throws Exception {
testValidateMergeCommitCreatedViaTheCreateChangeRestApi();
}
@Test
@GerritConfig(
name = "plugin.code-owners.mergeCommitStrategy",
value = "FILES_WITH_CONFLICT_RESOLUTION")
public void
validateMergeCommitCreatedViaTheCreateChangeRestApi_filesWithConflictResolutionAsMergeCommitStrategy()
throws Exception {
testValidateMergeCommitCreatedViaTheCreateChangeRestApi();
}
private void testValidateMergeCommitCreatedViaTheCreateChangeRestApi() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// Create another branch.
String branchName = "stable";
createBranch(BranchNameKey.create(project, branchName));
// Create a code owner config file in the other branch that can be imported.
CodeOwnerConfig.Key keyOfImportedCodeOwnerConfig =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(branchName)
.folderPath("/foo/")
.addCodeOwnerEmail(admin.email())
.create();
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportedCodeOwnerConfig).getFilePath());
// Create a code owner config file in the other branch that contains an import.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(branchName)
.folderPath("/")
.addImport(codeOwnerConfigReference)
.addCodeOwnerEmail(user.email())
.create();
// Create a change that merges the other branch into master. The code owner config files in the
// created merge commit will be validated.
ChangeInput changeInput = new ChangeInput();
changeInput.project = project.get();
changeInput.branch = "master";
changeInput.subject = "A change";
changeInput.status = ChangeStatus.NEW;
MergeInput mergeInput = new MergeInput();
mergeInput.source = gApi.projects().name(project.get()).branch(branchName).get().revision;
changeInput.merge = mergeInput;
gApi.changes().create(changeInput);
}
@Test
public void skipValidationForMergeCommitCreatedViaTheCreateChangeRestApi() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
// Create another branch.
String branchName = "stable";
createBranch(BranchNameKey.create(project, branchName));
// Create a code owner config file in the other branch.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch(branchName)
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.create();
// Create a conflicting code owner config file in the target branch.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
// Try creating a change that merges the other branch into master. The change creation fails
// because the code owner config file in the other branch conflicts with the code owner config
// file in the master branch.
ChangeInput changeInput = new ChangeInput();
changeInput.project = project.get();
changeInput.branch = "master";
changeInput.subject = "A change";
changeInput.status = ChangeStatus.NEW;
MergeInput mergeInput = new MergeInput();
mergeInput.source = gApi.projects().name(project.get()).branch(branchName).get().revision;
changeInput.merge = mergeInput;
MergeConflictException mergeConflictException =
assertThrows(MergeConflictException.class, () -> gApi.changes().create(changeInput));
assertThat(mergeConflictException)
.hasMessageThat()
.isEqualTo(String.format("merge conflict(s):\n%s", getCodeOwnerConfigFileName()));
// Try creating the merge change with conflicts. Fails because the code owner config file
// contains conflict markers which fails the code owner config file validation.
mergeInput.allowConflicts = true;
ResourceConflictException resourceConflictException =
assertThrows(ResourceConflictException.class, () -> gApi.changes().create(changeInput));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"[code-owners] invalid code owner config file '/%s'",
getCodeOwnerConfigFileName()));
// Create the merge change with skipping code owners validation.
changeInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
gApi.changes().create(changeInput);
}
@Test
public void userWithoutCapabilitySkipValidationCannotSkipValidationWithCreateChangeRestApi()
throws Exception {
requestScopeOperations.setApiUser(user.id());
ChangeInput changeInput = new ChangeInput();
changeInput.project = project.get();
changeInput.branch = "master";
changeInput.subject = "A change";
changeInput.status = ChangeStatus.NEW;
changeInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
ResourceConflictException resourceConflictException =
assertThrows(ResourceConflictException.class, () -> gApi.changes().create(changeInput));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"[code-owners] %s for plugin code-owners not permitted",
SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
public void skipValidationForMergeCommitCreatedViaTheCherryPickRevisionRestApi()
throws Exception {
// Create a conflicting code owner config file in the target branch.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(user.email())
.create();
// Create another branch.
BranchNameKey branchNameKey = BranchNameKey.create(project, "stable");
createBranch(branchNameKey);
// Create a change with a conflicting code owner config file in the other branch.
CodeOwnerConfig.Key codeOwnerConfigKey =
CodeOwnerConfig.Key.create(branchNameKey, Path.of("/"));
PushOneCommit.Result r =
createChange(
"Add code owner config",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
.build()));
r.assertOkStatus();
// Try creating a change that cherry picks the change on the other branch onto master.
// The change creation fails because the code owner config file in the other branch
// conflicts with the code owner config file in the master branch.
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = "master";
cherryPickInput.message = "A cherry pick";
IntegrationConflictException mergeConflictException =
assertThrows(
IntegrationConflictException.class,
() -> gApi.changes().id(r.getChangeId()).current().cherryPickAsInfo(cherryPickInput));
assertThat(mergeConflictException)
.hasMessageThat()
.contains("Cherry pick failed: merge conflict while merging commits");
// Try creating the cherry pick change with conflicts. Fails because the code owner config file
// contains conflict markers which fails the code owner config file validation.
cherryPickInput.allowConflicts = true;
ResourceConflictException resourceConflictException =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).current().cherryPickAsInfo(cherryPickInput));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"[code-owners] invalid code owner config file '/%s'",
getCodeOwnerConfigFileName()));
// Create the cherry pick change with skipping code owners validation.
cherryPickInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
gApi.changes().id(r.getChangeId()).current().cherryPickAsInfo(cherryPickInput);
}
@Test
public void userWithoutCapabilitySkipValidationCannotSkipValidationWithCherryPickRevisionRestApi()
throws Exception {
// Create another branch.
BranchNameKey branchNameKey = BranchNameKey.create(project, "stable");
createBranch(branchNameKey);
// Create a change with a code owner config file in the other branch.
CodeOwnerConfig.Key codeOwnerConfigKey =
CodeOwnerConfig.Key.create(branchNameKey, Path.of("/"));
PushOneCommit.Result r =
createChange(
"Add code owner config",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(
CodeOwnerSet.builder().addCodeOwnerEmail(admin.email()).build())
.build()));
r.assertOkStatus();
requestScopeOperations.setApiUser(user.id());
CherryPickInput cherryPickInput = new CherryPickInput();
cherryPickInput.destination = "master";
cherryPickInput.message = "A cherry pick";
cherryPickInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
ResourceConflictException resourceConflictException =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).current().cherryPickAsInfo(cherryPickInput));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"[code-owners] %s for plugin code-owners not permitted",
SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
public void skipValidationForRebaseWithConflicts() throws Exception {
// Create code owner config with 'admin' as code owner.
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.create();
// Create a change with a conflicting code owner config that makes 'user' the code owner.
// No need to reset the repo, since the commit that created to code owner config above wasn't
// fetched into testRepo.
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
"Add user as code owner",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user.email()).build())
.build()));
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.allowConflicts = true;
rebaseInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
ChangeInfo changeInfo = gApi.changes().id(r.getChangeId()).current().rebaseAsInfo(rebaseInput);
assertThat(changeInfo.containsGitConflicts).isTrue();
}
@Test
public void userWithoutCapabilitySkipValidationCannotSkipValidationWithRebase() throws Exception {
// Create code owner config with 'admin' as code owner.
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(admin.email())
.create();
// Create a change with a conflicting code owner config that makes 'user' the code owner.
// No need to reset the repo, since the commit that created to code owner config above wasn't
// fetched into testRepo.
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
"Add user as code owner",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.builder().addCodeOwnerEmail(user.email()).build())
.build()));
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
requestScopeOperations.setApiUser(user.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.REBASE).ref(RefNames.REFS_HEADS + "*").group(REGISTERED_USERS))
.update();
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.allowConflicts = true;
rebaseInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
ResourceConflictException resourceConflictException =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).current().rebaseAsInfo(rebaseInput));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"[code-owners] %s for plugin code-owners not permitted",
SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
public void skipValidationForRevert() throws Exception {
// Make admin a code owner so that admin can code-owner approve changes.
setAsRootCodeOwners(admin);
// Create a code owner config file with a non-resolvable code owner.
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/foo/");
String unknownEmail = "non-existing-email@example.com";
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
"Add code owner config file with non-resolvable code owner",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
.build()));
push.setPushOptions(
ImmutableList.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME)));
PushOneCommit.Result r = push.to("refs/for/master");
assertOkWithHints(
r,
"skipping validation of code owner config files",
String.format(
"the validation is skipped due to the --code-owners~%s push option",
SkipCodeOwnerConfigValidationPushOption.NAME));
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
// Fix the code owner config file.
PushOneCommit.Result r2 =
createChange(
"Fix code owner config file",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
assertOkWithHints(r2, "code owner config files validated, no issues found");
approve(r2.getChangeId());
gApi.changes().id(r2.getChangeId()).current().submit();
assertThat(gApi.changes().id(r2.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
// Try reverting the fix, expect failure due to invalid code owner config file.
RevertInput revertInput = new RevertInput();
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r2.getChangeId()).revert(revertInput));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"[code-owners] invalid code owner config files (see %s for help):\n"
+ " [code-owners] code owner email '%s' in '%s' cannot be resolved for %s",
getHelpPage(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
admin.username()));
// Revert the fix and skip the validation
revertInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
gApi.changes().id(r.getChangeId()).revert(revertInput);
}
@Test
public void userWithoutCapabilitySkipValidationCannotSkipValidationWithRevert() throws Exception {
// Make admin a code owner so that admin can code-owner approve changes.
setAsRootCodeOwners(admin);
// Create a code owner config without issues.
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/foo/");
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(admin.email()))
.build()));
assertOkWithHints(r, "code owner config files validated, no issues found");
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
// Trying to use the skip validation option on revert is rejected because user has no permission
// to skip the validation.
requestScopeOperations.setApiUser(user.id());
RevertInput revertInput = new RevertInput();
revertInput.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
ResourceConflictException resourceConflictException =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).revert(revertInput));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"[code-owners] %s for plugin code-owners not permitted",
SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true")
public void cannotCreateBranchWithInvalidCodeOwnerConfigFileViaRestApi() throws Exception {
// Add a non code owner config file to verify that it is not validated as code owner config file
PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/heads/master");
r.assertOkStatus();
// Create code owner configs with a non-existing user as code owner.
// We create 2 code owner configs with different commits so that it's tested that the validator
// checks all code owner config files and not only those added in the last commit.
String unknownEmail = "non-existing@example.com";
CodeOwnerConfig.Key codeOwnerConfigKey1 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
CodeOwnerConfig.Key codeOwnerConfigKey2 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(unknownEmail)
.create();
BranchInput input = new BranchInput();
input.ref = "new";
input.revision = projectOperations.project(project).getHead("master").name();
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.projects().name(project.get()).branch(input.ref).create(input));
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
+ "[code-owners] invalid code owner config files (see %s for help):\n"
+ " ERROR: code owner email '%s' in '%s' cannot be resolved for %s\n"
+ " ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
project,
getHelpPage(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true")
public void skipValidationForBranchCreationViaRestApi() throws Exception {
// Create code owner config with a non-existing user as code owner.
String unknownEmail = "non-existing@example.com";
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
BranchInput input = new BranchInput();
input.ref = "new";
input.revision = projectOperations.project(project).getHead("master").name();
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.projects().name(project.get()).branch(input.ref).create(input));
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
"[code-owners] invalid code owner config files (see %s for help):", getHelpPage()));
input.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
gApi.projects().name(project.get()).branch(input.ref).create(input);
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true")
public void userWithoutCapabilitySkipValidationCannotSkipValidationForBranchCreationViaRestApi()
throws Exception {
// Create code owner config with a non-existing user as code owner.
String unknownEmail = "non-existing@example.com";
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
requestScopeOperations.setApiUser(user.id());
BranchInput input = new BranchInput();
input.ref = "new";
input.revision = projectOperations.project(project).getHead("master").name();
input.validationOptions =
ImmutableMap.of(
String.format("code-owners~%s", SkipCodeOwnerConfigValidationPushOption.NAME), "true");
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.CREATE).ref(RefNames.REFS_HEADS + "*").group(REGISTERED_USERS))
.update();
ResourceConflictException resourceConflictException =
assertThrows(
ResourceConflictException.class,
() -> gApi.projects().name(project.get()).branch(input.ref).create(input));
assertThat(resourceConflictException)
.hasMessageThat()
.contains(
String.format(
"Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
+ "[code-owners] skipping code owner config validation not allowed:\n"
+ " ERROR: %s for plugin code-owners not permitted",
project, SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true")
public void cannotCreateBranchWithInvalidCodeOwnerConfigFileViaPush() throws Exception {
// Add a non code owner config file to verify that it is not validated as code owner config file
PushOneCommit.Result r = pushFactory.create(admin.newIdent(), testRepo).to("refs/heads/master");
r.assertOkStatus();
// Create code owner configs with a non-existing user as code owner.
// We create 2 code owner configs with different commits so that it's tested that the validator
// checks all code owner config files and not only those added in the last commit.
String unknownEmail = "non-existing@example.com";
CodeOwnerConfig.Key codeOwnerConfigKey1 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
CodeOwnerConfig.Key codeOwnerConfigKey2 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail(unknownEmail)
.create();
RevCommit head = projectOperations.project(project).getHead("master");
testRepo.git().fetch().call();
testRepo.reset(head.name());
PushResult r2 =
pushHead(
testRepo,
"refs/heads/new",
/* pushTags= */ false,
/* force= */ false,
/* pushOptions= */ ImmutableList.of());
assertPushRejected(
r2,
"refs/heads/new",
String.format(
"Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
+ "[code-owners] invalid code owner config files (see %s for help):\n"
+ " ERROR: code owner email '%s' in '%s' cannot be resolved for %s\n"
+ " ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
project,
getHelpPage(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey1).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey2).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true")
public void skipValidationForBranchCreationViaPush() throws Exception {
// Create code owner config with a non-existing user as code owner.
String unknownEmail = "non-existing@example.com";
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
RevCommit head = projectOperations.project(project).getHead("master");
testRepo.git().fetch().call();
testRepo.reset(head.name());
PushResult r =
pushHead(
testRepo,
"refs/heads/new",
/* pushTags= */ false,
/* force= */ false,
/* pushOptions= */ ImmutableList.of());
assertPushRejected(
r,
"refs/heads/new",
String.format(
"Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
+ "[code-owners] invalid code owner config files (see %s for help):\n"
+ " ERROR: code owner email '%s' in '%s' cannot be resolved for %s",
project,
getHelpPage(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
r =
pushHead(
testRepo,
"refs/heads/new",
/* pushTags= */ false,
/* force= */ false,
/* pushOptions= */ ImmutableList.of(
String.format(
"code-owners~%s=true", SkipCodeOwnerConfigValidationPushOption.NAME)));
assertPushOk(r, "refs/heads/new");
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "true")
public void userWithoutCapabilitySkipValidationCannotSkipValidationForBranchCreationViaPush()
throws Exception {
// Create code owner config with a non-existing user as code owner.
String unknownEmail = "non-existing@example.com";
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.CREATE).ref(RefNames.REFS_HEADS + "*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
PushResult r =
pushHead(
userRepo,
"refs/heads/new",
/* pushTags= */ false,
/* force= */ false,
/* pushOptions= */ ImmutableList.of(
String.format(
"code-owners~%s=true", SkipCodeOwnerConfigValidationPushOption.NAME)));
assertPushRejected(
r,
"refs/heads/new",
String.format(
"Validation for creation of ref 'refs/heads/new' in project %s failed:\n"
+ "[code-owners] skipping code owner config validation not allowed:\n"
+ " ERROR: %s for plugin code-owners not permitted",
project, SkipCodeOwnerConfigValidationCapability.ID));
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "false")
public void onBranchCreationValidationDisabled() throws Exception {
// Create a code owner config with a non-existing user as code owner.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail("non-existing@example.com")
.create();
RevCommit head = projectOperations.project(project).getHead("master");
testRepo.git().fetch().call();
testRepo.reset(head.name());
PushResult r =
pushHead(
testRepo,
"refs/heads/new",
/* pushTags= */ false,
/* force= */ false,
/* pushOptions= */ ImmutableList.of());
assertPushOk(r, "refs/heads/new");
assertThat(r.getMessages())
.contains(
"hint: [code-owners] skipping validation of code owner config files\n"
+ "hint: [code-owners] code owners config validation is disabled");
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnBranchCreation", value = "dry_run")
public void canCreateBranchWithInvalidCodeOwnerConfigIfValidationIsDoneAsDryRun()
throws Exception {
// Create a code owner config with a non-existing user as code owner.
String unknownEmail = "non-existing@example.com";
CodeOwnerConfig.Key codeOwnerConfigKey =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addCodeOwnerEmail(unknownEmail)
.create();
RevCommit head = projectOperations.project(project).getHead("master");
testRepo.git().fetch().call();
testRepo.reset(head.name());
PushResult r =
pushHead(
testRepo,
"refs/heads/new",
/* pushTags= */ false,
/* force= */ false,
/* pushOptions= */ ImmutableList.of());
assertPushOk(r, "refs/heads/new");
assertThat(r.getMessages())
.contains(
String.format(
"ERROR: [code-owners] invalid code owner config files (see %s for help)\n"
+ "ERROR: [code-owners] code owner email '%s' in '%s' cannot be resolved for"
+ " %s",
getHelpPage(),
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
}
@Test
@GerritConfig(name = "plugin.code-owners.rejectNonResolvableCodeOwners", value = "false")
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "true")
public void canUploadAndSubmitConfigWithUnresolvableCodeOwners() throws Exception {
setAsDefaultCodeOwners(admin);
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
// upload a code owner config that has issues (non-resolvable code owners)
String unknownEmail = "non-existing-email@example.com";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
.build()));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
// submit the change
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.rejectNonResolvableImports", value = "false")
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "true")
public void canUploadAndSubmitConfigWithUnresolvableImports() throws Exception {
setAsDefaultCodeOwners(admin);
skipTestIfImportsNotSupportedByCodeOwnersBackend();
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
// upload a code owner config that has issues (non-resolvable imports)
Project.NameKey nonExistingProject = Project.nameKey("non-existing");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(nonExistingProject, "master", "/"))
.getFilePath())
.setProject(nonExistingProject)
.build();
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(
CodeOwnerConfig.builder(keyOfImportingCodeOwnerConfig, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build())
.build()));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': project '%s' not found",
CodeOwnerConfigImportType.GLOBAL.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
nonExistingProject.get()),
String.format(
"invalid %s import in '%s': project '%s' not found",
CodeOwnerConfigImportType.PER_FILE.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
nonExistingProject.get()));
// submit the change
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.rejectNonResolvableCodeOwners", value = "true")
@GerritConfig(name = "plugin.code-owners.rejectNonResolvableImports", value = "true")
@GerritConfig(name = "plugin.code-owners.enableValidationOnCommitReceived", value = "false")
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "false")
public void rejectConfigOptionsAreIgnoredIfValidationIsDisabled() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
setAsDefaultCodeOwners(admin);
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
// upload a code owner config that has issues (non-resolvable code owners and non-resolvable
// imports)
String unknownEmail = "non-existing-email@example.com";
Project.NameKey nonExistingProject = Project.nameKey("non-existing");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(nonExistingProject, "master", "/"))
.getFilePath())
.setProject(nonExistingProject)
.build();
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(
CodeOwnerConfig.builder(keyOfImportingCodeOwnerConfig, TEST_REVISION)
.addImport(codeOwnerConfigReference)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build())
.build()));
assertOkWithHints(
r,
"skipping validation of code owner config files",
"code owners config validation is disabled");
// submit the change
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "true")
public void disableValidationForBranch() throws Exception {
setAsDefaultCodeOwners(admin);
// Disable the validation for the master branch.
updateCodeOwnersConfig(
project,
codeOwnersConfig -> {
codeOwnersConfig.setString(
GeneralConfig.SECTION_VALIDATION,
"refs/heads/master",
GeneralConfig.KEY_ENABLE_VALIDATION_ON_COMMIT_RECEIVED,
CodeOwnerConfigValidationPolicy.FALSE.name());
codeOwnersConfig.setString(
GeneralConfig.SECTION_VALIDATION,
"refs/heads/master",
GeneralConfig.KEY_ENABLE_VALIDATION_ON_SUBMIT,
CodeOwnerConfigValidationPolicy.FALSE.name());
});
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(createCodeOwnerConfigKey("/"))
.getJGitFilePath(),
"INVALID");
assertOkWithHints(
r,
"skipping validation of code owner config files",
"code owners config validation is disabled");
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "true")
public void disableRejectionOfNonResolvableCodeOwnersForBranch() throws Exception {
setAsDefaultCodeOwners(admin);
// Disable the rejection of non-resolvable code owners for the master branch.
updateCodeOwnersConfig(
project,
codeOwnersConfig ->
codeOwnersConfig.setBoolean(
GeneralConfig.SECTION_VALIDATION,
"refs/heads/master",
GeneralConfig.KEY_REJECT_NON_RESOLVABLE_CODE_OWNERS,
false));
CodeOwnerConfig.Key codeOwnerConfigKey = createCodeOwnerConfigKey("/");
String unknownEmail = "non-existing-email@example.com";
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getJGitFilePath(),
format(
CodeOwnerConfig.builder(codeOwnerConfigKey, TEST_REVISION)
.addCodeOwnerSet(CodeOwnerSet.createWithoutPathExpressions(unknownEmail))
.build()));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"code owner email '%s' in '%s' cannot be resolved for %s",
unknownEmail,
codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).getFilePath(),
identifiedUserFactory.create(admin.id()).getLoggableName()));
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
}
@Test
@GerritConfig(name = "plugin.code-owners.enableValidationOnSubmit", value = "true")
public void disableRejectionOfNonResolvableImportsForBranch() throws Exception {
skipTestIfImportsNotSupportedByCodeOwnersBackend();
setAsDefaultCodeOwners(admin);
// Disable the rejection of non-resolvable imports for the master branch.
updateCodeOwnersConfig(
project,
codeOwnersConfig ->
codeOwnersConfig.setBoolean(
GeneralConfig.SECTION_VALIDATION,
"refs/heads/master",
GeneralConfig.KEY_REJECT_NON_RESOLVABLE_IMPORTS,
false));
// create a code owner config that imports a code owner config from a non-existing project
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig = createCodeOwnerConfigKey("/");
Project.NameKey nonExistingProject = Project.nameKey("non-existing");
CodeOwnerConfigReference codeOwnerConfigReference =
CodeOwnerConfigReference.builder(
CodeOwnerConfigImportMode.GLOBAL_CODE_OWNER_SETS_ONLY,
codeOwnerConfigOperations
.codeOwnerConfig(CodeOwnerConfig.Key.create(nonExistingProject, "master", "/"))
.getFilePath())
.setProject(nonExistingProject)
.build();
CodeOwnerConfig codeOwnerConfig =
createCodeOwnerConfigWithImport(
keyOfImportingCodeOwnerConfig,
CodeOwnerConfigImportType.GLOBAL,
codeOwnerConfigReference);
PushOneCommit.Result r =
createChange(
"Add code owners",
codeOwnerConfigOperations
.codeOwnerConfig(keyOfImportingCodeOwnerConfig)
.getJGitFilePath(),
format(codeOwnerConfig));
assertOkWithWarnings(
r,
"invalid code owner config files",
String.format(
"invalid %s import in '%s': project '%s' not found",
CodeOwnerConfigImportType.GLOBAL.getType(),
codeOwnerConfigOperations.codeOwnerConfig(keyOfImportingCodeOwnerConfig).getFilePath(),
nonExistingProject.get()));
approve(r.getChangeId());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).get().status).isEqualTo(ChangeStatus.MERGED);
}
private CodeOwnerConfig createCodeOwnerConfigWithImport(
CodeOwnerConfig.Key keyOfImportingCodeOwnerConfig,
CodeOwnerConfigImportType importType,
CodeOwnerConfigReference codeOwnerConfigReference) {
CodeOwnerConfig.Builder codeOwnerConfigBuilder =
CodeOwnerConfig.builder(keyOfImportingCodeOwnerConfig, TEST_REVISION);
switch (importType) {
case GLOBAL:
codeOwnerConfigBuilder.addImport(codeOwnerConfigReference);
break;
case PER_FILE:
codeOwnerConfigBuilder.addCodeOwnerSet(
CodeOwnerSet.builder()
.addPathExpression("foo")
.addImport(codeOwnerConfigReference)
.build());
break;
default:
throw new IllegalStateException("unknown import type: " + importType);
}
return codeOwnerConfigBuilder.build();
}
private CodeOwnerConfig.Key createCodeOwnerConfigKey(String folderPath) {
return CodeOwnerConfig.Key.create(project, "master", folderPath);
}
private String format(CodeOwnerConfig codeOwnerConfig) throws Exception {
if (backendConfig.getDefaultBackend() instanceof FindOwnersBackend) {
return findOwnersCodeOwnerConfigParser.formatAsString(codeOwnerConfig);
} else if (backendConfig.getDefaultBackend() instanceof ProtoBackend) {
return protoCodeOwnerConfigParser.formatAsString(codeOwnerConfig);
}
throw new IllegalStateException(
String.format(
"unknown code owner backend: %s",
backendConfig.getDefaultBackend().getClass().getName()));
}
private String abbreviateName(AnyObjectId id) throws Exception {
return ObjectIds.abbreviateName(id, testRepo.getRevWalk().getObjectReader());
}
private static void assertOkWithoutMessages(PushOneCommit.Result pushResult) {
pushResult.assertOkStatus();
pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
private void assertOkWithHints(PushOneCommit.Result pushResult, String... hints)
throws Exception {
pushResult.assertOkStatus();
for (String hint : hints) {
pushResult.assertMessage(
String.format(
"hint: commit %s: [code-owners] %s", abbreviateName(pushResult.getCommit()), hint));
}
pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
}
private void assertOkWithFatals(PushOneCommit.Result pushResult, String... errors)
throws Exception {
pushResult.assertOkStatus();
for (String error : errors) {
pushResult.assertMessage(
String.format(
"fatal: commit %s: [code-owners] %s", abbreviateName(pushResult.getCommit()), error));
}
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
private void assertOkWithErrors(PushOneCommit.Result pushResult, String... errors)
throws Exception {
pushResult.assertOkStatus();
for (String error : errors) {
pushResult.assertMessage(
String.format(
"error: commit %s: [code-owners] %s", abbreviateName(pushResult.getCommit()), error));
}
pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
private void assertOkWithWarnings(PushOneCommit.Result pushResult, String... warnings)
throws Exception {
pushResult.assertOkStatus();
for (String warning : warnings) {
pushResult.assertMessage(
String.format(
"warning: commit %s: [code-owners] %s",
abbreviateName(pushResult.getCommit()), warning));
}
pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("hint");
}
private void assertErrorWithMessages(
PushOneCommit.Result pushResult, String summaryMessage, String... errors) throws Exception {
String abbreviatedCommit = abbreviateName(pushResult.getCommit());
pushResult.assertErrorStatus(
String.format("commit %s: [code-owners] %s", abbreviatedCommit, summaryMessage));
for (String error : errors) {
pushResult.assertMessage(
String.format("error: commit %s: [code-owners] %s", abbreviatedCommit, error));
}
pushResult.assertNotMessage("fatal");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
private void assertFatalWithMessages(
PushOneCommit.Result pushResult, String summaryMessage, String... errors) throws Exception {
String abbreviatedCommit = abbreviateName(pushResult.getCommit());
pushResult.assertErrorStatus(
String.format("commit %s: [code-owners] %s", abbreviatedCommit, summaryMessage));
for (String error : errors) {
pushResult.assertMessage(
String.format("fatal: commit %s: [code-owners] %s", abbreviatedCommit, error));
}
pushResult.assertNotMessage("error");
pushResult.assertNotMessage("warning");
pushResult.assertNotMessage("hint");
}
private String getHelpPage() {
return urlFormatter.get().getWebUrl().get()
+ "plugins/code-owners/Documentation/validation.html"
+ "#validation-checks-for-code-owner-config-files";
}
}