Propagate FetchRefReplicatedEvent

When a ref is replicated through a REST-API call, invoke
the FetchRefReplicatedEvent synchronously, exactly as the replication
plugin does.

Bug: Issue 13893
Change-Id: I1959583b66e17f47df1cd972da51241c39327899
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java
index 4de9523..ac7e79b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommand.java
@@ -18,11 +18,18 @@
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.pull.ApplyObjectMetrics;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
 import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
+import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
+import com.googlesource.gerrit.plugins.replication.pull.ReplicationState.RefFetchResult;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
@@ -35,6 +42,8 @@
 
 public class ApplyObjectCommand {
 
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private static final Set<RefUpdate.Result> SUCCESSFUL_RESULTS =
       ImmutableSet.of(
           RefUpdate.Result.NEW,
@@ -45,15 +54,18 @@
   private final PullReplicationStateLogger fetchStateLog;
   private final ApplyObject applyObject;
   private final ApplyObjectMetrics metrics;
+  private final DynamicItem<EventDispatcher> eventDispatcher;
 
   @Inject
   public ApplyObjectCommand(
       PullReplicationStateLogger fetchStateLog,
       ApplyObject applyObject,
-      ApplyObjectMetrics metrics) {
+      ApplyObjectMetrics metrics,
+      DynamicItem<EventDispatcher> eventDispatcher) {
     this.fetchStateLog = fetchStateLog;
     this.applyObject = applyObject;
     this.metrics = metrics;
+    this.eventDispatcher = eventDispatcher;
   }
 
   public void applyObject(
@@ -65,6 +77,21 @@
     RefUpdateState refUpdateState = applyObject.apply(name, new RefSpec(refName), revisionData);
     long elapsed = NANOSECONDS.toMillis(context.stop());
 
+    try {
+      eventDispatcher
+          .get()
+          .postEvent(
+              new FetchRefReplicatedEvent(
+                  name.get(),
+                  refName,
+                  sourceLabel,
+                  getStatus(refUpdateState),
+                  refUpdateState.getResult()));
+    } catch (PermissionBackendException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot post event for ref '%s', project %s", refName, name);
+    }
+
     if (!isSuccessful(refUpdateState.getResult())) {
       String message =
           String.format(
@@ -81,6 +108,12 @@
         elapsed);
   }
 
+  private RefFetchResult getStatus(RefUpdateState refUpdateState) {
+    return isSuccessful(refUpdateState.getResult())
+        ? ReplicationState.RefFetchResult.SUCCEEDED
+        : ReplicationState.RefFetchResult.FAILED;
+  }
+
   private Boolean isSuccessful(RefUpdate.Result result) {
     return SUCCESSFUL_RESULTS.contains(result);
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java
new file mode 100644
index 0000000..51051c0
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectCommandTest.java
@@ -0,0 +1,100 @@
+// 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.googlesource.gerrit.plugins.replication.pull.ApplyObjectMetrics;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
+import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
+import java.io.IOException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ApplyObjectCommandTest {
+  private static final String TEST_SOURCE_LABEL = "test-source-label";
+  private static final String TEST_REF_NAME = "refs/changes/01/1/1";
+  private static final NameKey TEST_PROJECT_NAME = Project.nameKey("test-project");
+  private static final String TEST_REMOTE_NAME = "test-remote-name";
+
+  @Mock private PullReplicationStateLogger fetchStateLog;
+  @Mock private ApplyObject applyObject;
+  @Mock private ApplyObjectMetrics metrics;
+  @Mock private DynamicItem<EventDispatcher> eventDispatcherDataItem;
+  @Mock private EventDispatcher eventDispatcher;
+  @Mock private Timer1.Context<String> timetContext;
+  @Captor ArgumentCaptor<Event> eventCaptor;
+
+  private ApplyObjectCommand objectUnderTest;
+
+  @Before
+  public void setup() throws MissingParentObjectException, IOException {
+    RefUpdateState state = new RefUpdateState(TEST_REMOTE_NAME, RefUpdate.Result.NEW);
+    when(eventDispatcherDataItem.get()).thenReturn(eventDispatcher);
+    when(metrics.start(anyString())).thenReturn(timetContext);
+    when(timetContext.stop()).thenReturn(100L);
+    when(applyObject.apply(any(), any(), any())).thenReturn(state);
+
+    objectUnderTest =
+        new ApplyObjectCommand(fetchStateLog, applyObject, metrics, eventDispatcherDataItem);
+  }
+
+  @Test
+  public void shouldSendEventWhenApplyObject()
+      throws PermissionBackendException, IOException, RefUpdateException,
+          MissingParentObjectException {
+    objectUnderTest.applyObject(
+        TEST_PROJECT_NAME, TEST_REF_NAME, createSampleRevisionData(), TEST_SOURCE_LABEL);
+
+    verify(eventDispatcher).postEvent(eventCaptor.capture());
+    Event sentEvent = eventCaptor.getValue();
+    assertThat(sentEvent).isInstanceOf(FetchRefReplicatedEvent.class);
+    FetchRefReplicatedEvent fetchEvent = (FetchRefReplicatedEvent) sentEvent;
+    assertThat(fetchEvent.getProjectNameKey()).isEqualTo(TEST_PROJECT_NAME);
+    assertThat(fetchEvent.getRefName()).isEqualTo(TEST_REF_NAME);
+  }
+
+  private RevisionData createSampleRevisionData() {
+    RevisionObjectData commitData = new RevisionObjectData(Constants.OBJ_COMMIT, new byte[] {});
+    RevisionObjectData treeData = new RevisionObjectData(Constants.OBJ_TREE, new byte[] {});
+    return new RevisionData(commitData, treeData, Lists.newArrayList());
+  }
+}