Add test API to update code owner configs

For acceptance tests we need an API that allows us to update code owner
configs directly without permission checks.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I86838f587ca37e5ecb611bfc3f267d119dba6aa3
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperations.java b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperations.java
index 1b9cf6e..8f5f28e 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperations.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperations.java
@@ -89,5 +89,29 @@
      * @return the corresponding {@code CodeOwnerConfig}
      */
     CodeOwnerConfig get();
+
+    /**
+     * Starts the fluent chain to update a code owner config. The returned builder can be used to
+     * specify how the attributes of the code owner config should be modified. To update the code
+     * owner config for real, {@link TestCodeOwnerConfigUpdate.Builder#update()} must be called.
+     *
+     * <p>Example:
+     *
+     * <pre>
+     * codeOwnerOperations
+     *   .codeOwnerConfig(codeOwnerConfigKey)
+     *   .forUpdate()
+     *   .addCodeOwnerEmail("jane.roe@example.com")
+     *   .removeCodeOwnerEmail("joe.doe@example.com")
+     *   .update();
+     * </pre>
+     *
+     * <p><strong>Note:</strong> The update will fail with an {@link IllegalStateException} if the
+     * code owner config to update doesn't exist. If you want to check for the existence of a code
+     * owner config, use {@link #exists()}.
+     *
+     * @return a builder to update the code owner config
+     */
+    TestCodeOwnerConfigUpdate.Builder forUpdate();
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java
index aa65587..b9f4e63 100644
--- a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImpl.java
@@ -109,8 +109,24 @@
                       String.format("code owner config %s does not exist", codeOwnerConfigKey)));
     }
 
+    @Override
+    public TestCodeOwnerConfigUpdate.Builder forUpdate() {
+      return TestCodeOwnerConfigUpdate.builder(this::updateNewCodeOwnerConfig);
+    }
+
     private Optional<CodeOwnerConfig> getCodeOwnerConfig() {
       return codeOwners.get(codeOwnerConfigKey);
     }
+
+    private void updateNewCodeOwnerConfig(TestCodeOwnerConfigUpdate codeOwnerConfigUpdate)
+        throws Exception {
+      codeOwnersUpdate
+          .get()
+          .upsertCodeOwnerConfig(
+              codeOwnerConfigKey,
+              CodeOwnerConfigUpdate.builder()
+                  .setCodeOwnerModification(codeOwnerConfigUpdate.codeOwnerModification()::apply)
+                  .build());
+    }
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestCodeOwnerConfigUpdate.java b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestCodeOwnerConfigUpdate.java
new file mode 100644
index 0000000..fb3cd79
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/acceptance/testsuite/TestCodeOwnerConfigUpdate.java
@@ -0,0 +1,174 @@
+// 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.testsuite;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Test API to update a code owner config.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ *
+ * <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
+ * Hence, it cannot be used for testing those APIs.
+ */
+@AutoValue
+public abstract class TestCodeOwnerConfigUpdate {
+  /**
+   * Defines how the code owners of the code owner config should be modified. By default (that is if
+   * nothing is specified), the code owners remain unchanged.
+   *
+   * @return a function which gets the current code owners of the code owner config as input and
+   *     outputs the desired resulting code owners
+   */
+  public abstract Function<ImmutableSet<CodeOwnerReference>, Set<CodeOwnerReference>>
+      codeOwnerModification();
+
+  /**
+   * Gets the function that updates the code owner config.
+   *
+   * @return the function that updates the code owner config
+   */
+  abstract ThrowingConsumer<TestCodeOwnerConfigUpdate> codeOwnerConfigUpdater();
+
+  /**
+   * Creates a builder for a {@link TestCodeOwnerConfigUpdate}.
+   *
+   * @param codeOwnerConfigUpdater function that updates the code owner config
+   * @return builder for a {@link TestCodeOwnerConfigUpdate}
+   */
+  public static Builder builder(
+      ThrowingConsumer<TestCodeOwnerConfigUpdate> codeOwnerConfigUpdater) {
+    return new AutoValue_TestCodeOwnerConfigUpdate.Builder()
+        .codeOwnerConfigUpdater(codeOwnerConfigUpdater)
+        .codeOwnerModification(in -> in);
+  }
+
+  /** Builder for a {@link TestCodeOwnerConfigUpdate}. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    /**
+     * Sets the code owner modification.
+     *
+     * @return the Builder instance for chaining calls
+     * @see TestCodeOwnerConfigUpdate#codeOwnerModification()
+     */
+    abstract Builder codeOwnerModification(
+        Function<ImmutableSet<CodeOwnerReference>, Set<CodeOwnerReference>> codeOwnerModification);
+
+    /**
+     * Gets the code owner modification.
+     *
+     * @see TestCodeOwnerConfigUpdate#codeOwnerModification()
+     */
+    abstract Function<ImmutableSet<CodeOwnerReference>, Set<CodeOwnerReference>>
+        codeOwnerModification();
+
+    /**
+     * Removes all code owners.
+     *
+     * @return the Builder instance for chaining calls
+     */
+    public Builder clearCodeOwners() {
+      return codeOwnerModification(originalCodeOwners -> ImmutableSet.of());
+    }
+
+    /**
+     * Adds a code owner.
+     *
+     * @param codeOwner code owner that should be added
+     * @return the Builder instance for chaining calls
+     */
+    public Builder addCodeOwner(CodeOwnerReference codeOwner) {
+      Function<ImmutableSet<CodeOwnerReference>, Set<CodeOwnerReference>> previousModification =
+          codeOwnerModification();
+      codeOwnerModification(
+          originalCodeOwners ->
+              Sets.union(
+                  previousModification.apply(originalCodeOwners), ImmutableSet.of(codeOwner)));
+      return this;
+    }
+
+    /**
+     * Adds a code owner by email.
+     *
+     * @param codeOwnerEmail email of the code owner that should be added
+     * @return the Builder instance for chaining calls
+     */
+    public Builder addCodeOwnerEmail(String codeOwnerEmail) {
+      return addCodeOwner(CodeOwnerReference.create(codeOwnerEmail));
+    }
+
+    /**
+     * Removes a code owner.
+     *
+     * <p>Removing a non-existing code owner is a no-op.
+     *
+     * @param codeOwner code owner that should be removed
+     * @return the Builder instance for chaining calls
+     */
+    public Builder removeCodeOwner(CodeOwnerReference codeOwner) {
+      Function<ImmutableSet<CodeOwnerReference>, Set<CodeOwnerReference>> previousModification =
+          codeOwnerModification();
+      codeOwnerModification(
+          originalCodeOwners ->
+              Sets.difference(
+                  previousModification.apply(originalCodeOwners), ImmutableSet.of(codeOwner)));
+      return this;
+    }
+
+    /**
+     * Removes a code owner by email.
+     *
+     * <p>Removing a non-existing code owner is a no-op.
+     *
+     * @param codeOwnerEmail email of code owner that should be removed
+     * @return the Builder instance for chaining calls
+     */
+    public Builder removeCodeOwnerEmail(String codeOwnerEmail) {
+      return removeCodeOwner(CodeOwnerReference.create(codeOwnerEmail));
+    }
+
+    /**
+     * Sets the function that updates the code owner config.
+     *
+     * @param codeOwnerConfigUpdater the function that updates the code owner config
+     * @return the Builder instance for chaining calls
+     */
+    abstract Builder codeOwnerConfigUpdater(
+        ThrowingConsumer<TestCodeOwnerConfigUpdate> codeOwnerConfigUpdater);
+
+    /**
+     * Builds the {@link TestCodeOwnerConfigUpdate} instance.
+     *
+     * @return the {@link TestCodeOwnerConfigUpdate} instance
+     */
+    abstract TestCodeOwnerConfigUpdate autoBuild();
+
+    /** Executes the code owner config update as specified. */
+    public void update() {
+      TestCodeOwnerConfigUpdate codeOwnerConfigUpdater = autoBuild();
+      codeOwnerConfigUpdater
+          .codeOwnerConfigUpdater()
+          .acceptAndThrowSilently(codeOwnerConfigUpdater);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java
index 289622c..801897b 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/testsuite/CodeOwnerConfigOperationsImplTest.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.CodeOwnerConfigOperations.PerCodeOwnerConfigOperations;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigUpdate;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigUpdate.CodeOwnerModification;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnersUpdate;
@@ -216,6 +217,84 @@
         .contains(String.format("code owner config %s already exists", codeOwnerConfigKey));
   }
 
+  @Test
+  public void addCodeOwner() throws Exception {
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfig(
+            codeOwners -> ImmutableSet.of(CodeOwnerReference.create(admin.email())));
+    codeOwnerConfigOperations
+        .codeOwnerConfig(codeOwnerConfig.key())
+        .forUpdate()
+        .addCodeOwnerEmail(user.email())
+        .update();
+    assertThat(getCodeOwnerConfigFromServer(codeOwnerConfig.key()))
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(admin.email(), user.email());
+  }
+
+  @Test
+  public void removeCodeOwner() throws Exception {
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfig(
+            codeOwners ->
+                ImmutableSet.of(
+                    CodeOwnerReference.create(admin.email()),
+                    CodeOwnerReference.create(user.email())));
+    codeOwnerConfigOperations
+        .codeOwnerConfig(codeOwnerConfig.key())
+        .forUpdate()
+        .removeCodeOwnerEmail(user.email())
+        .update();
+    assertThat(getCodeOwnerConfigFromServer(codeOwnerConfig.key()))
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(admin.email());
+  }
+
+  @Test
+  public void removeNonExistingCodeOwner() throws Exception {
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfig(
+            codeOwners -> ImmutableSet.of(CodeOwnerReference.create(admin.email())));
+    codeOwnerConfigOperations
+        .codeOwnerConfig(codeOwnerConfig.key())
+        .forUpdate()
+        .removeCodeOwnerEmail(user.email())
+        .update();
+    assertThat(getCodeOwnerConfigFromServer(codeOwnerConfig.key()))
+        .hasCodeOwnersEmailsThat()
+        .containsExactly(admin.email());
+  }
+
+  @Test
+  public void clearCodeOwners() throws Exception {
+    CodeOwnerConfig codeOwnerConfig =
+        createCodeOwnerConfig(
+            codeOwners ->
+                ImmutableSet.of(
+                    CodeOwnerReference.create(admin.email()),
+                    CodeOwnerReference.create(user.email())));
+    codeOwnerConfigOperations
+        .codeOwnerConfig(codeOwnerConfig.key())
+        .forUpdate()
+        .clearCodeOwners()
+        .update();
+
+    // Removing all code owners leads to a deletion of the code owner config file.
+    assertThat(codeOwners.get(codeOwnerConfig.key())).isEmpty();
+  }
+
+  @Test
+  public void cannotUpdateNonExistingCodeOwnerConfig() throws Exception {
+    CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/");
+    IllegalStateException exception =
+        assertThrows(
+            IllegalStateException.class,
+            () -> codeOwnerConfigOperations.codeOwnerConfig(codeOwnerConfigKey).get());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(String.format("code owner config %s does not exist", codeOwnerConfigKey));
+  }
+
   private CodeOwnerConfig getCodeOwnerConfigFromServer(CodeOwnerConfig.Key codeOwnerConfigKey) {
     return codeOwners
         .get(codeOwnerConfigKey)
@@ -226,12 +305,14 @@
   }
 
   private CodeOwnerConfig createArbitraryCodeOwnerConfig() {
+    return createCodeOwnerConfig(
+        codeOwners -> ImmutableSet.of(CodeOwnerReference.create(admin.email())));
+  }
+
+  private CodeOwnerConfig createCodeOwnerConfig(CodeOwnerModification codeOwnerModification) {
     CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/");
     CodeOwnerConfigUpdate codeOwnerConfigUpdate =
-        CodeOwnerConfigUpdate.builder()
-            .setCodeOwnerModification(
-                codeOwners -> ImmutableSet.of(CodeOwnerReference.create(admin.email())))
-            .build();
+        CodeOwnerConfigUpdate.builder().setCodeOwnerModification(codeOwnerModification).build();
     return codeOwnersUpdate
         .get()
         .upsertCodeOwnerConfig(codeOwnerConfigKey, codeOwnerConfigUpdate)