diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index f7ae938b5b..1d85d6a2df 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -463,6 +463,15 @@
provided
+
+
+
+ sonia.scm.plugins
+ scm-landingpage-plugin
+ 1.0.0-SNAPSHOT
+ true
+
+
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java
index ba41ada287..40b6230261 100644
--- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java
@@ -31,7 +31,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.NotFoundException;
import sonia.scm.event.ScmEventBus;
-import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.lifecycle.Restarter;
import sonia.scm.version.Version;
@@ -64,17 +63,19 @@ public class DefaultPluginManager implements PluginManager {
private final PluginCenter center;
private final PluginInstaller installer;
private final Restarter restarter;
+ private final ScmEventBus eventBus;
private final Collection pendingInstallQueue = new ArrayList<>();
private final Collection pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@Inject
- public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter) {
+ public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) {
this.loader = loader;
this.center = center;
this.installer = installer;
this.restarter = restarter;
+ this.eventBus = eventBus;
this.computeInstallationDependencies();
}
@@ -172,8 +173,10 @@ public class DefaultPluginManager implements PluginManager {
PendingPluginInstallation pending = installer.install(plugin);
dependencyTracker.addInstalled(plugin.getDescriptor());
pendingInstallations.add(pending);
+ eventBus.post(new PluginEvent(PluginEventType.INSTALLED, plugin));
} catch (PluginInstallException ex) {
cancelPending(pendingInstallations);
+ eventBus.post(new PluginEvent(PluginEventType.INSTALLATION_FAILED, plugin));
throw ex;
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java
index efc7d9f136..16f3123157 100644
--- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java
@@ -27,10 +27,11 @@ package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import sonia.scm.event.Event;
+import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import javax.inject.Inject;
-import java.io.IOException;
import java.util.Collections;
import java.util.Set;
@@ -40,16 +41,18 @@ class PluginCenterLoader {
private final AdvancedHttpClient client;
private final PluginCenterDtoMapper mapper;
+ private final ScmEventBus eventBus;
@Inject
- public PluginCenterLoader(AdvancedHttpClient client) {
- this(client, PluginCenterDtoMapper.INSTANCE);
+ public PluginCenterLoader(AdvancedHttpClient client, ScmEventBus eventBus) {
+ this(client, PluginCenterDtoMapper.INSTANCE, eventBus);
}
@VisibleForTesting
- PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper) {
+ PluginCenterLoader(AdvancedHttpClient client, PluginCenterDtoMapper mapper, ScmEventBus eventBus) {
this.client = client;
this.mapper = mapper;
+ this.eventBus = eventBus;
}
Set load(String url) {
@@ -59,8 +62,12 @@ class PluginCenterLoader {
return mapper.map(pluginCenterDto);
} catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex);
+ eventBus.post(new PluginCenterNotAvailableEvent());
return Collections.emptySet();
}
}
+ @Event
+ class PluginCenterNotAvailableEvent {}
+
}
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginEvent.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginEvent.java
new file mode 100644
index 0000000000..45716ae04b
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginEvent.java
@@ -0,0 +1,39 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.plugin;
+
+import lombok.Getter;
+import sonia.scm.event.Event;
+
+@Getter
+@Event
+public class PluginEvent {
+ private final PluginEventType eventType;
+ private final AvailablePlugin plugin;
+ public PluginEvent(PluginEventType eventType, AvailablePlugin plugin) {
+ this.eventType = eventType;
+ this.plugin = plugin;
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginEventType.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginEventType.java
new file mode 100644
index 0000000000..2cde7c35f8
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginEventType.java
@@ -0,0 +1,29 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.plugin;
+
+enum PluginEventType {
+ INSTALLED, INSTALLATION_FAILED
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationFailedEventSubscriber.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationFailedEventSubscriber.java
new file mode 100644
index 0000000000..5fd4188606
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationFailedEventSubscriber.java
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.plugin;
+
+import com.cloudogu.scm.myevents.MyEvent;
+import com.github.legman.Subscribe;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import sonia.scm.EagerSingleton;
+import sonia.scm.event.ScmEventBus;
+import sonia.scm.xml.XmlInstantAdapter;
+
+import javax.inject.Inject;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.time.Instant;
+
+@Extension(requires = "scm-landingpage-plugin")
+@EagerSingleton
+public class PluginInstallationFailedEventSubscriber {
+
+ private final ScmEventBus eventBus;
+
+ @Inject
+ public PluginInstallationFailedEventSubscriber(ScmEventBus eventBus) {
+ this.eventBus = eventBus;
+ }
+
+ @Subscribe
+ public void handleEvent(PluginEvent pluginEvent) {
+ if (pluginEvent.getEventType() == PluginEventType.INSTALLATION_FAILED) {
+ AvailablePlugin newPlugin = pluginEvent.getPlugin();
+
+ String permission = PluginPermissions.manage().asShiroString();
+ String pluginName = newPlugin.getDescriptor().getInformation().getDisplayName();
+
+ String pluginVersion = newPlugin.getDescriptor().getInformation().getVersion();
+
+ eventBus.post(new PluginInstallationFailedEvent(permission, pluginName, pluginVersion, Instant.now()));
+ }
+ }
+
+ @XmlRootElement
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @Getter
+ @NoArgsConstructor
+ static class PluginInstallationFailedEvent extends MyEvent {
+ private String pluginName;
+ private String pluginVersion;
+ @XmlJavaTypeAdapter(XmlInstantAdapter.class)
+ private Instant date;
+
+ PluginInstallationFailedEvent(String permission, String pluginName, String pluginVersion, Instant date) {
+ super(PluginInstalledEventSubscriber.PluginInstalledEvent.class.getSimpleName(), permission);
+
+ this.pluginName = pluginName;
+ this.pluginVersion = pluginVersion;
+ this.date = date;
+ }
+ }
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstalledEventSubscriber.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstalledEventSubscriber.java
new file mode 100644
index 0000000000..4913eb80cf
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstalledEventSubscriber.java
@@ -0,0 +1,98 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.plugin;
+
+import com.cloudogu.scm.myevents.MyEvent;
+import com.github.legman.Subscribe;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import sonia.scm.EagerSingleton;
+import sonia.scm.event.ScmEventBus;
+import sonia.scm.xml.XmlInstantAdapter;
+
+import javax.inject.Inject;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.time.Instant;
+import java.util.Optional;
+
+@Extension(requires = "scm-landingpage-plugin")
+@EagerSingleton
+public class PluginInstalledEventSubscriber {
+
+ private final ScmEventBus eventBus;
+ private final PluginManager pluginManager;
+
+ @Inject
+ public PluginInstalledEventSubscriber(ScmEventBus eventBus, PluginManager pluginManager) {
+ this.eventBus = eventBus;
+ this.pluginManager = pluginManager;
+ }
+
+ @Subscribe
+ public void handleEvent(PluginEvent pluginEvent) {
+ if (pluginEvent.getEventType() == PluginEventType.INSTALLED) {
+ AvailablePlugin newPlugin = pluginEvent.getPlugin();
+ Optional installedPlugin = pluginManager.getInstalled(newPlugin.getDescriptor().getInformation().getDisplayName());
+
+ String permission = PluginPermissions.manage().asShiroString();
+
+ String pluginName = newPlugin.getDescriptor().getInformation().getName();
+
+ String previousPluginVersion = null;
+ if (installedPlugin.isPresent()) {
+ previousPluginVersion = installedPlugin.get().getDescriptor().getInformation().getVersion();
+ }
+
+ String newPluginVersion = newPlugin.getDescriptor().getInformation().getVersion();
+
+ eventBus.post(new PluginInstalledEvent(permission, pluginName, previousPluginVersion, newPluginVersion, Instant.now()));
+ }
+ }
+
+ @XmlRootElement
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @Getter
+ @NoArgsConstructor
+ static class PluginInstalledEvent extends MyEvent {
+ private String pluginName;
+ private String previousPluginVersion;
+ private String newPluginVersion;
+ @XmlJavaTypeAdapter(XmlInstantAdapter.class)
+ private Instant date;
+
+ PluginInstalledEvent(String permission, String pluginName, String previousPluginVersion, String newPluginVersion, Instant date) {
+ super(PluginInstalledEventSubscriber.PluginInstalledEvent.class.getSimpleName(), permission);
+
+ this.pluginName = pluginName;
+ this.previousPluginVersion = previousPluginVersion;
+ this.newPluginVersion = newPluginVersion;
+ this.date = date;
+ }
+ }
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryCreatedEventSubscriber.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryCreatedEventSubscriber.java
new file mode 100644
index 0000000000..746d73b2d2
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryCreatedEventSubscriber.java
@@ -0,0 +1,88 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository;
+
+import com.cloudogu.scm.myevents.MyEvent;
+import com.github.legman.Subscribe;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.apache.shiro.SecurityUtils;
+import sonia.scm.EagerSingleton;
+import sonia.scm.HandlerEventType;
+import sonia.scm.event.ScmEventBus;
+import sonia.scm.plugin.Extension;
+import sonia.scm.user.User;
+import sonia.scm.xml.XmlInstantAdapter;
+
+import javax.inject.Inject;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+import java.time.Instant;
+
+@Extension(requires = "scm-landingpage-plugin")
+@EagerSingleton
+public class RepositoryCreatedEventSubscriber {
+
+ private final ScmEventBus eventBus;
+
+ @Inject
+ public RepositoryCreatedEventSubscriber(ScmEventBus eventBus) {
+ this.eventBus = eventBus;
+ }
+
+ @Subscribe
+ public void handleEvent(RepositoryEvent event) {
+ if (event.getEventType() == HandlerEventType.CREATE) {
+ Repository eventRepo = event.getItem();
+ String permission = RepositoryPermissions.read(event.getItem()).asShiroString();
+ String repository = eventRepo.getNamespace() + "/" + eventRepo.getName();
+ String creator = SecurityUtils.getSubject().getPrincipals().oneByType(User.class).getDisplayName();
+ Instant date = Instant.now();
+
+ eventBus.post(new RepositoryCreatedEvent(permission, repository, creator, date));
+ }
+ }
+
+ @XmlRootElement
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @Getter
+ @NoArgsConstructor
+ static class RepositoryCreatedEvent extends MyEvent {
+ private String repository;
+ private String creator;
+ @XmlJavaTypeAdapter(XmlInstantAdapter.class)
+ private Instant date;
+
+ RepositoryCreatedEvent(String permission, String repository, String creator, Instant date) {
+ super(RepositoryCreatedEvent.class.getSimpleName(), permission);
+ this.repository = repository;
+ this.creator = creator;
+ this.date = date;
+ }
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java
index 8f3f7bbe57..aa4d5cb5b1 100644
--- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java
@@ -36,16 +36,19 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NotFoundException;
import sonia.scm.ScmConstraintViolationException;
+import sonia.scm.event.ScmEventBus;
import sonia.scm.lifecycle.Restarter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -53,6 +56,7 @@ import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.in;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
@@ -83,6 +87,12 @@ class DefaultPluginManagerTest {
@Mock
private Restarter restarter;
+ @Mock
+ private ScmEventBus eventBus;
+
+ @Captor
+ private ArgumentCaptor eventCaptor;
+
@InjectMocks
private DefaultPluginManager manager;
@@ -537,8 +547,36 @@ class DefaultPluginManagerTest {
verify(installer, never()).install(oldScriptPlugin);
}
+
+ @Test
+ void shouldFirePluginEventOnInstallation() {
+ AvailablePlugin review = createAvailable("scm-review-plugin");
+ when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
+
+ manager.install("scm-review-plugin", false);
+
+ verify(eventBus).post(eventCaptor.capture());
+
+ assertThat(eventCaptor.getValue().getEventType()).isEqualTo(PluginEventType.INSTALLED);
+ assertThat(eventCaptor.getValue().getPlugin()).isEqualTo(review);
+ }
+
+ @Test
+ void shouldFirePluginEventOnFailedInstallation() {
+ AvailablePlugin review = createAvailable("scm-review-plugin");
+ when(center.getAvailable()).thenReturn(ImmutableSet.of(review));
+ doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(review);
+
+ assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false));
+
+ verify(eventBus).post(eventCaptor.capture());
+ assertThat(eventCaptor.getValue().getEventType()).isEqualTo(PluginEventType.INSTALLATION_FAILED);
+ assertThat(eventCaptor.getValue().getPlugin()).isEqualTo(review);
+ }
+
}
+
@Nested
class WithoutReadPermissions {
diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java
index 01fe540206..b00241eebf 100644
--- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java
@@ -24,12 +24,16 @@
package sonia.scm.plugin;
+import com.sun.mail.iap.Argument;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import java.io.IOException;
@@ -37,6 +41,10 @@ import java.util.Collections;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -50,6 +58,9 @@ class PluginCenterLoaderTest {
@Mock
private PluginCenterDtoMapper mapper;
+ @Mock
+ private ScmEventBus eventBus;
+
@InjectMocks
private PluginCenterLoader loader;
@@ -71,4 +82,13 @@ class PluginCenterLoaderTest {
Set fetch = loader.load(PLUGIN_URL);
assertThat(fetch).isEmpty();
}
+
+ @Test
+ void shouldThrowExceptionAndFirePluginCenterNotAvailableEvent() throws IOException {
+ when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch"));
+
+ loader.load(PLUGIN_URL);
+
+ verify(eventBus).post(any(PluginCenterLoader.PluginCenterNotAvailableEvent.class));
+ }
}
diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationFailedEventSubscriberTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationFailedEventSubscriberTest.java
new file mode 100644
index 0000000000..88dff091cf
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationFailedEventSubscriberTest.java
@@ -0,0 +1,98 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.plugin;
+
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.event.ScmEventBus;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PluginInstallationFailedEventSubscriberTest {
+
+ private AvailablePlugin newPlugin = PluginTestHelper.createAvailable("scm-hitchhiker-plugin");
+
+ @Mock
+ private Subject subject;
+
+ @Mock
+ private ScmEventBus eventBus;
+
+ @Mock
+ private PluginEvent event;
+
+ @Captor
+ private ArgumentCaptor eventCaptor;
+
+ @InjectMocks
+ private PluginInstallationFailedEventSubscriber subscriber;
+
+ @BeforeEach
+ void bindSubject() {
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void tearDownSubject() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldFireMyEvent() {
+ when(event.getEventType()).thenReturn(PluginEventType.INSTALLATION_FAILED);
+ when(event.getPlugin()).thenReturn(newPlugin);
+
+ subscriber.handleEvent(event);
+
+ verify(eventBus).post(eventCaptor.capture());
+
+ PluginInstallationFailedEventSubscriber.PluginInstallationFailedEvent pluginInstalledEvent = eventCaptor.getValue();
+ assertThat(pluginInstalledEvent.getPermission()).isEqualTo("plugin:manage");
+ assertThat(pluginInstalledEvent.getPluginVersion()).isEqualTo("1.0");
+ assertThat(pluginInstalledEvent.getPluginName()).isEqualTo(newPlugin.getDescriptor().getInformation().getDisplayName());
+ }
+
+ @Test
+ void shouldNotFireMyEventWhenNotCreatedEvent() {
+ when(event.getEventType()).thenReturn(PluginEventType.INSTALLED);
+ subscriber.handleEvent(event);
+
+ verify(eventBus, never()).post(eventCaptor.capture());
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstalledEventSubscriberTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstalledEventSubscriberTest.java
new file mode 100644
index 0000000000..61348c96c3
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstalledEventSubscriberTest.java
@@ -0,0 +1,106 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.plugin;
+
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.event.ScmEventBus;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class PluginInstalledEventSubscriberTest {
+
+ private InstalledPlugin oldPlugin = PluginTestHelper.createInstalled("scm-hitchhiker-plugin");
+ private AvailablePlugin newPlugin = PluginTestHelper.createAvailable("scm-hitchhiker-plugin", "1.1");
+
+ @Mock
+ private Subject subject;
+
+ @Mock
+ private ScmEventBus eventBus;
+
+ @Mock
+ private PluginEvent event;
+
+ @Mock
+ private PluginManager pluginManager;
+
+ @Captor
+ private ArgumentCaptor eventCaptor;
+
+ @InjectMocks
+ private PluginInstalledEventSubscriber subscriber;
+
+ @BeforeEach
+ void bindSubject() {
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void tearDownSubject() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldFireMyEvent() {
+ when(event.getEventType()).thenReturn(PluginEventType.INSTALLED);
+ when(event.getPlugin()).thenReturn(newPlugin);
+ when(pluginManager.getInstalled("scm-hitchhiker-plugin")).thenReturn(Optional.of(oldPlugin));
+
+ subscriber.handleEvent(event);
+
+ verify(eventBus).post(eventCaptor.capture());
+
+ PluginInstalledEventSubscriber.PluginInstalledEvent pluginInstalledEvent = eventCaptor.getValue();
+ assertThat(pluginInstalledEvent.getPermission()).isEqualTo("plugin:manage");
+ assertThat(pluginInstalledEvent.getPreviousPluginVersion()).isEqualTo("1.0");
+ assertThat(pluginInstalledEvent.getNewPluginVersion()).isEqualTo("1.1");
+ assertThat(pluginInstalledEvent.getPluginName()).isEqualTo(newPlugin.getDescriptor().getInformation().getDisplayName());
+ }
+
+ @Test
+ void shouldNotFireMyEventWhenNotCreatedEvent() {
+ when(event.getEventType()).thenReturn(PluginEventType.INSTALLATION_FAILED);
+ subscriber.handleEvent(event);
+
+ verify(eventBus, never()).post(eventCaptor.capture());
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryCreatedEventSubscriberTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryCreatedEventSubscriberTest.java
new file mode 100644
index 0000000000..59930d1709
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryCreatedEventSubscriberTest.java
@@ -0,0 +1,109 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.repository;
+
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.HandlerEventType;
+import sonia.scm.event.ScmEventBus;
+import sonia.scm.user.User;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RepositoryCreatedEventSubscriberTest {
+
+ private Repository REPOSITORY = new Repository("1", "git", "nicest", "repo");
+
+ @Mock
+ private Subject subject;
+
+ @Mock
+ private ScmEventBus eventBus;
+
+ @Mock
+ private RepositoryEvent event;
+
+ @Mock
+ private PrincipalCollection principalCollection;
+
+ @Captor
+ private ArgumentCaptor eventCaptor;
+
+ @InjectMocks
+ private RepositoryCreatedEventSubscriber subscriber;
+
+ @BeforeEach
+ void bindSubject() {
+ ThreadContext.bind(subject);
+ }
+
+ @AfterEach
+ void tearDownSubject() {
+ ThreadContext.unbindSubject();
+ }
+
+ @Test
+ void shouldFireMyEvent() {
+ User trillian = new User("trillian", "Trillian", "tricia@hitchhiker.org");
+
+ when(event.getEventType()).thenReturn(HandlerEventType.CREATE);
+ when(event.getItem()).thenReturn(REPOSITORY);
+ when(subject.getPrincipals()).thenReturn(principalCollection);
+ when(principalCollection.oneByType(User.class)).thenReturn(trillian);
+
+ subscriber.handleEvent(event);
+
+ verify(eventBus).post(eventCaptor.capture());
+
+ RepositoryCreatedEventSubscriber.RepositoryCreatedEvent repositoryCreatedEvent = eventCaptor.getValue();
+ assertThat(repositoryCreatedEvent.getPermission()).isEqualTo("repository:read:1");
+ assertThat(repositoryCreatedEvent.getRepository()).isEqualTo(REPOSITORY.getNamespace() + "/" + REPOSITORY.getName());
+ assertThat(repositoryCreatedEvent.getType()).isEqualTo(RepositoryCreatedEventSubscriber.RepositoryCreatedEvent.class.getSimpleName());
+ assertThat(repositoryCreatedEvent.getCreator()).isEqualTo("Trillian");
+ }
+
+ @Test
+ void shouldNotFireMyEventWhenNotCreatedEvent() {
+ when(event.getEventType()).thenReturn(HandlerEventType.BEFORE_CREATE);
+ subscriber.handleEvent(event);
+
+ verify(eventBus, never()).post(eventCaptor.capture());
+ }
+
+}
diff --git a/yarn.lock b/yarn.lock
index 1f60f6bbed..61c247fdc5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2047,17 +2047,16 @@
dependencies:
"@types/node" ">= 8"
-"@pmmmwh/react-refresh-webpack-plugin@^0.1.3":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.1.3.tgz#bb6815315028087e6af4f96d063376880caf9c82"
- integrity sha512-FJ8WzpGrao8Gz8KNAeU9dcTYr1RjbAGnXJMKKXTp4oAw494SqQK4HyGT8HMmIQt0ayukuP+A71w1oV8/5xSAWQ==
+"@pmmmwh/react-refresh-webpack-plugin@^0.2.0":
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.2.0.tgz#e2a684d430f74ad6465680d9a5869f52f307ec1e"
+ integrity sha512-rjdNzcWroULJeD/Y0+eETy9LhM7c5tbPF+wqT5G680rwDkh3iothIPEqGAuEE2WJlXEaAq293aO6ySzsIU518Q==
dependencies:
ansi-html "^0.0.7"
error-stack-parser "^2.0.4"
html-entities "^1.2.1"
lodash.debounce "^4.0.8"
react-dev-utils "^9.1.0"
- sockjs-client "^1.4.0"
"@reach/router@^1.2.1":
version "1.2.1"
@@ -13389,7 +13388,7 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
-sockjs-client@1.4.0, sockjs-client@^1.4.0:
+sockjs-client@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5"
integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==