Merge "Send project-deleted event after project deletion"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
index 459d819..47feddf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteAction.java
@@ -14,8 +14,12 @@
 
 package com.googlesource.gerrit.plugins.deleteproject;
 
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritInstanceId;
+import com.google.gerrit.server.events.EventDispatcher;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -36,7 +40,9 @@
       DeleteLog deleteLog,
       DeletePreconditions preConditions,
       Configuration cfg,
-      HideProject hideProject) {
+      HideProject hideProject,
+      DynamicItem<EventDispatcher> dispatcher,
+      @Nullable @GerritInstanceId String instanceId) {
     super(
         dbHandler,
         fsHandler,
@@ -45,7 +51,9 @@
         deleteLog,
         preConditions,
         cfg,
-        hideProject);
+        hideProject,
+        dispatcher,
+        instanceId);
     this.protectedProjects = protectedProjects;
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
index 40463b2..75a948a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/DeleteProject.java
@@ -14,13 +14,17 @@
 
 package com.googlesource.gerrit.plugins.deleteproject;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritInstanceId;
+import com.google.gerrit.server.events.EventDispatcher;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,6 +52,8 @@
   private final DeleteLog deleteLog;
   private final Configuration cfg;
   private final HideProject hideProject;
+  private final DynamicItem<EventDispatcher> dispatcher;
+  private final String instanceId;
 
   @Inject
   DeleteProject(
@@ -58,7 +64,9 @@
       DeleteLog deleteLog,
       DeletePreconditions preConditions,
       Configuration cfg,
-      HideProject hideProject) {
+      HideProject hideProject,
+      DynamicItem<EventDispatcher> dispatcher,
+      @Nullable @GerritInstanceId String instanceId) {
     this.dbHandler = dbHandler;
     this.fsHandler = fsHandler;
     this.cacheHandler = cacheHandler;
@@ -67,6 +75,8 @@
     this.preConditions = preConditions;
     this.cfg = cfg;
     this.hideProject = hideProject;
+    this.dispatcher = dispatcher;
+    this.instanceId = instanceId;
   }
 
   @Override
@@ -94,6 +104,20 @@
       } else {
         hideProject.apply(rsrc);
       }
+
+      ProjectDeletedEvent event = new ProjectDeletedEvent();
+      event.projectName = project.getName();
+      event.instanceId = instanceId;
+
+      /**
+       * EventBroker checks if user has the permission to access the project. But because this
+       * project is already deleted, check will always fail. That's why this event will be delivered
+       * only to the unrestricted listeners. Unrestricted events listeners are implementing {@link
+       * com.google.gerrit.server.events.EventListener} which allows to listen to events without
+       * user visibility restrictions. For example these events are going to be delivered to
+       * multi-site {@link com.googlesource.gerrit.plugins.multisite.event.EventHandler}
+       */
+      dispatcher.get().postEvent(project.getNameKey(), event);
     } catch (Exception e) {
       ex = e;
       throw e;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
index 4d0a802..a2e3f15 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/PluginModule.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.events.EventTypes;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.internal.UniqueAnnotations;
@@ -60,6 +61,8 @@
           .to(ArchiveRepositoryRemover.class);
     }
 
+    EventTypes.register(ProjectDeletedEvent.TYPE, ProjectDeletedEvent.class);
+
     install(
         new RestApiModule() {
           @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEvent.java b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEvent.java
new file mode 100644
index 0000000..31a41cc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEvent.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2022 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.deleteproject;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.server.events.ProjectEvent;
+
+public class ProjectDeletedEvent extends ProjectEvent {
+  public static final String TYPE = "project-deleted";
+  public String projectName;
+
+  public ProjectDeletedEvent() {
+    super(TYPE);
+  }
+
+  @Override
+  public NameKey getProjectNameKey() {
+    return Project.nameKey(projectName);
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index b72ee3a..2f0e7f6 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -35,6 +35,28 @@
 can be configured to listen to the project deletion event and to
 replicate project deletions.
 
+Event after project deletion
+-----------------------------------
+
+This plugin generates an event after project deletion. Format of
+the event:
+
+=== Project Deleted
+
+Sent after project deletion.
+
+type:: "project-deleted"
+
+projectName:: Name of the deleted project
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+*NOTE*: This event will be delivered only to the unrestricted listeners.
+Unrestricted events listeners implement
+`com.google.gerrit.server.events.EventListener` without performing any
+permission checking.
+
 Access
 ------
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEventTest.java b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEventTest.java
new file mode 100644
index 0000000..07e3ee4
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/deleteproject/ProjectDeletedEventTest.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2022 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.deleteproject;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.deleteproject.DeleteProject.Input;
+import com.googlesource.gerrit.plugins.deleteproject.cache.CacheDeleteHandler;
+import com.googlesource.gerrit.plugins.deleteproject.database.DatabaseDeleteHandler;
+import com.googlesource.gerrit.plugins.deleteproject.fs.FilesystemDeleteHandler;
+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 ProjectDeletedEventTest {
+
+  private static final NameKey PROJECT_NAME_KEY = Project.nameKey("test-project");
+  private static final String INSTANCE_ID = "test-instance-id";
+
+  @Mock private DatabaseDeleteHandler dbHandler;
+  @Mock private FilesystemDeleteHandler fsHandler;
+  @Mock private CacheDeleteHandler cacheHandler;
+  @Mock private Provider<CurrentUser> userProvider;
+  @Mock private DeleteLog deleteLog;
+  @Mock private DeletePreconditions preConditions;
+  @Mock private Configuration cfg;
+  @Mock private EventDispatcher dispatcher;
+  @Mock private DynamicItem<EventDispatcher> dispatcherProvider;
+  @Mock private HideProject hideProject;
+  @Mock private IdentifiedUser currentUser;
+  @Mock private ProjectState state;
+  @Captor private ArgumentCaptor<ProjectDeletedEvent> projectDeletedEventCaptor;
+
+  private Project project = Project.builder(PROJECT_NAME_KEY).build();
+
+  private DeleteProject objectUnderTest;
+
+  @Before
+  public void setup() throws Exception {
+    when(dispatcherProvider.get()).thenReturn(dispatcher);
+    when(userProvider.get()).thenReturn(currentUser);
+    when(state.getProject()).thenReturn(project);
+    objectUnderTest =
+        new DeleteProject(
+            dbHandler,
+            fsHandler,
+            cacheHandler,
+            userProvider,
+            deleteLog,
+            preConditions,
+            cfg,
+            hideProject,
+            dispatcherProvider,
+            INSTANCE_ID);
+  }
+
+  @Test
+  public void shouldSendProjectDeletedEventAfterProjectDeletion() throws Exception {
+    Input input = new Input();
+    input.force = false;
+    input.preserve = false;
+    objectUnderTest.doDelete(new ProjectResource(state, currentUser), input);
+
+    verify(dispatcher).postEvent(eq(PROJECT_NAME_KEY), projectDeletedEventCaptor.capture());
+    ProjectDeletedEvent event = projectDeletedEventCaptor.getValue();
+    assertThat(event.instanceId).isEqualTo(INSTANCE_ID);
+    assertThat(event.getProjectNameKey()).isEqualTo(PROJECT_NAME_KEY);
+    assertThat(event.type).isEqualTo(ProjectDeletedEvent.TYPE);
+  }
+}