blob: 2c7b15dfb3542530a11898af2d4fe68c389474e3 [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.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.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.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.plugins.codeowners.JgitPath;
import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
import com.google.gerrit.plugins.codeowners.config.BackendConfig;
import com.google.gerrit.plugins.codeowners.config.StatusConfig;
import com.google.inject.Inject;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
/**
* Acceptance test for the {@link
* com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles} REST endpoint.
*
* <p>Further tests for the {@link
* com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFiles} REST endpoint that
* require using the REST API are implemented in {@link
* com.google.gerrit.plugins.codeowners.acceptance.restapi.CheckCodeOwnerConfigFilesRestIT}.
*/
public class CheckCodeOwnerConfigFilesIT extends AbstractCodeOwnersIT {
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ProjectOperations projectOperations;
private BackendConfig backendConfig;
@Before
public void setUpCodeOwnersPlugin() throws Exception {
backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
}
@Test
public void requiresCallerToBeProjectOwner() throws Exception {
requestScopeOperations.setApiUser(user.id());
AuthException authException =
assertThrows(AuthException.class, () -> checkCodeOwnerConfigFilesIn(project));
assertThat(authException).hasMessageThat().isEqualTo("write refs/meta/config not permitted");
}
@Test
public void noCodeOwnerConfigFile() throws Exception {
assertThat(checkCodeOwnerConfigFilesIn(project))
.containsExactly(
"refs/heads/master", ImmutableMap.of(),
"refs/meta/config", ImmutableMap.of());
}
@Test
public void nonVisibleBranchesAreSkipped() throws Exception {
String branchName = "non-visible";
createBranch(BranchNameKey.create(project, branchName));
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref(RefNames.fullName(branchName)).group(REGISTERED_USERS))
.update();
assertThat(checkCodeOwnerConfigFilesIn(project))
.containsExactly(
"refs/heads/master", ImmutableMap.of(), "refs/meta/config", ImmutableMap.of());
}
@Test
@GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
public void disabledBranchesAreSkipped() throws Exception {
assertThat(checkCodeOwnerConfigFilesIn(project))
.containsExactly("refs/heads/master", ImmutableMap.of());
}
@Test
@GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
public void validateDisabledBranches() throws Exception {
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.validateDisabledBranches()
.check())
.containsExactly(
"refs/heads/master", ImmutableMap.of(),
"refs/meta/config", ImmutableMap.of());
}
@Test
public void noIssuesInCodeOwnerConfigFile() throws Exception {
// Create some code owner config files.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.addCodeOwnerEmail(user.email())
.create();
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/baz/")
.addCodeOwnerEmail(admin.email())
.create();
assertThat(checkCodeOwnerConfigFilesIn(project))
.containsExactly(
"refs/heads/master", ImmutableMap.of(),
"refs/meta/config", ImmutableMap.of());
}
@Test
public void nonParseableCodeOwnerConfigFile() throws Exception {
String codeOwnerConfigPath = "/" + getCodeOwnerConfigFileName();
createInvalidCodeOwnerConfig(codeOwnerConfigPath);
assertThat(checkCodeOwnerConfigFilesIn(project))
.containsExactly(
"refs/heads/master",
ImmutableMap.of(
codeOwnerConfigPath,
ImmutableList.of(
fatal(
String.format(
"invalid code owner config file '%s':\n %s",
codeOwnerConfigPath,
getParsingErrorMessage(
ImmutableMap.of(
FindOwnersBackend.class,
"invalid line: INVALID",
ProtoBackend.class,
"1:8: Expected \"{\".")))))),
"refs/meta/config", ImmutableMap.of());
}
@Test
public void issuesInCodeOwnerConfigFile() throws Exception {
// imports are not supported for the proto backend
assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
// Create some code owner config files with issues.
CodeOwnerConfig.Key keyOfInvalidConfig1 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addImport(
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.ALL, "/not-a-code-owner-config"))
.create();
String pathOfInvalidConfig1 =
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig1).getFilePath();
CodeOwnerConfig.Key keyOfInvalidConfig2 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.addCodeOwnerEmail("unknown1@example.com")
.addCodeOwnerEmail("unknown2@example.com")
.create();
String pathOfInvalidConfig2 =
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig2).getFilePath();
// Also create a code owner config files without issues.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/baz")
.addCodeOwnerEmail(user.email())
.create();
assertThat(checkCodeOwnerConfigFilesIn(project))
.containsExactly(
"refs/heads/master",
ImmutableMap.of(
pathOfInvalidConfig1,
ImmutableList.of(
error(
String.format(
"invalid global import in '%s': '/not-a-code-owner-config' is"
+ " not a code owner config file",
pathOfInvalidConfig1))),
pathOfInvalidConfig2,
ImmutableList.of(
error(
String.format(
"code owner email 'unknown1@example.com' in '%s' cannot be"
+ " resolved for admin",
pathOfInvalidConfig2)),
error(
String.format(
"code owner email 'unknown2@example.com' in '%s' cannot be"
+ " resolved for admin",
pathOfInvalidConfig2)))),
"refs/meta/config", ImmutableMap.of());
}
@Test
public void validateSpecifiedBranches() throws Exception {
createBranch(BranchNameKey.create(project, "stable-1.0"));
createBranch(BranchNameKey.create(project, "stable-1.1"));
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("refs/heads/stable-1.0", "refs/heads/stable-1.1"))
.check())
.containsExactly(
"refs/heads/stable-1.0", ImmutableMap.of(),
"refs/heads/stable-1.1", ImmutableMap.of());
}
@Test
public void validateSpecifiedBranches_shortNames() throws Exception {
createBranch(BranchNameKey.create(project, "stable-1.0"));
createBranch(BranchNameKey.create(project, "stable-1.1"));
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("stable-1.0", "stable-1.1"))
.check())
.containsExactly(
"refs/heads/stable-1.0", ImmutableMap.of(),
"refs/heads/stable-1.1", ImmutableMap.of());
}
@Test
public void cannotValidateNonExistingBranch() throws Exception {
UnprocessableEntityException exception =
assertThrows(
UnprocessableEntityException.class,
() ->
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("refs/heads/non-existing"))
.check());
assertThat(exception).hasMessageThat().isEqualTo("branch refs/heads/non-existing not found");
}
@Test
public void cannotValidateNonVisibleBranch() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
UnprocessableEntityException exception =
assertThrows(
UnprocessableEntityException.class,
() ->
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("refs/heads/master"))
.check());
assertThat(exception).hasMessageThat().isEqualTo("branch refs/heads/master not found");
}
@Test
@GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
public void cannotValidateDisabledBranchWithoutEnablingValidationForDisabledBranches()
throws Exception {
BadRequestException exception =
assertThrows(
BadRequestException.class,
() ->
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("refs/meta/config"))
.check());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
"code owners functionality for branch refs/meta/config is disabled,"
+ " set 'validate_disabled_braches' in the input to 'true' if code owner config"
+ " files in this branch should be validated");
}
@Test
@GerritConfig(name = "plugin.code-owners.disabledBranch", value = "refs/meta/config")
public void validateSpecifiedBranchThatIsDisabled() throws Exception {
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.validateDisabledBranches()
.setBranches(ImmutableList.of("refs/meta/config"))
.check())
.containsExactly("refs/meta/config", ImmutableMap.of());
}
@Test
public void validateExactFile() throws Exception {
// imports are not supported for the proto backend
assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
// Create some code owner config files with issues.
CodeOwnerConfig.Key keyOfInvalidConfig1 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addImport(
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.ALL, "/not-a-code-owner-config"))
.create();
String pathOfInvalidConfig1 =
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig1).getFilePath();
CodeOwnerConfig.Key keyOfInvalidConfig2 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.addCodeOwnerEmail("unknown1@example.com")
.addCodeOwnerEmail("unknown2@example.com")
.create();
String pathOfInvalidConfig2 =
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig2).getFilePath();
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("master"))
.setPath(pathOfInvalidConfig1)
.check())
.containsExactly(
"refs/heads/master",
ImmutableMap.of(
pathOfInvalidConfig1,
ImmutableList.of(
error(
String.format(
"invalid global import in '%s': '/not-a-code-owner-config' is"
+ " not a code owner config file",
pathOfInvalidConfig1)))));
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("master"))
.setPath(pathOfInvalidConfig2)
.check())
.containsExactly(
"refs/heads/master",
ImmutableMap.of(
pathOfInvalidConfig2,
ImmutableList.of(
error(
String.format(
"code owner email 'unknown1@example.com' in '%s' cannot be"
+ " resolved for admin",
pathOfInvalidConfig2)),
error(
String.format(
"code owner email 'unknown2@example.com' in '%s' cannot be"
+ " resolved for admin",
pathOfInvalidConfig2)))));
}
@Test
public void validateFilesMatchingGlob() throws Exception {
// imports are not supported for the proto backend
assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
// Create some code owner config files with issues.
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/")
.addImport(
CodeOwnerConfigReference.create(
CodeOwnerConfigImportMode.ALL, "/not-a-code-owner-config"))
.create();
CodeOwnerConfig.Key keyOfInvalidConfig2 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/")
.addCodeOwnerEmail("unknown1@example.com")
.create();
String pathOfInvalidConfig2 =
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig2).getFilePath();
CodeOwnerConfig.Key keyOfInvalidConfig3 =
codeOwnerConfigOperations
.newCodeOwnerConfig()
.project(project)
.branch("master")
.folderPath("/foo/bar/")
.addCodeOwnerEmail("unknown2@example.com")
.create();
String pathOfInvalidConfig3 =
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig3).getFilePath();
assertThat(
projectCodeOwnersApiFactory
.project(project)
.checkCodeOwnerConfigFiles()
.setBranches(ImmutableList.of("master"))
.setPath("/foo/**")
.check())
.containsExactly(
"refs/heads/master",
ImmutableMap.of(
pathOfInvalidConfig2,
ImmutableList.of(
error(
String.format(
"code owner email 'unknown1@example.com' in '%s' cannot be"
+ " resolved for admin",
pathOfInvalidConfig2))),
codeOwnerConfigOperations.codeOwnerConfig(keyOfInvalidConfig3).getFilePath(),
ImmutableList.of(
error(
String.format(
"code owner email 'unknown2@example.com' in '%s' cannot be"
+ " resolved for admin",
pathOfInvalidConfig3)))));
}
private ConsistencyProblemInfo fatal(String message) {
return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.FATAL, message);
}
private ConsistencyProblemInfo error(String message) {
return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
}
private Map<String, Map<String, List<ConsistencyProblemInfo>>> checkCodeOwnerConfigFilesIn(
Project.NameKey projectName) throws RestApiException {
return projectCodeOwnersApiFactory.project(projectName).checkCodeOwnerConfigFiles().check();
}
private String getCodeOwnerConfigFileName() {
CodeOwnerBackend backend = backendConfig.getDefaultBackend();
if (backend instanceof FindOwnersBackend) {
return FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME;
} else if (backend instanceof ProtoBackend) {
return ProtoBackend.CODE_OWNER_CONFIG_FILE_NAME;
}
throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
}
private void createInvalidCodeOwnerConfig(String path) throws Exception {
disableCodeOwnersForProject(project);
String changeId =
createChange("Add invalid code owners file", JgitPath.of(path).get(), "INVALID")
.getChangeId();
approve(changeId);
gApi.changes().id(changeId).current().submit();
setCodeOwnersConfig(project, null, StatusConfig.KEY_DISABLED, "false");
}
private String getParsingErrorMessage(
ImmutableMap<Class<? extends CodeOwnerBackend>, String> messagesByBackend) {
CodeOwnerBackend codeOwnerBackend = backendConfig.getDefaultBackend();
assertThat(messagesByBackend).containsKey(codeOwnerBackend.getClass());
return messagesByBackend.get(codeOwnerBackend.getClass());
}
}