| // Copyright (C) 2021 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.acceptance.server.project; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static com.google.gerrit.acceptance.GitUtil.fetch; |
| import static com.google.gerrit.acceptance.GitUtil.pushHead; |
| |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.server.project.ProjectConfig; |
| import java.util.Locale; |
| import java.util.function.Consumer; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevObject; |
| import org.eclipse.jgit.transport.PushResult; |
| import org.eclipse.jgit.transport.RemoteRefUpdate; |
| import org.eclipse.jgit.transport.RemoteRefUpdate.Status; |
| import org.eclipse.jgit.treewalk.TreeWalk; |
| import org.eclipse.jgit.util.RawParseUtils; |
| import org.junit.Test; |
| |
| /** |
| * Tests validating submit requirements on upload of {@code project.config} to {@code |
| * refs/meta/config}. |
| */ |
| public class SubmitRequirementsValidationIT extends AbstractDaemonTest { |
| @Test |
| public void validSubmitRequirementIsAccepted_optionalParametersNotSet() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\"")); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertOkStatus(r); |
| } |
| |
| @Test |
| public void validSubmitRequirementIsAccepted_allParametersSet() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| updateProjectConfig( |
| projectConfig -> { |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_DESCRIPTION, |
| /* value= */ "foo bar description"); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION, |
| /* value= */ "branch:refs/heads/master"); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\""); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION, |
| /* value= */ "label:\"override=+1\""); |
| projectConfig.setBoolean( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, |
| /* value= */ false); |
| }); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertOkStatus(r); |
| } |
| |
| @Test |
| public void parametersDirectlyInSubmitRequirementsSectionAreRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| updateProjectConfig( |
| projectConfig -> { |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ null, |
| /* name= */ ProjectConfig.KEY_SR_DESCRIPTION, |
| /* value= */ "foo bar description"); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ null, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\""); |
| }); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Submit requirements must be defined in submit-requirement.<name>" |
| + " subsections. Setting parameters directly in the submit-requirement section is" |
| + " not allowed: [%s, %s]", |
| ProjectConfig.KEY_SR_DESCRIPTION, ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION)); |
| } |
| |
| @Test |
| public void unsupportedParameterDirectlyInSubmitRequirementsSectionIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ null, |
| /* name= */ "unknown", |
| /* value= */ "value")); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| "project.config: Submit requirements must be defined in submit-requirement.<name>" |
| + " subsections. Setting parameters directly in the submit-requirement section is" |
| + " not allowed: [unknown]"); |
| } |
| |
| @Test |
| public void unsupportedParameterForSubmitRequirementIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ "unknown", |
| /* value= */ "value")); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Unsupported parameters for submit requirement '%s': [unknown]", |
| submitRequirementName)); |
| } |
| |
| @Test |
| public void conflictingSubmitRequirementsAreRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| updateProjectConfig( |
| projectConfig -> { |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\""); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName.toLowerCase(Locale.US), |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\""); |
| }); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Submit requirement '%s' conflicts with '%s'.", |
| submitRequirementName.toLowerCase(Locale.US), submitRequirementName)); |
| } |
| |
| @Test |
| public void conflictingSubmitRequirementIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| String submitRequirementName = "Code-Review"; |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\"")); |
| PushResult r = pushRefsMetaConfig(); |
| assertOkStatus(r); |
| |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName.toLowerCase(Locale.US), |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\"")); |
| r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Submit requirement '%s' conflicts with '%s'.", |
| submitRequirementName.toLowerCase(Locale.US), submitRequirementName)); |
| } |
| |
| @Test |
| public void submitRequirementWithoutSubmittabilityExpressionIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_DESCRIPTION, |
| /* value= */ "foo bar description")); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Setting a submittability expression for submit requirement '%s' is" |
| + " required: Missing %s.%s.%s", |
| submitRequirementName, |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| submitRequirementName, |
| ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION)); |
| } |
| |
| @Test |
| public void submitRequirementWithInvalidSubmittabilityExpressionIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| String invalidExpression = "invalid_field:invalid_value"; |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ invalidExpression)); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is" |
| + " invalid: Unsupported operator %s", |
| invalidExpression, |
| submitRequirementName, |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| submitRequirementName, |
| ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| invalidExpression)); |
| } |
| |
| @Test |
| public void submitRequirementWithInvalidApplicabilityExpressionIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| String invalidExpression = "invalid_field:invalid_value"; |
| updateProjectConfig( |
| projectConfig -> { |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\""); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION, |
| /* value= */ invalidExpression); |
| }); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is" |
| + " invalid: Unsupported operator %s", |
| invalidExpression, |
| submitRequirementName, |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| submitRequirementName, |
| ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION, |
| invalidExpression)); |
| } |
| |
| @Test |
| public void submitRequirementWithInvalidOverrideExpressionIsRejected() throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| String invalidExpression = "invalid_field:invalid_value"; |
| updateProjectConfig( |
| projectConfig -> { |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION, |
| /* value= */ "label:\"Code-Review=+2\""); |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION, |
| /* value= */ invalidExpression); |
| }); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is" |
| + " invalid: Unsupported operator %s", |
| invalidExpression, |
| submitRequirementName, |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| submitRequirementName, |
| ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION, |
| invalidExpression)); |
| } |
| |
| @Test |
| public void submitRequirementWithInvalidAllowOverrideInChildProjectsIsRejected() |
| throws Exception { |
| fetchRefsMetaConfig(); |
| |
| String submitRequirementName = "Code-Review"; |
| String invalidValue = "invalid"; |
| updateProjectConfig( |
| projectConfig -> |
| projectConfig.setString( |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| /* subsection= */ submitRequirementName, |
| /* name= */ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, |
| /* value= */ invalidValue)); |
| |
| PushResult r = pushRefsMetaConfig(); |
| assertErrorStatus( |
| r, |
| "Invalid project configuration", |
| String.format( |
| "project.config: Invalid value %s.%s.%s for submit requirement '%s': %s", |
| ProjectConfig.SUBMIT_REQUIREMENT, |
| submitRequirementName, |
| ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, |
| submitRequirementName, |
| invalidValue)); |
| } |
| |
| private void fetchRefsMetaConfig() throws Exception { |
| fetch(testRepo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG); |
| testRepo.reset(RefNames.REFS_CONFIG); |
| } |
| |
| private PushResult pushRefsMetaConfig() throws Exception { |
| return pushHead(testRepo, RefNames.REFS_CONFIG); |
| } |
| |
| private void updateProjectConfig(Consumer<Config> configUpdater) throws Exception { |
| RevCommit head = getHead(testRepo.getRepository(), RefNames.REFS_CONFIG); |
| Config projectConfig = readProjectConfig(head); |
| configUpdater.accept(projectConfig); |
| RevCommit commit = |
| testRepo.update( |
| RefNames.REFS_CONFIG, |
| testRepo |
| .commit() |
| .parent(head) |
| .message("Update project config") |
| .author(admin.newIdent()) |
| .committer(admin.newIdent()) |
| .add(ProjectConfig.PROJECT_CONFIG, projectConfig.toText())); |
| |
| testRepo.reset(commit); |
| } |
| |
| private Config readProjectConfig(RevCommit commit) throws Exception { |
| try (TreeWalk tw = |
| TreeWalk.forPath( |
| testRepo.getRevWalk().getObjectReader(), |
| ProjectConfig.PROJECT_CONFIG, |
| commit.getTree())) { |
| if (tw == null) { |
| throw new IllegalStateException( |
| String.format("%s does not exist", ProjectConfig.PROJECT_CONFIG)); |
| } |
| } |
| RevObject blob = testRepo.get(commit.getTree(), ProjectConfig.PROJECT_CONFIG); |
| byte[] data = testRepo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE); |
| String content = RawParseUtils.decode(data); |
| |
| Config projectConfig = new Config(); |
| projectConfig.fromText(content); |
| return projectConfig; |
| } |
| |
| public void assertOkStatus(PushResult result) { |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(RefNames.REFS_CONFIG); |
| assertThat(refUpdate).isNotNull(); |
| assertWithMessage(getMessage(result, refUpdate)) |
| .that(refUpdate.getStatus()) |
| .isEqualTo(Status.OK); |
| } |
| |
| public void assertErrorStatus(PushResult result, String... expectedMessages) { |
| RemoteRefUpdate refUpdate = result.getRemoteUpdate(RefNames.REFS_CONFIG); |
| assertThat(refUpdate).isNotNull(); |
| assertWithMessage(getMessage(result, refUpdate)) |
| .that(refUpdate.getStatus()) |
| .isEqualTo(Status.REJECTED_OTHER_REASON); |
| for (String expectedMessage : expectedMessages) { |
| assertThat(result.getMessages()).contains(expectedMessage); |
| } |
| } |
| |
| private String getMessage(PushResult result, RemoteRefUpdate refUpdate) { |
| StringBuilder b = new StringBuilder(); |
| if (refUpdate.getMessage() != null) { |
| b.append(refUpdate.getMessage()); |
| b.append("\n"); |
| } |
| b.append(result.getMessages()); |
| return b.toString(); |
| } |
| } |