Allow to set required approval and override approval via REST

This makes changing the approval configuration easier.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Ic5883790b932391440e099b7a20bfc9e2817ea4b
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
index 5d899f3..684d715 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerProjectConfigInput.java
@@ -40,4 +40,25 @@
 
   /** The file extension that should be used for code owner config files in this project. */
   public String fileExtension;
+
+  /**
+   * The approval that is required from code owners.
+   *
+   * <p>The required approval must be specified in the format {@code <label-name>+<label-value>}.
+   *
+   * <p>If an empty string is provided the required approval configuration is unset. Unsetting the
+   * required approval means that the inherited required approval configuration or the default
+   * required approval ({@code Code-Review+1}) will apply.
+   *
+   * <p>In contrast to providing an empty string, providing {@code null} (or not setting the value)
+   * means that the required approval configuration is not updated.
+   */
+  public String requiredApproval;
+
+  /**
+   * The approvals that count as override for the code owners submit check.
+   *
+   * <p>The override approvals must be specified in the format {@code <label-name>+<label-value>}.
+   */
+  public List<String> overrideApprovals;
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
index 91543a6..1f2733a 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/PutCodeOwnerProjectConfig.java
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration.SECTION_CODE_OWNERS;
 import static com.google.gerrit.plugins.codeowners.backend.config.GeneralConfig.KEY_FILE_EXTENSION;
+import static com.google.gerrit.plugins.codeowners.backend.config.OverrideApprovalConfig.KEY_OVERRIDE_APPROVAL;
+import static com.google.gerrit.plugins.codeowners.backend.config.RequiredApprovalConfig.KEY_REQUIRED_APPROVAL;
 import static com.google.gerrit.plugins.codeowners.backend.config.StatusConfig.KEY_DISABLED;
 import static com.google.gerrit.plugins.codeowners.backend.config.StatusConfig.KEY_DISABLED_BRANCH;
 
@@ -118,6 +120,27 @@
             SECTION_CODE_OWNERS, /* subsection= */ null, KEY_FILE_EXTENSION, input.fileExtension);
       }
 
+      if (input.requiredApproval != null) {
+        if (input.requiredApproval.isEmpty()) {
+          codeOwnersConfig.unset(
+              SECTION_CODE_OWNERS, /* subsection= */ null, KEY_REQUIRED_APPROVAL);
+        } else {
+          codeOwnersConfig.setString(
+              SECTION_CODE_OWNERS,
+              /* subsection= */ null,
+              KEY_REQUIRED_APPROVAL,
+              input.requiredApproval);
+        }
+      }
+
+      if (input.overrideApprovals != null) {
+        codeOwnersConfig.setStringList(
+            SECTION_CODE_OWNERS,
+            /* subsection= */ null,
+            KEY_OVERRIDE_APPROVAL,
+            input.overrideApprovals);
+      }
+
       validateConfig(projectResource.getProjectState(), codeOwnersConfig);
 
       codeOwnersProjectConfigFile.commit(metaDataUpdate);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
index 045f01b..2da6c3d 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/PutCodeOwnerProjectConfigIT.java
@@ -15,22 +15,26 @@
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.RequiredApprovalSubject.assertThat;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.OptionalSubject.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.acceptance.UseClockStep;
 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.RefNames;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerProjectConfigInfo;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerProjectConfigInput;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.restapi.project.DeleteRef;
 import com.google.inject.Inject;
@@ -189,6 +193,100 @@
   }
 
   @Test
+  public void setRequiredApproval() throws Exception {
+    RequiredApproval requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
+
+    String otherLabel = "Other";
+    LabelDefinitionInput labelInput = new LabelDefinitionInput();
+    labelInput.values = ImmutableMap.of("+2", "Approval", "+1", "LGTM", " 0", "No Vote");
+    gApi.projects().name(project.get()).label(otherLabel).create(labelInput).get();
+
+    CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+    input.requiredApproval = otherLabel + "+2";
+    CodeOwnerProjectConfigInfo updatedConfig =
+        projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(updatedConfig.requiredApproval.label).isEqualTo(otherLabel);
+    assertThat(updatedConfig.requiredApproval.value).isEqualTo(2);
+    requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo(otherLabel);
+    assertThat(requiredApproval).hasValueThat().isEqualTo(2);
+
+    input.requiredApproval = "";
+    updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(updatedConfig.requiredApproval.label).isEqualTo("Code-Review");
+    assertThat(updatedConfig.requiredApproval.value).isEqualTo(1);
+    requiredApproval = codeOwnersPluginConfiguration.getRequiredApproval(project);
+    assertThat(requiredApproval).hasLabelNameThat().isEqualTo("Code-Review");
+    assertThat(requiredApproval).hasValueThat().isEqualTo(1);
+  }
+
+  @Test
+  public void setInvalidRequiredApproval() throws Exception {
+    CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+    input.requiredApproval = "Non-Existing-Label+2";
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> projectCodeOwnersApiFactory.project(project).updateConfig(input));
+    assertThat(exception)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "invalid config:\n"
+                    + "* Required approval 'Non-Existing-Label+2' that is configured in"
+                    + " code-owners.config (parameter codeOwners.requiredApproval) is invalid:"
+                    + " Label Non-Existing-Label doesn't exist for project %s.",
+                project));
+  }
+
+  @Test
+  public void setOverrideApproval() throws Exception {
+    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
+
+    String overrideLabel1 = "Bypass-Owners";
+    String overrideLabel2 = "Owners-Override";
+    createOwnersOverrideLabel(overrideLabel1);
+    createOwnersOverrideLabel(overrideLabel2);
+
+    CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+    input.overrideApprovals = ImmutableList.of(overrideLabel1 + "+1", overrideLabel2 + "+1");
+    CodeOwnerProjectConfigInfo updatedConfig =
+        projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(updatedConfig.overrideApproval).hasSize(2);
+    assertThat(updatedConfig.overrideApproval.get(0).label).isEqualTo(overrideLabel1);
+    assertThat(updatedConfig.overrideApproval.get(0).value).isEqualTo(1);
+    assertThat(updatedConfig.overrideApproval.get(1).label).isEqualTo(overrideLabel2);
+    assertThat(updatedConfig.overrideApproval.get(1).value).isEqualTo(1);
+    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).hasSize(2);
+
+    input.overrideApprovals = ImmutableList.of();
+    updatedConfig = projectCodeOwnersApiFactory.project(project).updateConfig(input);
+    assertThat(updatedConfig.overrideApproval).isNull();
+    assertThat(codeOwnersPluginConfiguration.getOverrideApproval(project)).isEmpty();
+  }
+
+  @Test
+  public void setInvalidOverrideApproval() throws Exception {
+    CodeOwnerProjectConfigInput input = new CodeOwnerProjectConfigInput();
+    input.overrideApprovals = ImmutableList.of("Non-Existing-Label+2");
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> projectCodeOwnersApiFactory.project(project).updateConfig(input));
+    assertThat(exception)
+        .hasMessageThat()
+        .contains(
+            String.format(
+                "invalid config:\n"
+                    + "* Required approval 'Non-Existing-Label+2' that is configured in"
+                    + " code-owners.config (parameter codeOwners.overrideApproval) is invalid:"
+                    + " Label Non-Existing-Label doesn't exist for project %s.",
+                project));
+  }
+
+  @Test
   @UseClockStep
   public void checkCommitData() throws Exception {
     RevCommit head1 = projectOperations.project(project).getHead(RefNames.REFS_CONFIG);
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index ebf1afc..bde20bd 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -837,6 +837,8 @@
 | `disabled` | optional | Whether the code owners functionality should be disabled/enabled for the project.
 | `disabled_branch` | optional | List of branches for which the code owners functionality is disabled. Can be exact refs, ref patterns or regular expressions. Overrides any existing disabled branch configuration.
 | `file_extension` | optional | The file extension that should be used for code owner config files in this project.
+| `required_approval` | optional | The approval that is required from code owners. Must be specified in the format "\<label-name\>+\<label-value\>". If an empty string is provided the required approval configuration is unset. Unsetting the required approval means that the inherited required approval configuration or the default required approval (`Code-Review+1`) will apply. In contrast to providing an empty string, providing `null` (or not setting the value) means that the required approval configuration is not updated.
+| `override_approvals` | optional | The approvals that count as override for the code owners submit check. Must be specified in the format "\<label-name\>+\<label-value\>".
 
 ---