🔀 Merge “next” into “feature/notification-handlers”
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 2 Jan 2020 21:00:17 +0000 (22:00 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 2 Jan 2020 21:00:17 +0000 (22:00 +0100)
62 files changed:
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/main/SoneModule.kt
src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt [new file with mode: 0644]
src/main/resources/i18n/sone.de.properties
src/main/resources/i18n/sone.en.properties
src/main/resources/i18n/sone.es.properties
src/main/resources/i18n/sone.fr.properties
src/main/resources/i18n/sone.ja.properties
src/main/resources/i18n/sone.no.properties
src/main/resources/i18n/sone.pl.properties
src/main/resources/i18n/sone.ru.properties
src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/FreenetModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/main/SoneModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt
src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/Guice.kt
src/test/kotlin/net/pterodactylus/sone/test/Matchers.kt
src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/PageToadletRegistryTest.kt
src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt [deleted file]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt [new file with mode: 0644]

index a3cb385..d3dd3bd 100644 (file)
@@ -22,11 +22,15 @@ import static java.util.logging.Logger.*;
 import java.util.logging.Logger;
 import java.util.logging.*;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.sone.core.*;
+import net.pterodactylus.sone.core.event.*;
 import net.pterodactylus.sone.fcp.*;
 import net.pterodactylus.sone.freenet.wot.*;
 import net.pterodactylus.sone.web.*;
 import net.pterodactylus.sone.web.notification.NotificationHandler;
+import net.pterodactylus.sone.web.notification.NotificationHandlerModule;
 
 import freenet.l10n.BaseL10n.*;
 import freenet.l10n.*;
@@ -58,7 +62,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                        private final LoadingCache<String, Class<?>> classCache = CacheBuilder.newBuilder()
                                        .build(new CacheLoader<String, Class<?>>() {
                                                @Override
-                                               public Class<?> load(String key) throws Exception {
+                                               public Class<?> load(@Nonnull String key) throws Exception {
                                                        return SonePlugin.class.getClassLoader().loadClass(key);
                                                }
                                        });
@@ -106,6 +110,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        /** The core. */
        private Core core;
 
+       /** The event bus. */
+       private EventBus eventBus;
+
        /** The web interface. */
        private WebInterface webInterface;
 
@@ -197,16 +204,33 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                /* create the web interface. */
                webInterface = injector.getInstance(WebInterface.class);
-               NotificationHandler notificationHandler = injector.getInstance(NotificationHandler.class);
+
+               /* we need to request this to install all notification handlers. */
+               injector.getInstance(NotificationHandler.class);
+
+               /* and this is required to shutdown all tickers. */
+               injector.getInstance(TickerShutdown.class);
 
                /* start core! */
                core.start();
 
                /* start the web interface! */
                webInterface.start();
-               webInterface.setFirstStart(injector.getInstance(Key.get(Boolean.class, Names.named("FirstStart"))));
-               webInterface.setNewConfig(injector.getInstance(Key.get(Boolean.class, Names.named("NewConfig"))));
-               notificationHandler.start();
+
+               /* send some events on startup */
+               eventBus = injector.getInstance(EventBus.class);
+
+               /* first start? */
+               if (injector.getInstance(Key.get(Boolean.class, Names.named("FirstStart")))) {
+                       eventBus.post(new FirstStart());
+               } else {
+                       /* new config? */
+                       if (injector.getInstance(Key.get(Boolean.class, Names.named("NewConfig")))) {
+                               eventBus.post(new ConfigNotRead());
+                       }
+               }
+
+               eventBus.post(new Startup());
        }
 
        @VisibleForTesting
@@ -214,8 +238,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
                FreenetModule freenetModule = new FreenetModule(pluginRespirator);
                AbstractModule soneModule = new SoneModule(this, new EventBus());
                Module webInterfaceModule = new WebInterfaceModule();
+               Module notificationHandlerModule = new NotificationHandlerModule();
 
-               return createInjector(freenetModule, soneModule, webInterfaceModule);
+               return createInjector(freenetModule, soneModule, webInterfaceModule, notificationHandlerModule);
        }
 
        @VisibleForTesting
@@ -228,6 +253,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
         */
        @Override
        public void terminate() {
+               /* send shutdown event. */
+               eventBus.post(new Shutdown());
+
                try {
                        /* stop the web interface. */
                        webInterface.stop();
index bcc7a11..50c63ee 100644 (file)
@@ -21,25 +21,20 @@ import static com.google.common.collect.FluentIterable.from;
 import static java.util.logging.Logger.getLogger;
 
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.UUID;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Logger;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.inject.Named;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.ElementLoader;
 import net.pterodactylus.sone.core.event.*;
-import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Sone;
@@ -157,9 +152,6 @@ public class WebInterface implements SessionProvider {
        private final MetricRegistry metricRegistry;
        private final Translation translation;
 
-       /** The “new Sone” notification. */
-       private final ListNotification<Sone> newSoneNotification;
-
        /** The “new post” notification. */
        private final ListNotification<Post> newPostNotification;
 
@@ -178,27 +170,6 @@ public class WebInterface implements SessionProvider {
        /** Notifications for sone inserts. */
        private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
 
-       /** Sone locked notification ticker objects. */
-       private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
-
-       /** The “Sone locked” notification. */
-       private final ListNotification<Sone> lockedSonesNotification;
-
-       /** The “new version” notification. */
-       private final TemplateNotification newVersionNotification;
-
-       /** The “inserting images” notification. */
-       private final ListNotification<Image> insertingImagesNotification;
-
-       /** The “inserted images” notification. */
-       private final ListNotification<Image> insertedImagesNotification;
-
-       /** The “image insert failed” notification. */
-       private final ListNotification<Image> imageInsertFailedNotification;
-
-       /** Scheduled executor for time-based notifications. */
-       private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
-
        @Inject
        public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
                        PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
@@ -208,7 +179,8 @@ public class WebInterface implements SessionProvider {
                        RenderFilter renderFilter,
                        LinkedElementRenderFilter linkedElementRenderFilter,
                        PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
-                       NotificationManager notificationManager) {
+                       NotificationManager notificationManager, @Named("newRemotePost") ListNotification<Post> newPostNotification,
+                       @Named("localPost") ListNotification<Post> localPostNotification) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
@@ -225,6 +197,8 @@ public class WebInterface implements SessionProvider {
                this.l10nFilter = l10nFilter;
                this.translation = translation;
                this.notificationManager = notificationManager;
+               this.newPostNotification = newPostNotification;
+               this.localPostNotification = localPostNotification;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
                soneTextParser = new SoneTextParser(getCore(), getCore());
 
@@ -233,15 +207,6 @@ public class WebInterface implements SessionProvider {
                templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
-               Template newSoneNotificationTemplate = loaders.loadTemplate("/templates/notify/newSoneNotification.html");
-               newSoneNotification = new ListNotification<>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
-
-               Template newPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
-               newPostNotification = new ListNotification<>("new-post-notification", "posts", newPostNotificationTemplate, false);
-
-               Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
-               localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
-
                Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
                newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
 
@@ -250,21 +215,6 @@ public class WebInterface implements SessionProvider {
 
                Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
                mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
-
-               Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
-               lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
-
-               Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
-               newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
-
-               Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
-               insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
-
-               Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
-               insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
-
-               Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
-               imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
        }
 
        //
@@ -402,16 +352,6 @@ public class WebInterface implements SessionProvider {
                return formPassword;
        }
 
-       /**
-        * Returns the posts that have been announced as new in the
-        * {@link #newPostNotification}.
-        *
-        * @return The new posts
-        */
-       public Set<Post> getNewPosts() {
-               return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
-       }
-
        @Nonnull
        public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
                Set<Post> allNewPosts = ImmutableSet.<Post> builder()
@@ -440,37 +380,6 @@ public class WebInterface implements SessionProvider {
                return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
        }
 
-       /**
-        * Sets whether the current start of the plugin is the first start. It is
-        * considered a first start if the configuration file does not exist.
-        *
-        * @param firstStart
-        *            {@code true} if no configuration file existed when Sone was
-        *            loaded, {@code false} otherwise
-        */
-       public void setFirstStart(boolean firstStart) {
-               if (firstStart) {
-                       Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
-                       Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
-                       notificationManager.addNotification(firstStartNotification);
-               }
-       }
-
-       /**
-        * Sets whether Sone was started with a fresh configuration file.
-        *
-        * @param newConfig
-        *            {@code true} if Sone was started with a fresh configuration,
-        *            {@code false} if the existing configuration could be read
-        */
-       public void setNewConfig(boolean newConfig) {
-               if (newConfig && !hasFirstStartNotification()) {
-                       Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
-                       Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
-                       notificationManager.addNotification(configNotReadNotification);
-               }
-       }
-
        //
        // PRIVATE ACCESSORS
        //
@@ -494,36 +403,6 @@ public class WebInterface implements SessionProvider {
         */
        public void start() {
                registerToadlets();
-
-               /* notification templates. */
-               Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
-
-               final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
-               notificationManager.addNotification(startupNotification);
-
-               ticker.schedule(new Runnable() {
-
-                       @Override
-                       public void run() {
-                               startupNotification.dismiss();
-                       }
-               }, 2, TimeUnit.MINUTES);
-
-               Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
-               final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
-               ticker.scheduleAtFixedRate(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               if (getCore().getIdentityManager().isConnected()) {
-                                       wotMissingNotification.dismiss();
-                               } else {
-                                       notificationManager.addNotification(wotMissingNotification);
-                               }
-                       }
-
-               }, 15, 15, TimeUnit.SECONDS);
        }
 
        /**
@@ -531,7 +410,6 @@ public class WebInterface implements SessionProvider {
         */
        public void stop() {
                pageToadletRegistry.unregisterToadlets();
-               ticker.shutdownNow();
        }
 
        //
@@ -690,20 +568,6 @@ public class WebInterface implements SessionProvider {
        //
 
        /**
-        * Notifies the web interface that a new {@link Sone} was found.
-        *
-        * @param newSoneFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void newSoneFound(NewSoneFoundEvent newSoneFoundEvent) {
-               newSoneNotification.add(newSoneFoundEvent.getSone());
-               if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(newSoneNotification);
-               }
-       }
-
-       /**
         * Notifies the web interface that a new {@link Post} was found.
         *
         * @param newPostFoundEvent
@@ -713,19 +577,11 @@ public class WebInterface implements SessionProvider {
        public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
                Post post = newPostFoundEvent.getPost();
                boolean isLocal = post.getSone().isLocal();
-               if (isLocal) {
-                       localPostNotification.add(post);
-               } else {
-                       newPostNotification.add(post);
-               }
                if (!hasFirstStartNotification()) {
-                       notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
                        if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
                                mentionNotification.add(post);
                                notificationManager.addNotification(mentionNotification);
                        }
-               } else {
-                       getCore().markPostKnown(post);
                }
        }
 
@@ -755,17 +611,6 @@ public class WebInterface implements SessionProvider {
                }
        }
 
-       /**
-        * Notifies the web interface that a {@link Sone} was marked as known.
-        *
-        * @param markSoneKnownEvent
-        *            The event
-        */
-       @Subscribe
-       public void markSoneKnown(MarkSoneKnownEvent markSoneKnownEvent) {
-               newSoneNotification.remove(markSoneKnownEvent.getSone());
-       }
-
        @Subscribe
        public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
                removePost(markPostKnownEvent.getPost());
@@ -777,18 +622,12 @@ public class WebInterface implements SessionProvider {
        }
 
        @Subscribe
-       public void soneRemoved(SoneRemovedEvent soneRemovedEvent) {
-               newSoneNotification.remove(soneRemovedEvent.getSone());
-       }
-
-       @Subscribe
        public void postRemoved(PostRemovedEvent postRemovedEvent) {
                removePost(postRemovedEvent.getPost());
        }
 
        private void removePost(Post post) {
                newPostNotification.remove(post);
-               localPostNotification.remove(post);
                if (!localSoneMentionedInNewPostOrReply(post)) {
                        mentionNotification.remove(post);
                }
@@ -808,39 +647,6 @@ public class WebInterface implements SessionProvider {
        }
 
        /**
-        * Notifies the web interface that a Sone was locked.
-        *
-        * @param soneLockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneLocked(SoneLockedEvent soneLockedEvent) {
-               final Sone sone = soneLockedEvent.getSone();
-               ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
-
-                       @Override
-                       @SuppressWarnings("synthetic-access")
-                       public void run() {
-                               lockedSonesNotification.add(sone);
-                               notificationManager.addNotification(lockedSonesNotification);
-                       }
-               }, 5, TimeUnit.MINUTES);
-               lockedSonesTickerObjects.put(sone, tickerObject);
-       }
-
-       /**
-        * Notifies the web interface that a Sone was unlocked.
-        *
-        * @param soneUnlockedEvent
-        *            The event
-        */
-       @Subscribe
-       public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
-               lockedSonesNotification.remove(soneUnlockedEvent.getSone());
-               lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
-       }
-
-       /**
         * Notifies the web interface that a {@link Sone} is being inserted.
         *
         * @param soneInsertingEvent
@@ -887,70 +693,6 @@ public class WebInterface implements SessionProvider {
                }
        }
 
-       /**
-        * Notifies the web interface that a new Sone version was found.
-        *
-        * @param updateFoundEvent
-        *            The event
-        */
-       @Subscribe
-       public void updateFound(UpdateFoundEvent updateFoundEvent) {
-               newVersionNotification.set("latestVersion", updateFoundEvent.getVersion());
-               newVersionNotification.set("latestEdition", updateFoundEvent.getLatestEdition());
-               newVersionNotification.set("releaseTime", updateFoundEvent.getReleaseTime());
-               newVersionNotification.set("disruptive", updateFoundEvent.isDisruptive());
-               notificationManager.addNotification(newVersionNotification);
-       }
-
-       /**
-        * Notifies the web interface that an image insert was started
-        *
-        * @param imageInsertStartedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
-               insertingImagesNotification.add(imageInsertStartedEvent.getImage());
-               notificationManager.addNotification(insertingImagesNotification);
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert was aborted.
-        *
-        * @param imageInsertAbortedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
-               insertingImagesNotification.remove(imageInsertAbortedEvent.getImage());
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert is finished.
-        *
-        * @param imageInsertFinishedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
-               insertingImagesNotification.remove(imageInsertFinishedEvent.getImage());
-               insertedImagesNotification.add(imageInsertFinishedEvent.getImage());
-               notificationManager.addNotification(insertedImagesNotification);
-       }
-
-       /**
-        * Notifies the web interface that an {@link Image} insert has failed.
-        *
-        * @param imageInsertFailedEvent
-        *            The event
-        */
-       @Subscribe
-       public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
-               insertingImagesNotification.remove(imageInsertFailedEvent.getImage());
-               imageInsertFailedNotification.add(imageInsertFailedEvent.getImage());
-               notificationManager.addNotification(imageInsertFailedNotification);
-       }
-
        @Subscribe
        public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
                pageToadletRegistry.activateDebugMode();
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/ConfigNotRead.kt
new file mode 100644 (file)
index 0000000..c8f1573
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Sone - ConfigNotRead.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that Sone could not read an existing configuration
+ * successfully, and a new configuration was created. This is different from
+ * [FirstStart] in that `FirstStart` signals that there *was* no existing
+ * configuration to be read.
+ */
+class ConfigNotRead
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/FirstStart.kt
new file mode 100644 (file)
index 0000000..4ff81f3
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * Sone - FirstStart.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that Sone was started for the first time. This event
+ * will only be triggered once, on startup.
+ */
+class FirstStart
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Shutdown.kt
new file mode 100644 (file)
index 0000000..4bc2f61
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Shutdown.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals the shutdown of Sone.
+ */
+class Shutdown
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/Startup.kt
new file mode 100644 (file)
index 0000000..9a9d273
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Startup.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals the startup of Sone.
+ */
+class Startup
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustAppeared.kt
new file mode 100644 (file)
index 0000000..97daff1
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - WebOfTrustAppeared.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the web of trust is reachable.
+ */
+class WebOfTrustAppeared
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/WebOfTrustDisappeared.kt
new file mode 100644 (file)
index 0000000..63b4a1f
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - WebOfTrustDisappeared.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core.event
+
+/**
+ * Event that signals that the web of trust is not reachable.
+ */
+class WebOfTrustDisappeared
diff --git a/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt b/src/main/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPinger.kt
new file mode 100644 (file)
index 0000000..026389f
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Sone - WebOfTrustPinger.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.utils.*
+import java.util.concurrent.atomic.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * [Runnable] that is scheduled via an [Executor][java.util.concurrent.Executor],
+ * checks whether the web of trust plugin can be communicated with, sends
+ * events if its status changes and reschedules itself.
+ */
+class WebOfTrustPinger @Inject constructor(
+               private val eventBus: EventBus,
+               @Named("webOfTrustReacher") private val webOfTrustReacher: Runnable,
+               @Named("webOfTrustReschedule") private val reschedule: Consumer<Runnable>) : Runnable {
+
+       private val lastState = AtomicBoolean(false)
+
+       override fun run() {
+               try {
+                       webOfTrustReacher()
+                       if (!lastState.get()) {
+                               eventBus.post(WebOfTrustAppeared())
+                               lastState.set(true)
+                       }
+               } catch (e: PluginException) {
+                       if (lastState.get()) {
+                               eventBus.post(WebOfTrustDisappeared())
+                               lastState.set(false)
+                       }
+               }
+               reschedule(this)
+       }
+
+}
index 97b342a..749de0d 100644 (file)
@@ -7,15 +7,20 @@ import com.google.inject.*
 import com.google.inject.matcher.*
 import com.google.inject.name.Names.*
 import com.google.inject.spi.*
-import freenet.l10n.*
 import net.pterodactylus.sone.database.*
 import net.pterodactylus.sone.database.memory.*
 import net.pterodactylus.sone.freenet.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.util.config.*
 import net.pterodactylus.util.config.ConfigurationException
+import net.pterodactylus.util.logging.*
 import net.pterodactylus.util.version.Version
 import java.io.*
+import java.util.concurrent.*
+import java.util.concurrent.Executors.*
+import java.util.logging.*
+import javax.inject.*
+import javax.inject.Singleton
 
 open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus: EventBus) : AbstractModule() {
 
@@ -55,14 +60,26 @@ open class SoneModule(private val sonePlugin: SonePlugin, private val eventBus:
                loaders?.let { bind(Loaders::class.java).toInstance(it) }
                bind(MetricRegistry::class.java).`in`(Singleton::class.java)
                bind(WebOfTrustConnector::class.java).to(PluginWebOfTrustConnector::class.java).`in`(Singleton::class.java)
+               bind(TickerShutdown::class.java).`in`(Singleton::class.java)
 
                bindListener(Matchers.any(), object : TypeListener {
                        override fun <I> hear(typeLiteral: TypeLiteral<I>, typeEncounter: TypeEncounter<I>) {
-                               typeEncounter.register(InjectionListener { injectee -> eventBus.register(injectee) })
+                               typeEncounter.register(InjectionListener { injectee ->
+                                       logger.fine { "Injecting $injectee..." }
+                                       eventBus.register(injectee)
+                               })
                        }
                })
        }
 
+       @Provides
+       @Singleton
+       @Named("notification")
+       fun getNotificationTicker(): ScheduledExecutorService =
+                       newSingleThreadScheduledExecutor()
+
+       private val logger: Logger = Logging.getLogger(javaClass)
+
 }
 
 private fun String.parseVersion(): Version = Version.parse(this)
diff --git a/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt b/src/main/kotlin/net/pterodactylus/sone/main/TickerShutdown.kt
new file mode 100644 (file)
index 0000000..5f96050
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Sone - TickerShutdown.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.main
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import java.util.concurrent.*
+import javax.inject.*
+
+/**
+ * Wrapper around all [tickers][ScheduledExecutorService] used in Sone,
+ * ensuring proper shutdown.
+ */
+class TickerShutdown @Inject constructor(@Named("notification") private val notificationTicker: ScheduledExecutorService) {
+
+       @Subscribe
+       fun shutdown(shutdown: Shutdown) {
+               notificationTicker.shutdown()
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt b/src/main/kotlin/net/pterodactylus/sone/notify/Notifications.kt
new file mode 100644 (file)
index 0000000..329eedd
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Sone - Notifications.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.notify
+
+import net.pterodactylus.util.notify.*
+
+/**
+ * Returns whether the notification manager contains a notification with the given ID.
+ */
+operator fun NotificationManager.contains(id: String) =
+               getNotification(id) != null
+
+/**
+ * Returns whether the notification manager currently has a “first start” notification.
+ */
+fun NotificationManager.hasFirstStartNotification() =
+               "first-start-notification" in this
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Functions.kt
new file mode 100644 (file)
index 0000000..99f43b6
--- /dev/null
@@ -0,0 +1,9 @@
+package net.pterodactylus.sone.utils
+
+import java.util.function.*
+
+/** Allows easy invocation of Java Consumers. */
+operator fun <T> Consumer<T>.invoke(t: T) = accept(t)
+
+/** Allows easy invocation of Java Runnables. */
+operator fun Runnable.invoke() = run()
index 63f3fc6..3d87aa7 100644 (file)
@@ -1,6 +1,5 @@
 package net.pterodactylus.sone.web
 
-import com.google.common.eventbus.*
 import com.google.inject.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
@@ -11,7 +10,6 @@ import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.template.*
 import net.pterodactylus.sone.text.*
-import net.pterodactylus.sone.web.notification.*
 import net.pterodactylus.util.notify.*
 import net.pterodactylus.util.template.*
 import javax.inject.*
@@ -134,9 +132,4 @@ class WebInterfaceModule : AbstractModule() {
        fun getNotificationManager() =
                        NotificationManager()
 
-       @Provides
-       @Singleton
-       fun getNotificationHandler(eventBus: EventBus, loaders: Loaders, notificationManager: NotificationManager) =
-                       NotificationHandler(eventBus, loaders, notificationManager)
-
 }
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandler.kt
new file mode 100644 (file)
index 0000000..46961c0
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Sone - ConfigNotReadHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [ConfigNotRead] events.
+ */
+class ConfigNotReadHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("configNotRead") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun configNotRead(configNotRead: ConfigNotRead) {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandler.kt
new file mode 100644 (file)
index 0000000..aceda7e
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Sone - FirstStartHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handles the notification shown on first start of Sone.
+ */
+class FirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("firstStart") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun firstStart(firstStart: FirstStart) {
+               notificationManager.addNotification(notification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandler.kt
new file mode 100644 (file)
index 0000000..0d36de6
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - ImageInsertHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Notification handler for the various image-insert-related events.
+ *
+ * @see ImageInsertStartedEvent
+ * @see ImageInsertAbortedEvent
+ * @see ImageInsertFailedEvent
+ * @see ImageInsertFinishedEvent
+ */
+class ImageInsertHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("imageInserting") private val imageInsertingNotification: ListNotification<Image>,
+               @Named("imageFailed") private val imageFailedNotification: ListNotification<Image>,
+               @Named("imageInserted") private val imageInsertedNotification: ListNotification<Image>) {
+
+       @Subscribe
+       fun imageInsertStarted(imageInsertStartedEvent: ImageInsertStartedEvent) {
+               imageInsertingNotification.add(imageInsertStartedEvent.image)
+               notificationManager.addNotification(imageInsertingNotification)
+       }
+
+       @Subscribe
+       fun imageInsertAborted(imageInsertAbortedEvent: ImageInsertAbortedEvent) {
+               imageInsertingNotification.remove(imageInsertAbortedEvent.image)
+       }
+
+       @Subscribe
+       fun imageInsertFailed(imageInsertFailedEvent: ImageInsertFailedEvent) {
+               imageInsertingNotification.remove(imageInsertFailedEvent.image)
+               imageFailedNotification.add(imageInsertFailedEvent.image)
+               notificationManager.addNotification(imageFailedNotification)
+       }
+
+       @Subscribe
+       fun imageInsertFinished(imageInsertFinishedEvent: ImageInsertFinishedEvent) {
+               imageInsertingNotification.remove(imageInsertFinishedEvent.image)
+               imageInsertedNotification.add(imageInsertFinishedEvent.image)
+               notificationManager.addNotification(imageInsertedNotification)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandler.kt
new file mode 100644 (file)
index 0000000..a5153ce
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Sone - NewLocalPostHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for local posts.
+ */
+class LocalPostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("localPost") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               newPostFoundEvent.post.onLocal { post ->
+                       notification.add(post)
+               }
+               if (!notificationManager.hasFirstStartNotification()) {
+                       notificationManager.addNotification(notification)
+               }
+       }
+
+       @Subscribe
+       fun postRemoved(postRemovedEvent: PostRemovedEvent) {
+               postRemovedEvent.post.onLocal { post ->
+                       notification.remove(post)
+               }
+       }
+
+       @Subscribe
+       fun postMarkedAsKnown(markPostKnownEvent: MarkPostKnownEvent) {
+               markPostKnownEvent.post.onLocal { post ->
+                       notification.remove(post)
+               }
+       }
+
+}
+
+private fun Post.onLocal(action: (Post) -> Unit) =
+               if (sone.isLocal) action(this) else Unit
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandler.kt
new file mode 100644 (file)
index 0000000..caca76e
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Sone - MarkPostKnownDuringFirstStartHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import java.util.function.*
+import javax.inject.*
+
+/**
+ * Handler that marks a [new][NewPostFoundEvent] [post][Post] as known while
+ * the [notification manager][NotificationManager] shows a [first start notification]
+ * [NotificationManager.hasFirstStartNotification].
+ */
+class MarkPostKnownDuringFirstStartHandler @Inject constructor(private val notificationManager: NotificationManager, private val markPostAsKnown: Consumer<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               if (notificationManager.hasFirstStartNotification()) {
+                       markPostAsKnown(newPostFoundEvent.post)
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandler.kt
new file mode 100644 (file)
index 0000000..0c2368a
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Sone - NewRemotePostHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for [NewPostFoundEvent]s that adds the new post to the “new posts” notification and
+ * displays the notification if the “first start” notification is not being shown.
+ */
+class NewRemotePostHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newRemotePost") private val notification: ListNotification<Post>) {
+
+       @Subscribe
+       fun newPostFound(newPostFoundEvent: NewPostFoundEvent) {
+               if (!newPostFoundEvent.post.sone.isLocal) {
+                       notification.add(newPostFoundEvent.post)
+                       if (!notificationManager.hasFirstStartNotification()) {
+                               notificationManager.addNotification(notification)
+                       }
+               }
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandler.kt
new file mode 100644 (file)
index 0000000..f47d456
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Sone - NewSoneHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Notification handler for “new Sone discovered” events.
+ */
+class NewSoneHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newSone") private val notification: ListNotification<Sone>) {
+
+       @Subscribe
+       fun newSoneFound(newSoneFoundEvent: NewSoneFoundEvent) {
+               if (!notificationManager.hasFirstStartNotification()) {
+                       notification.add(newSoneFoundEvent.sone)
+                       notificationManager.addNotification(notification)
+               }
+       }
+
+       @Subscribe
+       fun markedSoneKnown(markSoneKnownEvent: MarkSoneKnownEvent) {
+               notification.remove(markSoneKnownEvent.sone)
+       }
+
+       @Subscribe
+       fun soneRemoved(soneRemovedEvent: SoneRemovedEvent) {
+               notification.remove(soneRemovedEvent.sone)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandler.kt
new file mode 100644 (file)
index 0000000..95ef0ef
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Sone - NewVersionHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for the “new version” notification.
+ */
+class NewVersionHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("newVersion") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun newVersionFound(updateFoundEvent: UpdateFoundEvent) {
+               notification.set("latestVersion", updateFoundEvent.version)
+               notification.set("releaseTime", updateFoundEvent.releaseTime)
+               notification.set("latestEdition", updateFoundEvent.latestEdition)
+               notification.set("disruptive", updateFoundEvent.isDisruptive)
+               notificationManager.addNotification(notification)
+       }
+
+}
index 7f81f9a..21a5b37 100644 (file)
 
 package net.pterodactylus.sone.web.notification
 
-import com.google.common.eventbus.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.util.notify.*
+import net.pterodactylus.sone.freenet.wot.*
 import javax.inject.*
 
 /**
- * Handler for notifications that can create notifications and register them with an event bus.
+ * Container that causes notification handlers to be created and (more importantly) registered
+ * on creation with the event bus.
  */
-@Suppress("UnstableApiUsage")
-class NotificationHandler @Inject constructor(private val eventBus: EventBus, private val loaders: Loaders, private val notificationManager: NotificationManager) {
-
-       fun start() {
-               SoneLockedOnStartupHandler(notificationManager, loaders.loadTemplate("/templates/notify/soneLockedOnStartupNotification.html"))
-                               .also(eventBus::register)
-       }
-
-}
+@Suppress("UNUSED_PARAMETER")
+class NotificationHandler @Inject constructor(
+               markPostKnownDuringFirstStartHandler: MarkPostKnownDuringFirstStartHandler,
+               newSoneHandler: NewSoneHandler,
+               newRemotePostHandler: NewRemotePostHandler,
+               soneLockedOnStartupHandler: SoneLockedOnStartupHandler,
+               soneLockedHandler: SoneLockedHandler,
+               newVersionHandler: NewVersionHandler,
+               imageInsertHandler: ImageInsertHandler,
+               firstStartHandler: FirstStartHandler,
+               configNotReadHandler: ConfigNotReadHandler,
+               startupHandler: StartupHandler,
+               webOfTrustPinger: WebOfTrustPinger,
+               webOfTrustHandler: WebOfTrustHandler
+)
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModule.kt
new file mode 100644 (file)
index 0000000..74c9f43
--- /dev/null
@@ -0,0 +1,150 @@
+/**
+ * Sone - NotificationHandlerModuleTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.inject.*
+import com.google.inject.binder.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import java.util.function.*
+import javax.inject.*
+import javax.inject.Singleton
+
+/**
+ * Guice module for creating all notification handlers.
+ */
+class NotificationHandlerModule : AbstractModule() {
+
+       override fun configure() {
+               bind(NotificationHandler::class.java).`in`(Singleton::class.java)
+               bind<MarkPostKnownDuringFirstStartHandler>().asSingleton()
+               bind<SoneLockedOnStartupHandler>().asSingleton()
+               bind<NewSoneHandler>().asSingleton()
+               bind<NewRemotePostHandler>().asSingleton()
+               bind<SoneLockedHandler>().asSingleton()
+               bind<LocalPostHandler>().asSingleton()
+               bind<NewVersionHandler>().asSingleton()
+               bind<ImageInsertHandler>().asSingleton()
+               bind<FirstStartHandler>().asSingleton()
+               bind<ConfigNotReadHandler>().asSingleton()
+               bind<StartupHandler>().asSingleton()
+               bind<WebOfTrustHandler>().asSingleton()
+       }
+
+       @Provides
+       fun getMarkPostKnownHandler(core: Core): Consumer<Post> = Consumer { core.markPostKnown(it) }
+
+       @Provides
+       @Singleton
+       @Named("soneLockedOnStartup")
+       fun getSoneLockedOnStartupNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sone-locked-on-startup", "sones", loaders.loadTemplate("/templates/notify/soneLockedOnStartupNotification.html"))
+
+       @Provides
+       @Named("newSone")
+       fun getNewSoneNotification(loaders: Loaders) =
+                       ListNotification<Sone>("new-sone-notification", "sones", loaders.loadTemplate("/templates/notify/newSoneNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newRemotePost")
+       fun getNewPostNotification(loaders: Loaders) =
+                       ListNotification<Post>("new-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("soneLocked")
+       fun getSoneLockedNotification(loaders: Loaders) =
+                       ListNotification<Sone>("sones-locked-notification", "sones", loaders.loadTemplate("/templates/notify/lockedSonesNotification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("localPost")
+       fun getLocalPostNotification(loaders: Loaders) =
+                       ListNotification<Post>("local-post-notification", "posts", loaders.loadTemplate("/templates/notify/newPostNotification.html"), dismissable = false)
+
+       @Provides
+       @Singleton
+       @Named("newVersion")
+       fun getNewVersionNotification(loaders: Loaders) =
+                       TemplateNotification("new-version-notification", loaders.loadTemplate("/templates/notify/newVersionNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("imageInserting")
+       fun getImageInsertingNotification(loaders: Loaders) =
+                       ListNotification<Image>("inserting-images-notification", "images", loaders.loadTemplate("/templates/notify/inserting-images-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("imageFailed")
+       fun getImageInsertingFailedNotification(loaders: Loaders) =
+                       ListNotification<Image>("image-insert-failed-notification", "images", loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("imageInserted")
+       fun getImageInsertedNotification(loaders: Loaders) =
+                       ListNotification<Image>("inserted-images-notification", "images", loaders.loadTemplate("/templates/notify/inserted-images-notification.html"), dismissable = true)
+
+       @Provides
+       @Singleton
+       @Named("firstStart")
+       fun getFirstStartNotification(loaders: Loaders) =
+                       TemplateNotification("first-start-notification", loaders.loadTemplate("/templates/notify/firstStartNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("configNotRead")
+       fun getConfigNotReadNotification(loaders: Loaders) =
+                       TemplateNotification("config-not-read-notification", loaders.loadTemplate("/templates/notify/configNotReadNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("startup")
+       fun getStartupNotification(loaders: Loaders) =
+                       TemplateNotification("startup-notification", loaders.loadTemplate("/templates/notify/startupNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("webOfTrust")
+       fun getWebOfTrustNotification(loaders: Loaders) =
+                       TemplateNotification("wot-missing-notification", loaders.loadTemplate("/templates/notify/wotMissingNotification.html"))
+
+       @Provides
+       @Singleton
+       @Named("webOfTrustReacher")
+       fun getWebOfTrustReacher(webOfTrustConnector: WebOfTrustConnector): Runnable =
+                       Runnable { webOfTrustConnector.ping() }
+
+       @Provides
+       @Singleton
+       @Named("webOfTrustReschedule")
+       fun getWebOfTrustReschedule(@Named("notification") ticker: ScheduledExecutorService) =
+                       Consumer<Runnable> { ticker.schedule(it, 15, SECONDS) }
+
+       private inline fun <reified T> bind(): AnnotatedBindingBuilder<T> = bind(T::class.java)
+       private fun ScopedBindingBuilder.asSingleton() = `in`(Singleton::class.java)
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandler.kt
new file mode 100644 (file)
index 0000000..2217bad
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Sone - SoneLockedHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.atomic.*
+import javax.inject.*
+
+/**
+ * Handler for [SoneLockedEvent]s and [SoneUnlockedEvent]s that can schedule notifications after
+ * a certain timeout.
+ */
+class SoneLockedHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("soneLocked") private val notification: ListNotification<Sone>,
+               @Named("notification") private val executor: ScheduledExecutorService) {
+
+       private val future: AtomicReference<ScheduledFuture<*>> = AtomicReference()
+
+       @Subscribe
+       fun soneLocked(soneLockedEvent: SoneLockedEvent) {
+               synchronized(future) {
+                       notification.add(soneLockedEvent.sone)
+                       future.get()?.also(this::cancelPreviousFuture)
+                       future.set(executor.schedule(::showNotification, 5, TimeUnit.MINUTES))
+               }
+       }
+
+       @Subscribe
+       fun soneUnlocked(soneUnlockedEvent: SoneUnlockedEvent) {
+               synchronized(future) {
+                       notification.remove(soneUnlockedEvent.sone)
+                       future.get()?.also(::cancelPreviousFuture)
+               }
+       }
+
+       private fun cancelPreviousFuture(future: ScheduledFuture<*>) {
+               future.cancel(true)
+       }
+
+       private fun showNotification() {
+               notificationManager.addNotification(notification)
+       }
+
+}
index d6ec08f..c6de63f 100644 (file)
@@ -22,15 +22,13 @@ import net.pterodactylus.sone.core.event.*
 import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.notify.*
 import net.pterodactylus.util.notify.*
-import net.pterodactylus.util.template.*
+import javax.inject.*
 
 /**
  * Handler for [SoneLockedOnStartup][net.pterodactylus.sone.core.event.SoneLockedOnStartup] events
  * that adds the appropriate notification to the [NotificationManager].
  */
-class SoneLockedOnStartupHandler(private val notificationManager: NotificationManager, template: Template) {
-
-       private val notification = ListNotification<Sone>("sone-locked-on-startup", "sones", template)
+class SoneLockedOnStartupHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("soneLockedOnStartup") private val notification: ListNotification<Sone>) {
 
        @Subscribe
        @Suppress("UnstableApiUsage")
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/StartupHandler.kt
new file mode 100644 (file)
index 0000000..06acf94
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Sone - StartupHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import javax.inject.*
+
+/**
+ * Handler for the [Startup] event notification.
+ */
+class StartupHandler @Inject constructor(
+               private val notificationManager: NotificationManager,
+               @Named("startup") private val notification: TemplateNotification,
+               @Named("notification") private val ticker: ScheduledExecutorService) {
+
+       @Subscribe
+       fun startup(startup: Startup) {
+               notificationManager.addNotification(notification)
+               ticker.schedule({ notificationManager.removeNotification(notification) }, 2, MINUTES)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandler.kt
new file mode 100644 (file)
index 0000000..924d395
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Sone - WebOfTrustHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for web of trust-related notifications and the [WebOfTrustAppeared]
+ * and [WebOfTrustDisappeared] events.
+ */
+class WebOfTrustHandler @Inject constructor(private val notificationManager: NotificationManager, @Named("webOfTrust") private val notification: TemplateNotification) {
+
+       @Subscribe
+       fun webOfTrustAppeared(webOfTrustAppeared: WebOfTrustAppeared) {
+               notificationManager.removeNotification(notification)
+       }
+
+       @Subscribe
+       fun webOfTrustDisappeared(webOfTrustDisappeared: WebOfTrustDisappeared) {
+               notificationManager.addNotification(notification)
+       }
+
+}
index 6c91fcd..a448377 100644 (file)
@@ -461,4 +461,4 @@ Notification.Mention.Text=Sie wurden in diesen Nachrichten erwähnt:
 Notification.SoneIsInserting.Text=Ihre Sone sone://{0} wird jetzt hoch geladen.
 Notification.SoneIsInserted.Text=Ihre Sone sone://{0} wurde in {1,number} {1,choice,0#Sekunden|1#Sekunde|1<Sekunden} hoch geladen.
 Notification.SoneInsertAborted.Text=Ihre Sone sone://{0} konnte nicht hoch geladen werden.
-Notification.SoneLockedOnStartup.Text=Einige Sones wurden beim Starten gesperrt, weil sie keine Inhalte enthielten. Versionen vor v81 hatten einen Fehler, der Sones ohne Inhalte zur Folge hatte. Um zu verhindern, dass solchermaßen defekte Sones hoch geladen werden, wurden sie automatisch gesperrt. Bitte überprüfen Sie Ihre Sones, benutzen Sie den Sonerettungsmodus und entsperren Sie die Sones manuell, wenn Sie mit den Inhalten Ihrer Sones zufrieden sind. Die gesperrten Sones sind:
+Notification.SoneLockedOnStartup.Text=Versionen vor v81 hatten einen Fehler, der Sones ohne Inhalte zur Folge hatte. Um zu verhindern, dass solchermaßen defekte Sones hoch geladen werden, wurden sie automatisch gesperrt. Bitte überprüfen Sie Ihre Sones, benutzen Sie den Sonerettungsmodus und entsperren Sie die Sones manuell, wenn Sie mit den Inhalten Ihrer Sones zufrieden sind. Die gesperrten Sones sind:
index 1c0f2cb..4376025 100644 (file)
@@ -463,4 +463,4 @@ Notification.Mention.Text=You have been mentioned in the following posts:
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have automatically been locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
index 42a62d5..7c88daf 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Has sido mencionado en las siguientes publicaciones:
 Notification.SoneIsInserting.Text=Tu Sone sone://{0} está siendo insertado.
 Notification.SoneIsInserted.Text=Tu Sone sone://{0} ha sido insertado en {1,number} {1,choice,0#segundos|1#segundo|1<segundos}.
 Notification.SoneInsertAborted.Text=Tu Sone sone://{0} no pudo ser insertado.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 324–328, 360, 464
index 4e6c361..24858aa 100644 (file)
@@ -462,4 +462,4 @@ Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant être ins
 Notification.SoneIsInserted.Text=votre Sone sone://{0} a été inséré dans {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Votre Sone sone://{0} ne peut pas être inséré.
 Notification.SoneLockedOnStartup.Text=Les versions antérieures à v81 avaient un bug vidant les Sones. Pour éviter d'insérer des Sones vides ils ont été automatiquement vérouillés. Vérifiez vos Sones, utilisez le mode récupération si nécessaire puis dévérouillez vos Sones lorsque vous êtes satisfait du résultat. Les Sones vérouillés sont:
-#
+# 464
index e2509a7..2af11b5 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=次の投稿でメンションされています:
 Notification.SoneIsInserting.Text=あなたのSone sone://{0}は現在インサート中です。
 Notification.SoneIsInserted.Text=あなたのSone sone://{0}は{1,number}{1,choice,0#秒|1#秒|1<秒}でインサートされました。
 Notification.SoneInsertAborted.Text=あなたのSone sone://{0}のインサートに失敗しました。
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-51, 67, 103, 324–328, 360, 455, 464
index dfb49bb..4289240 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Du har blitt nevnt i følgende innlegg:
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-464
index 0ce5740..71a4a85 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Zostałeś oznaczony w następujących postach:
 Notification.SoneIsInserting.Text=Twoje Sone sone://{0} jest w tej chili wysyłane.
 Notification.SoneIsInserted.Text=Twoje sone://{0} zostało wysłane w {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Twoje Sone sone://{0} nie mogło zostać wysłane.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 324–328, 360, 455, 464
index 8604ef6..2eb7f14 100644 (file)
@@ -461,5 +461,5 @@ Notification.Mention.Text=Вас упомянули в следующих соо
 Notification.SoneIsInserting.Text=Your Sone sone://{0} is now being inserted.
 Notification.SoneIsInserted.Text=Your Sone sone://{0} has been inserted in {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Your Sone sone://{0} could not be inserted.
-Notification.SoneLockedOnStartup.Text=Some Sones were locked on startup because they don’t contain anything. Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
+Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
 # 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-464
diff --git a/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt b/src/test/kotlin/net/pterodactylus/sone/freenet/wot/WebOfTrustPingerTest.kt
new file mode 100644 (file)
index 0000000..cbd9172
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Sone - WebOfTrustPinger.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.wot
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.freenet.plugin.*
+import net.pterodactylus.sone.utils.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.atomic.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [WebOfTrustPinger].
+ */
+class WebOfTrustPingerTest {
+
+       private val eventBus = EventBus()
+       private val webOfTrustReachable = AtomicBoolean()
+       private val webOfTrustReacher = Runnable { webOfTrustReachable.get().onFalse { throw PluginException() } }
+       private val rescheduled = AtomicBoolean()
+       private val reschedule: Consumer<Runnable> = Consumer { if (it == pinger) rescheduled.set(true) }
+       private val pinger = WebOfTrustPinger(eventBus, webOfTrustReacher, reschedule)
+
+       @Test
+       fun `pinger sends wot appeared event when run first and wot is reachable`() {
+               webOfTrustReachable.set(true)
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger reschedules when wot is reachable`() {
+               webOfTrustReachable.set(true)
+               pinger()
+               assertThat(rescheduled.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event when run first and wot is not reachable`() {
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `pinger reschedules when wot is not reachable`() {
+               pinger()
+               assertThat(rescheduled.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event when run twice and wot is not reachable on second call`() {
+               val disappearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) })
+               webOfTrustReachable.set(true)
+               pinger()
+               webOfTrustReachable.set(false)
+               pinger()
+               assertThat(disappearedReceived.get(), equalTo(true))
+       }
+
+       @Test
+       fun `pinger sends wot appeared event only once`() {
+               webOfTrustReachable.set(true)
+               val appearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustAppearedCatcher { appearedReceived.set(true) })
+               pinger()
+               appearedReceived.set(false)
+               pinger()
+               assertThat(appearedReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `pinger sends wot disappeared event only once`() {
+               val disappearedReceived = AtomicBoolean()
+               eventBus.register(WebOfTrustDisappearedCatcher { disappearedReceived.set(true) })
+               pinger()
+               disappearedReceived.set(false)
+               pinger()
+               assertThat(disappearedReceived.get(), equalTo(false))
+       }
+
+}
+
+private class WebOfTrustAppearedCatcher(private val received: () -> Unit) {
+       @Subscribe
+       fun webOfTrustAppeared(webOfTrustAppeared: WebOfTrustAppeared) {
+               received()
+       }
+}
+
+private class WebOfTrustDisappearedCatcher(private val received: () -> Unit) {
+       @Subscribe
+       fun webOfTrustDisappeared(webOfTrustDisappeared: WebOfTrustDisappeared) {
+               received()
+       }
+}
index e94e34e..c6aed88 100644 (file)
@@ -34,12 +34,6 @@ class FreenetModuleTest {
        private val module = FreenetModule(pluginRespirator)
        private val injector = Guice.createInjector(module)
 
-       private inline fun <reified T : Any> verifySingletonInstance() {
-               val firstInstance = injector.getInstance<T>()
-               val secondInstance = injector.getInstance<T>()
-               assertThat(firstInstance, sameInstance(secondInstance))
-       }
-
        @Test
        fun `plugin respirator is not bound`() {
                expectedException.expect(Exception::class.java)
@@ -53,7 +47,7 @@ class FreenetModuleTest {
 
        @Test
        fun `node is returned as singleton`() {
-               verifySingletonInstance<Node>()
+               injector.verifySingletonInstance<Node>()
        }
 
        @Test
@@ -63,7 +57,7 @@ class FreenetModuleTest {
 
        @Test
        fun `high level simply client is returned as singleton`() {
-               verifySingletonInstance<HighLevelSimpleClient>()
+               injector.verifySingletonInstance<HighLevelSimpleClient>()
        }
 
        @Test
@@ -73,7 +67,7 @@ class FreenetModuleTest {
 
        @Test
        fun `session manager is returned as singleton`() {
-               verifySingletonInstance<SessionManager>()
+               injector.verifySingletonInstance<SessionManager>()
                verify(pluginRespirator).getSessionManager("Sone")
        }
 
@@ -84,7 +78,7 @@ class FreenetModuleTest {
 
        @Test
        fun `toadlet container is returned as singleten`() {
-               verifySingletonInstance<ToadletContainer>()
+               injector.verifySingletonInstance<ToadletContainer>()
        }
 
        @Test
@@ -94,7 +88,7 @@ class FreenetModuleTest {
 
        @Test
        fun `page maker is returned as singleton`() {
-               verifySingletonInstance<PageMaker>()
+               injector.verifySingletonInstance<PageMaker>()
        }
 
        @Test
@@ -106,7 +100,7 @@ class FreenetModuleTest {
 
        @Test
        fun `plugin respirator facade is returned as singleton`() {
-               verifySingletonInstance<PluginRespiratorFacade>()
+               injector.verifySingletonInstance<PluginRespiratorFacade>()
        }
 
        @Test
@@ -116,7 +110,7 @@ class FreenetModuleTest {
 
        @Test
        fun `plugin connector facade is returned as singleton`() {
-               verifySingletonInstance<PluginConnector>()
+               injector.verifySingletonInstance<PluginConnector>()
        }
 
 }
index e95955c..1a71f4b 100644 (file)
@@ -6,7 +6,6 @@ import com.google.common.eventbus.*
 import com.google.inject.Guice.*
 import com.google.inject.name.Names.*
 import freenet.l10n.*
-import freenet.pluginmanager.*
 import net.pterodactylus.sone.core.*
 import net.pterodactylus.sone.database.*
 import net.pterodactylus.sone.database.memory.*
@@ -20,6 +19,7 @@ import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.mockito.Mockito.*
 import java.io.*
+import java.util.concurrent.*
 import java.util.concurrent.atomic.*
 import kotlin.test.*
 
@@ -190,9 +190,7 @@ class SoneModuleTest {
 
        @Test
        fun `core is created as singleton`() {
-               val firstCore = injector.getInstance<Core>()
-               val secondCore = injector.getInstance<Core>()
-               assertThat(secondCore, sameInstance(firstCore))
+               injector.verifySingletonInstance<Core>()
        }
 
        @Test
@@ -209,27 +207,23 @@ class SoneModuleTest {
        }
 
        @Test
-       fun `metrics registry can be created`() {
-               assertThat(injector.getInstance<MetricRegistry>(), notNullValue())
+       fun `metrics registry is created as singleton`() {
+               injector.verifySingletonInstance<MetricRegistry>()
        }
 
        @Test
-       fun `metrics registry is created as singleton`() {
-               val firstMetricRegistry = injector.getInstance<MetricRegistry>()
-               val secondMetricRegistry = injector.getInstance<MetricRegistry>()
-               assertThat(firstMetricRegistry, sameInstance(secondMetricRegistry))
+       fun `wot connector is created as singleton`() {
+               injector.verifySingletonInstance<WebOfTrustConnector>()
        }
 
        @Test
-       fun `wot connector can be created`() {
-               assertThat(injector.getInstance<WebOfTrustConnector>(), notNullValue())
+       fun `notification ticker is created as singleton`() {
+               injector.verifySingletonInstance<ScheduledExecutorService>(named("notification"))
        }
 
        @Test
-       fun `wot connector is created as singleton`() {
-               val firstWebOfTrustConnector = injector.getInstance<WebOfTrustConnector>()
-               val secondWebOfTrustConnector = injector.getInstance<WebOfTrustConnector>()
-               assertThat(firstWebOfTrustConnector, sameInstance(secondWebOfTrustConnector))
+       fun `ticker shutdown is created as singleton`() {
+               injector.verifySingletonInstance<TickerShutdown>()
        }
 
 }
index eef312f..e658fe2 100644 (file)
@@ -1,11 +1,13 @@
 package net.pterodactylus.sone.main
 
+import com.google.common.eventbus.*
 import com.google.inject.*
 import freenet.client.async.*
 import freenet.l10n.BaseL10n.LANGUAGE.*
 import freenet.node.*
 import freenet.pluginmanager.*
 import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.core.event.*
 import net.pterodactylus.sone.fcp.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.test.*
@@ -14,6 +16,8 @@ import net.pterodactylus.sone.web.notification.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.mockito.Mockito.*
+import java.io.*
+import java.util.concurrent.atomic.*
 import kotlin.test.*
 
 /**
@@ -22,8 +26,7 @@ import kotlin.test.*
 @Dirty
 class SonePluginTest {
 
-       private var injector = mockInjector()
-       private val sonePlugin by lazy { SonePlugin { injector } }
+       private var sonePlugin = SonePlugin { injector }
        private val pluginRespirator = deepMock<PluginRespirator>()
        private val node = deepMock<Node>()
        private val clientCore = deepMock<NodeClientCore>()
@@ -71,11 +74,12 @@ class SonePluginTest {
                assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
        }
 
-       private fun runSonePluginWithRealInjector(): Injector {
+       private fun runSonePluginWithRealInjector(injectorConsumer: (Injector) -> Unit = {}): Injector {
                lateinit var injector: Injector
-               val sonePlugin = SonePlugin {
+               sonePlugin = SonePlugin {
                        Guice.createInjector(*it).also {
                                injector = it
+                               injectorConsumer(it)
                        }
                }
                sonePlugin.setLanguage(ENGLISH)
@@ -91,27 +95,153 @@ class SonePluginTest {
        }
 
        @Test
-       fun `notification handler is being started`() {
+       fun `notification handler is being requested`() {
                sonePlugin.runPlugin(pluginRespirator)
-               val notificationHandler = injector.getInstance<NotificationHandler>()
-               verify(notificationHandler).start()
+               assertThat(getInjected(NotificationHandler::class.java), notNullValue())
        }
 
-}
+       @Test
+       fun `ticker shutdown is being requested`() {
+               sonePlugin.runPlugin(pluginRespirator)
+               assertThat(getInjected(TickerShutdown::class.java), notNullValue())
+       }
+
+       private class FirstStartListener(private val firstStartReceived: AtomicBoolean) {
+               @Subscribe
+               fun firstStart(firstStart: FirstStart) {
+                       firstStartReceived.set(true)
+               }
+       }
 
-private fun mockInjector() = mock<Injector>().apply {
-       val injected = mutableMapOf<Pair<TypeLiteral<*>, Annotation?>, Any>()
-       fun mockValue(clazz: Class<*>) = false.takeIf { clazz.name == java.lang.Boolean::class.java.name } ?: mock(clazz)
-       whenever(getInstance(any<Key<*>>())).then {
-               injected.getOrPut((it.getArgument(0) as Key<*>).let { it.typeLiteral to it.annotation }) {
-                       it.getArgument<Key<*>>(0).typeLiteral.type.typeName.toClass().let(::mockValue)
+       @Test
+       fun `first-start event is sent to event bus when first start is true`() {
+               File("sone.properties").delete()
+               val firstStartReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(FirstStartListener(firstStartReceived))
                }
+               assertThat(firstStartReceived.get(), equalTo(true))
        }
-       whenever(getInstance(any<Class<*>>())).then {
-               injected.getOrPut(TypeLiteral.get(it.getArgument(0) as Class<*>) to null) {
-                       it.getArgument<Class<*>>(0).let(::mockValue)
+
+       @Test
+       fun `first-start event is not sent to event bus when first start is false`() {
+               File("sone.properties").deleteAfter {
+                       writeText("# empty")
+                       val firstStartReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(FirstStartListener(firstStartReceived))
+                       }
+                       assertThat(firstStartReceived.get(), equalTo(false))
+               }
+       }
+
+       private class ConfigNotReadListener(private val configNotReadReceiver: AtomicBoolean) {
+               @Subscribe
+               fun configNotRead(configNotRead: ConfigNotRead) {
+                       configNotReadReceiver.set(true)
                }
        }
+
+       @Test
+       fun `config-not-read event is sent to event bus when new config is true`() {
+               File("sone.properties").deleteAfter {
+                       writeText("Invalid")
+                       val configNotReadReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(ConfigNotReadListener(configNotReadReceived))
+                       }
+                       assertThat(configNotReadReceived.get(), equalTo(true))
+               }
+       }
+
+       @Test
+       fun `config-not-read event is not sent to event bus when first start is true`() {
+               File("sone.properties").delete()
+               val configNotReadReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(ConfigNotReadListener(configNotReadReceived))
+               }
+               assertThat(configNotReadReceived.get(), equalTo(false))
+       }
+
+       @Test
+       fun `config-not-read event is not sent to event bus when new config is false`() {
+               File("sone.properties").deleteAfter {
+                       writeText("# comment")
+                       val configNotReadReceived = AtomicBoolean()
+                       runSonePluginWithRealInjector {
+                               val eventBus = it.getInstance(EventBus::class.java)
+                               eventBus.register(ConfigNotReadListener(configNotReadReceived))
+                       }
+                       assertThat(configNotReadReceived.get(), equalTo(false))
+               }
+       }
+
+       private class StartupListener(private val startupReceived: () -> Unit) {
+               @Subscribe
+               fun startup(startup: Startup) {
+                       startupReceived()
+               }
+       }
+
+       @Test
+       fun `startup event is sent to event bus`() {
+               val startupReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(StartupListener { startupReceived.set(true) })
+               }
+               assertThat(startupReceived.get(), equalTo(true))
+       }
+
+       private class ShutdownListener(private val shutdownReceived: () -> Unit) {
+               @Subscribe
+               fun shutdown(shutdown: Shutdown) {
+                       shutdownReceived()
+               }
+       }
+
+       @Test
+       fun `shutdown event is sent to event bus on terminate`() {
+               val shutdownReceived = AtomicBoolean()
+               runSonePluginWithRealInjector {
+                       val eventBus = it.getInstance(EventBus::class.java)
+                       eventBus.register(ShutdownListener { shutdownReceived.set(true) })
+               }
+               sonePlugin.terminate()
+               assertThat(shutdownReceived.get(), equalTo(true))
+       }
+
+       private fun <T> getInjected(clazz: Class<T>, annotation: Annotation? = null): T? =
+                       injected[TypeLiteral.get(clazz) to annotation] as? T
+
+       private val injected =
+                       mutableMapOf<Pair<TypeLiteral<*>, Annotation?>, Any>()
+
+       private val injector = mock<Injector>().apply {
+               fun mockValue(clazz: Class<*>) = false.takeIf { clazz.name == java.lang.Boolean::class.java.name } ?: mock(clazz)
+               whenever(getInstance(any<Key<*>>())).then {
+                       injected.getOrPut((it.getArgument(0) as Key<*>).let { it.typeLiteral to it.annotation }) {
+                               it.getArgument<Key<*>>(0).typeLiteral.type.typeName.toClass().let(::mockValue)
+                       }
+               }
+               whenever(getInstance(any<Class<*>>())).then {
+                       injected.getOrPut(TypeLiteral.get(it.getArgument(0) as Class<*>) to null) {
+                               it.getArgument<Class<*>>(0).let(::mockValue)
+                       }
+               }
+       }
+
 }
 
 private fun String.toClass(): Class<*> = SonePlugin::class.java.classLoader.loadClass(this)
+
+private fun File.deleteAfter(action: File.() -> Unit) = try {
+       action(this)
+} finally {
+       this.delete()
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt b/src/test/kotlin/net/pterodactylus/sone/main/TickerShutdownTest.kt
new file mode 100644 (file)
index 0000000..c02b962
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Sone - TickerShutdownTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.main
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.test.*
+import org.mockito.Mockito.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [TickerShutdown].
+ */
+@Suppress("UnstableApiUsage")
+class TickerShutdownTest {
+
+       private val eventBus = EventBus()
+       private val notificationTicker = mock<ScheduledExecutorService>()
+
+       init {
+               eventBus.register(TickerShutdown(notificationTicker))
+       }
+
+       @Test
+       fun `ticker is shutdown on shutdown`() {
+               eventBus.post(Shutdown())
+               verify(notificationTicker).shutdown()
+       }
+
+}
index c8f2417..b0bed5c 100644 (file)
@@ -2,6 +2,8 @@ package net.pterodactylus.sone.test
 
 import com.google.inject.*
 import com.google.inject.name.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
 import org.mockito.*
 import javax.inject.Provider
 import kotlin.reflect.*
@@ -17,6 +19,13 @@ inline fun <reified T : Any> Injector.getInstance(annotation: Annotation? = null
                ?.let { getInstance(Key.get(object : TypeLiteral<T>() {}, it)) }
                ?: getInstance(Key.get(object : TypeLiteral<T>() {}))
 
+
+inline fun <reified T : Any> Injector.verifySingletonInstance(annotation: Annotation? = null) {
+       val firstInstance = getInstance<T>(annotation)
+       val secondInstance = getInstance<T>(annotation)
+       assertThat(firstInstance, sameInstance(secondInstance))
+}
+
 fun <T : Any> supply(javaClass: Class<T>): Source<T> = object : Source<T> {
        override fun fromInstance(instance: T) = Module { it.bind(javaClass).toInstance(instance) }
        override fun byInstance(instance: T) = Module { it.bind(javaClass).toProvider(Provider<T> { instance }) }
index 7fc9428..85ed9e7 100644 (file)
@@ -5,8 +5,27 @@ import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.utils.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.*
+import org.hamcrest.Matchers
 import org.hamcrest.Matchers.*
 
+/**
+ * Returns a [hamcrest matcher][Matcher] constructed from the given predicate.
+ */
+fun <T> matches(description: String? = null, predicate: (T) -> Boolean) = object : TypeSafeDiagnosingMatcher<T>() {
+
+       override fun matchesSafely(item: T, mismatchDescription: Description) =
+                       predicate(item).onFalse {
+                               mismatchDescription.appendValue(item).appendText(" did not match predicate")
+                       }
+
+       override fun describeTo(description: Description) {
+               description.appendText("matches predicate ").appendValue(predicate)
+       }
+
+}.let { matcher ->
+       description?.let { describedAs(description, matcher) } ?: matcher
+}
+
 fun hasHeader(name: String, value: String) = object : TypeSafeDiagnosingMatcher<Header>() {
        override fun matchesSafely(item: Header, mismatchDescription: Description) =
                        compare(item.name, { it.equals(name, ignoreCase = true) }) { mismatchDescription.appendText("name is ").appendValue(it) }
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestLoaders.kt
new file mode 100644 (file)
index 0000000..f71e9f5
--- /dev/null
@@ -0,0 +1,21 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.util.template.*
+import net.pterodactylus.util.web.*
+
+/**
+ * [Loaders] implementation for use in tests. Use [templates] to control what templates are
+ * returned by the [loadTemplate] method.
+ */
+class TestLoaders : Loaders {
+
+       val templates = mutableMapOf<String, Template>()
+
+       override fun loadTemplate(path: String) = templates[path] ?: Template()
+
+       override fun <REQ : Request> loadStaticPage(basePath: String, prefix: String, mimeType: String) = TestPage<REQ>()
+
+       override fun getTemplateProvider() = TemplateProvider { _, _ -> Template() }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt b/src/test/kotlin/net/pterodactylus/sone/test/TestPage.kt
new file mode 100644 (file)
index 0000000..762f789
--- /dev/null
@@ -0,0 +1,14 @@
+package net.pterodactylus.sone.test
+
+import net.pterodactylus.util.web.*
+
+/**
+ * Dummy implementation of a [Page].
+ */
+class TestPage<REQ : Request> : Page<REQ> {
+
+       override fun getPath() = ""
+       override fun isPrefixPage() = false
+       override fun handleRequest(freenetRequest: REQ, response: Response) = response
+
+}
index 739a0dd..2134661 100644 (file)
@@ -5,7 +5,6 @@ import freenet.clients.http.*
 import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.page.*
-import net.pterodactylus.util.web.*
 import org.junit.*
 import org.junit.rules.*
 import org.mockito.Mockito.*
@@ -36,7 +35,7 @@ class PageToadletRegistryTest {
                verify(pageMaker).addNavigationCategory("/Sone/index.html", "Navigation.Menu.Sone.Name", "Navigation.Menu.Sone.Tooltip", sonePlugin)
        }
 
-       private val page = TestPage()
+       private val page = TestPage<FreenetRequest>()
 
        @Test
        fun `adding a page without menuname will add it correctly`() {
@@ -147,10 +146,4 @@ class PageToadletRegistryTest {
                                whenever(this.menuName).thenReturn(menuName)
                        }
 
-       private class TestPage : Page<FreenetRequest> {
-               override fun getPath() = ""
-               override fun isPrefixPage() = false
-               override fun handleRequest(freenetRequest: FreenetRequest, response: Response) = response
-       }
-
 }
index c03d9f8..8840caf 100644 (file)
@@ -256,9 +256,7 @@ class WebInterfaceModuleTest {
 
        @Test
        fun `template context factory is created as singleton`() {
-           val factory1 = injector.getInstance<TemplateContextFactory>()
-           val factory2 = injector.getInstance<TemplateContextFactory>()
-               assertThat(factory1, sameInstance(factory2))
+               injector.verifySingletonInstance<TemplateContextFactory>()
        }
 
        @Test
@@ -280,26 +278,12 @@ class WebInterfaceModuleTest {
        @Test
        fun `page toadlet factory is created with correct prefix`() {
                val page = mock<Page<FreenetRequest>>()
-           assertThat(injector.getInstance<PageToadletFactory>().createPageToadlet(page).path(), startsWith("/Sone/"))
+               assertThat(injector.getInstance<PageToadletFactory>().createPageToadlet(page).path(), startsWith("/Sone/"))
        }
 
        @Test
        fun `notification manager is created as singleton`() {
-               val firstNotificationManager = injector.getInstance<NotificationManager>()
-               val secondNotificationManager = injector.getInstance<NotificationManager>()
-               assertThat(firstNotificationManager, sameInstance(secondNotificationManager))
-       }
-
-       @Test
-       fun `notification handler can be created`() {
-               assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
-       }
-
-       @Test
-       fun `notification handler is created as singleton`() {
-               val firstNotificationHandler = injector.getInstance<NotificationHandler>()
-               val secondNotificationHandler = injector.getInstance<NotificationHandler>()
-               assertThat(firstNotificationHandler, sameInstance(secondNotificationHandler))
+               injector.verifySingletonInstance<NotificationManager>()
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ConfigNotReadHandlerTest.kt
new file mode 100644 (file)
index 0000000..cc02606
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Sone - ConfigNotReadHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [ConfigNotReadHandler].
+ */
+@Suppress("UnstableApiUsage")
+class ConfigNotReadHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+
+       init {
+               eventBus.register(ConfigNotReadHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification to manager when config was not read`() {
+               eventBus.post(ConfigNotRead())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/FirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..2bda6d1
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Sone - FirstStartHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [FirstStartHandler].
+ */
+@Suppress("UnstableApiUsage")
+class FirstStartHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification(Template())
+
+       init {
+               eventBus.register(FirstStartHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler can be created`() {
+               FirstStartHandler(notificationManager, notification)
+       }
+
+       @Test
+       fun `handler adds notification to manager on first start event`() {
+               eventBus.post(FirstStart())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/ImageInsertHandlerTest.kt
new file mode 100644 (file)
index 0000000..3f570a9
--- /dev/null
@@ -0,0 +1,107 @@
+/**
+ * Sone - ImageInsertHandler.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import freenet.keys.FreenetURI.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [ImageInsertHandler].
+ */
+@Suppress("UnstableApiUsage")
+class ImageInsertHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val imageInsertingNotification = ListNotification<Image>("", "", Template())
+       private val imageFailedNotification = ListNotification<Image>("", "", Template())
+       private val imageInsertedNotification = ListNotification<Image>("", "", Template())
+
+       init {
+               eventBus.register(ImageInsertHandler(notificationManager, imageInsertingNotification, imageFailedNotification, imageInsertedNotification))
+       }
+
+       @Test
+       fun `handler adds notification when image insert starts`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               assertThat(notificationManager.notifications, contains<Notification>(imageInsertingNotification))
+       }
+
+       @Test
+       fun `handler adds image to notification when image insert starts`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               assertThat(imageInsertingNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert is aborted`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertAbortedEvent(image))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert fails`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds image to insert-failed notification when insert fails`() {
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(imageFailedNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler adds insert-failed notification to manager when insert fails`() {
+               eventBus.post(ImageInsertFailedEvent(image, Throwable()))
+               assertThat(notificationManager.notifications, contains<Notification>(imageFailedNotification))
+       }
+
+       @Test
+       fun `handler removes image from inserting notification when insert succeeds`() {
+               eventBus.post(ImageInsertStartedEvent(image))
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(imageInsertingNotification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds image to inserted notification when insert succeeds`() {
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(imageInsertedNotification.elements, contains(image))
+       }
+
+       @Test
+       fun `handler adds inserted notification to manager when insert succeeds`() {
+               eventBus.post(ImageInsertFinishedEvent(image, EMPTY_CHK_URI))
+               assertThat(notificationManager.notifications, contains<Notification>(imageInsertedNotification))
+       }
+
+}
+
+private val image: Image = ImageImpl()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/LocalPostHandlerTest.kt
new file mode 100644 (file)
index 0000000..d435101
--- /dev/null
@@ -0,0 +1,115 @@
+/**
+ * Sone - NewLocalPostHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.io.*
+import kotlin.test.*
+
+/**
+ * Unit test for [LocalPostHandler].
+ */
+class LocalPostHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Post>("", "", Template())
+
+       init {
+               eventBus.register(LocalPostHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds post by local sone to notification`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notification.elements, contains<Post>(localPost))
+       }
+
+       @Test
+       fun `handler does not add post by remote sone to notification`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds notification to manager`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler does not add notification during first start`() {
+               notificationManager.addNotification(object : AbstractNotification("first-start-notification") {
+                       override fun render(writer: Writer?) = Unit
+               })
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notificationManager.notifications, not(hasItem<Notification>(notification)))
+       }
+
+       @Test
+       fun `handler removes post from notification when post is removed`() {
+               notification.add(localPost)
+               notificationManager.addNotification(notification)
+               eventBus.post(PostRemovedEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not remove remote post from notification when post is removed`() {
+               notification.add(remotePost)
+               notificationManager.addNotification(notification)
+               eventBus.post(PostRemovedEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+       @Test
+       fun `handler removes post from notification when post is marked as known`() {
+               notification.add(localPost)
+               notificationManager.addNotification(notification)
+               eventBus.post(MarkPostKnownEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not remove remote post from notification when post is marked as known`() {
+               notification.add(remotePost)
+               notificationManager.addNotification(notification)
+               eventBus.post(MarkPostKnownEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+}
+
+private val localSone: Sone = object : IdOnlySone("local-sone") {
+       override fun isLocal() = true
+}
+private val localPost: Post = object : Post.EmptyPost("local-post") {
+       override fun getSone() = localSone
+}
+private val remoteSone: Sone = IdOnlySone("remote-sone")
+private val remotePost: Post = object : Post.EmptyPost("remote-post") {
+       override fun getSone() = remoteSone
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/MarkPostKnownDuringFirstStartHandlerTest.kt
new file mode 100644 (file)
index 0000000..28a1719
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Sone - MarkPostKnownDuringFirstStartHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.io.*
+import java.util.function.Consumer
+import kotlin.test.*
+
+/**
+ * Unit test for [MarkPostKnownDuringFirstStartHandler].
+ */
+@Suppress("UnstableApiUsage")
+class MarkPostKnownDuringFirstStartHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val markedPosts = mutableListOf<Post>()
+       private val handler = MarkPostKnownDuringFirstStartHandler(notificationManager, Consumer { markedPosts += it })
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `post is not marked as known if not during first start`() {
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(markedPosts, emptyIterable())
+       }
+
+       @Test
+       fun `new post is marked as known during first start`() {
+               notificationManager.addNotification(object : AbstractNotification("first-start-notification") {
+                       override fun render(writer: Writer?) = Unit
+               })
+               eventBus.post(NewPostFoundEvent(post))
+               assertThat(markedPosts, contains(post))
+       }
+
+}
+
+private val post: Post = Post.EmptyPost("post")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewRemotePostHandlerTest.kt
new file mode 100644 (file)
index 0000000..5e4e5fc
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Sone - NewRemotePostHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.io.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NewRemotePostHandler].
+ */
+@Suppress("UnstableApiUsage")
+class NewRemotePostHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Post>("", "", Template())
+       private val handler = NewRemotePostHandler(notificationManager, notification)
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `handler adds remote post to new-post notification`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notification.elements, contains(remotePost))
+       }
+
+       @Test
+       fun `handler does not add local post to new-post notification`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler adds notification for remote post to notification manager`() {
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler does not add notification for local post to notification manager`() {
+               eventBus.post(NewPostFoundEvent(localPost))
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `handler does not add notification to notification manager during first start`() {
+               notificationManager.addNotification(object : AbstractNotification("first-start-notification") {
+                       override fun render(writer: Writer?) = Unit
+               })
+               eventBus.post(NewPostFoundEvent(remotePost))
+               assertThat(notificationManager.notifications, not(hasItem(notification)))
+       }
+
+}
+
+private val remoteSone: Sone = IdOnlySone("remote-sone")
+private val remotePost: Post = object : EmptyPost("remote-post") {
+       override fun getSone() = remoteSone
+}
+
+private val localSone: Sone = object : IdOnlySone("local-sone") {
+       override fun isLocal() = true
+}
+private val localPost: Post = object : EmptyPost("local-post") {
+       override fun getSone() = localSone
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewSoneHandlerTest.kt
new file mode 100644 (file)
index 0000000..ff0947d
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * Sone - NewSoneHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.io.*
+import kotlin.test.*
+
+class NewSoneHandlerTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val handler = NewSoneHandler(notificationManager, notification)
+
+       init {
+               eventBus.register(handler)
+       }
+
+       @Test
+       fun `handler adds notification if new sone event is fired`() {
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler adds sone to notification`() {
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `handler does not add notification on new sone event if first-start notification is present`() {
+               notificationManager.addNotification(object : AbstractNotification("first-start-notification") {
+                       override fun render(writer: Writer) = Unit
+               })
+               eventBus.post(NewSoneFoundEvent(sone))
+               assertThat(notificationManager.notifications.single().id, equalTo("first-start-notification"))
+       }
+
+       @Test
+       fun `handler removes sone from notification if sone is marked as known`() {
+               notification.add(sone)
+               eventBus.post(MarkSoneKnownEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `handler removes sone from notification if sone is removed`() {
+               notification.add(sone)
+               eventBus.post(SoneRemovedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone-id")
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NewVersionHandlerTest.kt
new file mode 100644 (file)
index 0000000..d37f60b
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Sone - NewVersionHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import net.pterodactylus.util.version.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NewVersionHandler].
+ */
+@Suppress("UnstableApiUsage")
+class NewVersionHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification(Template())
+
+       init {
+               eventBus.register(NewVersionHandler(notificationManager, notification))
+               eventBus.post(UpdateFoundEvent(Version(1, 2, 3), 1000L, 2000L, true))
+       }
+
+       @Test
+       fun `new-version handler adds notification to manager on new version`() {
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler sets version in notification`() {
+               assertThat(notification.get("latestVersion"), equalTo<Any>(Version(1, 2, 3)))
+       }
+
+       @Test
+       fun `handler sets release time in notification`() {
+               assertThat(notification.get("releaseTime"), equalTo<Any>(1000L))
+       }
+
+       @Test
+       fun `handler sets edition in notification`() {
+               assertThat(notification.get("latestEdition"), equalTo<Any>(2000L))
+       }
+
+       @Test
+       fun `handler sets disruptive flag in notification`() {
+               assertThat(notification.get("disruptive"), equalTo<Any>(true))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerModuleTest.kt
new file mode 100644 (file)
index 0000000..ca52c1f
--- /dev/null
@@ -0,0 +1,460 @@
+/**
+ * Sone - NotificationHandlerModuleTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.inject.*
+import com.google.inject.Guice.*
+import com.google.inject.name.Names.*
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.Post.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.freenet.wot.*
+import net.pterodactylus.sone.main.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers
+import org.hamcrest.Matchers.*
+import org.mockito.*
+import org.mockito.Mockito.*
+import java.io.*
+import java.util.concurrent.*
+import java.util.concurrent.TimeUnit.*
+import java.util.function.*
+import kotlin.test.*
+
+/**
+ * Unit test for [NotificationHandlerModule].
+ */
+class NotificationHandlerModuleTest {
+
+       private val core = mock<Core>()
+       private val webOfTrustConnector = mock<WebOfTrustConnector>()
+       private val ticker = mock<ScheduledExecutorService>()
+       private val notificationManager = NotificationManager()
+       private val loaders = TestLoaders()
+       private val injector: Injector = createInjector(
+                       Core::class.isProvidedBy(core),
+                       NotificationManager::class.isProvidedBy(notificationManager),
+                       Loaders::class.isProvidedBy(loaders),
+                       WebOfTrustConnector::class.isProvidedBy(webOfTrustConnector),
+                       ScheduledExecutorService::class.withNameIsProvidedBy(ticker, "notification"),
+                       NotificationHandlerModule()
+       )
+
+       @Test
+       fun `notification handler is created as singleton`() {
+               injector.verifySingletonInstance<NotificationHandler>()
+       }
+
+       @Test
+       fun `mark-post-known-during-first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<MarkPostKnownDuringFirstStartHandler>()
+       }
+
+       @Test
+       fun `mark-post-known-during-first-start handler is created with correct action`() {
+               notificationManager.addNotification(object : AbstractNotification("first-start-notification") {
+                       override fun render(writer: Writer?) = Unit
+               })
+               val handler = injector.getInstance<MarkPostKnownDuringFirstStartHandler>()
+               val post = mock<Post>()
+               handler.newPostFound(NewPostFoundEvent(post))
+               verify(core).markPostKnown(post)
+       }
+
+       @Test
+       fun `sone-locked-on-startup handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedOnStartupHandler>()
+       }
+
+       @Test
+       fun `module can create sone-locked-on-startup notification with correct id`() {
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+               assertThat(notification.id, equalTo("sone-locked-on-startup"))
+       }
+
+       @Test
+       fun `sone-locked-on-startup notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+       }
+
+       @Test
+       fun `module can create sone-locked-on-startup notification with correct template and key`() {
+               loaders.templates += "/templates/notify/soneLockedOnStartupNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup"))
+               val sone1 = IdOnlySone("sone1")
+               val sone2 = IdOnlySone("sone2")
+               notification.add(sone1)
+               notification.add(sone2)
+               assertThat(notification.render(), equalTo(listOf(sone1, sone2).toString()))
+       }
+
+       @Test
+       fun `sone-locked-on-startup notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLockedOnStartup")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `new-sone handler is created as singleton`() {
+               injector.verifySingletonInstance<NewSoneHandler>()
+       }
+
+       @Test
+       fun `new-sone notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("newSone")).id, equalTo("new-sone-notification"))
+       }
+
+       @Test
+       fun `new-sone notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newSoneNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("newSone"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `new-sone notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("newSone")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post handler is created as singleton`() {
+               injector.verifySingletonInstance<NewRemotePostHandler>()
+       }
+
+       @Test
+       fun `new-remote-post notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("newRemotePost"))
+       }
+
+       @Test
+       fun `new-remote-post notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("newRemotePost")).id, equalTo("new-post-notification"))
+       }
+
+       @Test
+       fun `new-remote-post notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("newRemotePost")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `new-remote-post notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("newRemotePost"))
+               val posts = listOf(EmptyPost("post1"), EmptyPost("post2"))
+               posts.forEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `sone-locked notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Sone>>(named("soneLocked"))
+       }
+
+       @Test
+       fun `sone-locked notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `sone-locked notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Sone>>(named("soneLocked")).id, equalTo("sones-locked-notification"))
+       }
+
+       @Test
+       fun `sone-locked notification has correct key and template`() {
+               loaders.templates += "/templates/notify/lockedSonesNotification.html" to "<% sones>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Sone>>(named("soneLocked"))
+               val sones = listOf(IdOnlySone("sone1"), IdOnlySone("sone2"))
+               sones.forEach(notification::add)
+               assertThat(notification.render(), equalTo(sones.toString()))
+       }
+
+       @Test
+       fun `sone-locked handler is created as singleton`() {
+               injector.verifySingletonInstance<SoneLockedHandler>()
+       }
+
+       @Test
+       fun `local-post notification is not dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("localPost")).isDismissable, equalTo(false))
+       }
+
+       @Test
+       fun `local-post notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Post>>(named("localPost")).id, equalTo("local-post-notification"))
+       }
+
+       @Test
+       fun `local-post notification has correct key and template`() {
+               loaders.templates += "/templates/notify/newPostNotification.html" to "<% posts>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Post>>(named("localPost"))
+               val posts = listOf(EmptyPost("post1"), EmptyPost("post2"))
+               posts.forEach(notification::add)
+               assertThat(notification.render(), equalTo(posts.toString()))
+       }
+
+       @Test
+       fun `local-post notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Post>>(named("localPost"))
+       }
+
+       @Test
+       fun `local-post handler is created as singleton`() {
+               injector.verifySingletonInstance<LocalPostHandler>()
+       }
+
+       @Test
+       fun `new-version notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("newVersion"))
+       }
+
+       @Test
+       fun `new-version notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("newVersion")).id, equalTo("new-version-notification"))
+       }
+
+       @Test
+       fun `new-version notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("newVersion")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `new-version notification loads correct template`() {
+               loaders.templates += "/templates/notify/newVersionNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("newVersion"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `new-version handler is created as singleton`() {
+               injector.verifySingletonInstance<NewVersionHandler>()
+       }
+
+       @Test
+       fun `inserting-image notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageInserting"))
+       }
+
+       @Test
+       fun `inserting-image notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserting")).id, equalTo("inserting-images-notification"))
+       }
+
+       @Test
+       fun `inserting-image notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserting")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserting-image notification loads correct template`() {
+               loaders.templates += "/templates/notify/inserting-images-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageInserting"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `inserting-image-failed notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageFailed"))
+       }
+
+       @Test
+       fun `inserting-image-failed notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageFailed")).id, equalTo("image-insert-failed-notification"))
+       }
+
+       @Test
+       fun `inserting-image-failed notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageFailed")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserting-image-failed notification loads correct template`() {
+               loaders.templates += "/templates/notify/image-insert-failed-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageFailed"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `inserted-image notification is created as singleton`() {
+               injector.verifySingletonInstance<ListNotification<Image>>(named("imageInserted"))
+       }
+
+       @Test
+       fun `inserted-image notification has correct ID`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserted")).id, equalTo("inserted-images-notification"))
+       }
+
+       @Test
+       fun `inserted-image notification is dismissable`() {
+               assertThat(injector.getInstance<ListNotification<Image>>(named("imageInserted")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `inserted-image notification loads correct template`() {
+               loaders.templates += "/templates/notify/inserted-images-notification.html" to "<% images>".asTemplate()
+               val notification = injector.getInstance<ListNotification<Image>>(named("imageInserted"))
+               val images = listOf(ImageImpl(), ImageImpl()).onEach(notification::add)
+               assertThat(notification.render(), equalTo(images.toString()))
+       }
+
+       @Test
+       fun `image insert handler is created as singleton`() {
+               injector.verifySingletonInstance<ImageInsertHandler>()
+       }
+
+       @Test
+       fun `first-start notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("firstStart"))
+       }
+
+       @Test
+       fun `first-start notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("firstStart")).id, equalTo("first-start-notification"))
+       }
+
+       @Test
+       fun `first-start notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("firstStart")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `first-start notification loads correct template`() {
+               loaders.templates += "/templates/notify/firstStartNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("firstStart"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `first-start handler is created as singleton`() {
+               injector.verifySingletonInstance<FirstStartHandler>()
+       }
+
+       @Test
+       fun `config-not-read notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("configNotRead"))
+       }
+
+       @Test
+       fun `config-not-read notification has correct ID `() {
+               assertThat(injector.getInstance<TemplateNotification>(named("configNotRead")).id, equalTo("config-not-read-notification"))
+       }
+
+       @Test
+       fun `config-not-read notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("configNotRead")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `config-not-read notification loads correct template`() {
+               loaders.templates += "/templates/notify/configNotReadNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("configNotRead"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `config-not-read handler is created as singleton`() {
+               injector.verifySingletonInstance<ConfigNotReadHandler>()
+       }
+
+       @Test
+       fun `startup notification can be created`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("startup"))
+       }
+
+       @Test
+       fun `startup notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("startup")).id, equalTo("startup-notification"))
+       }
+
+       @Test
+       fun `startup notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("startup")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `startup notification loads correct template`() {
+               loaders.templates += "/templates/notify/startupNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("startup"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `startup handler is created as singleton`() {
+               injector.verifySingletonInstance<StartupHandler>()
+       }
+
+       @Test
+       fun `web-of-trust notification is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("webOfTrust"))
+       }
+
+       @Test
+       fun `web-of-trust notification has correct ID`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("webOfTrust")).id, equalTo("wot-missing-notification"))
+       }
+
+       @Test
+       fun `web-of-trust notification is dismissable`() {
+               assertThat(injector.getInstance<TemplateNotification>(named("webOfTrust")).isDismissable, equalTo(true))
+       }
+
+       @Test
+       fun `web-of-trust notification loads correct template`() {
+               loaders.templates += "/templates/notify/wotMissingNotification.html" to "1".asTemplate()
+               val notification = injector.getInstance<TemplateNotification>(named("webOfTrust"))
+               assertThat(notification.render(), equalTo("1"))
+       }
+
+       @Test
+       fun `web-of-trust handler is created as singleton`() {
+               injector.verifySingletonInstance<TemplateNotification>(named("webOfTrust"))
+       }
+
+       @Test
+       fun `web-of-trust reacher is created as singleton`() {
+               injector.verifySingletonInstance<Runnable>(named("webOfTrustReacher"))
+       }
+
+       @Test
+       fun `web-of-trust reacher access the wot connector`() {
+               injector.getInstance<Runnable>(named("webOfTrustReacher")).run()
+               verify(webOfTrustConnector).ping()
+       }
+
+       @Test
+       fun `web-of-trust reschedule is created as singleton`() {
+               injector.verifySingletonInstance<Consumer<Runnable>>(named("webOfTrustReschedule"))
+       }
+
+       @Test
+       fun `web-of-trust reschedule schedules at the correct delay`() {
+               val webOfTrustPinger = injector.getInstance<WebOfTrustPinger>()
+               injector.getInstance<Consumer<Runnable>>(named("webOfTrustReschedule"))(webOfTrustPinger)
+               verify(ticker).schedule(ArgumentMatchers.eq(webOfTrustPinger), ArgumentMatchers.eq(15L), ArgumentMatchers.eq(SECONDS))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt
deleted file mode 100644 (file)
index d145038..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Sone - NotificationHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.notification
-
-import com.google.common.eventbus.*
-import com.google.inject.*
-import com.google.inject.Guice.*
-import net.pterodactylus.sone.main.*
-import net.pterodactylus.sone.test.*
-import net.pterodactylus.util.notify.*
-import net.pterodactylus.util.template.*
-import net.pterodactylus.util.web.*
-import org.hamcrest.MatcherAssert.*
-import org.hamcrest.Matchers.*
-import kotlin.test.*
-
-/**
- * Unit test for [NotificationHandler].
- */
-class NotificationHandlerTest {
-
-       private val eventBus = TestEventBus()
-       private val loaders = TestLoaders()
-       private val notificationManager = NotificationManager()
-       private val handler = NotificationHandler(eventBus, loaders, notificationManager)
-
-       @Test
-       fun `notification handler can be created by guice`() {
-               val injector = createInjector(
-                               EventBus::class.isProvidedBy(eventBus),
-                               NotificationManager::class.isProvidedBy(notificationManager),
-                               Loaders::class.isProvidedBy(loaders)
-               )
-               assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
-       }
-
-       @Test
-       fun `notification handler registers handler for sone-locked event`() {
-               handler.start()
-               assertThat(eventBus.registeredObjects.any { it.javaClass == SoneLockedOnStartupHandler::class.java }, equalTo(true))
-       }
-
-       @Test
-       fun `notification handler loads sone-locked notification template`() {
-               handler.start()
-               assertThat(loaders.requestedTemplatePaths.any { it == "/templates/notify/soneLockedOnStartupNotification.html" }, equalTo(true))
-       }
-
-}
-
-@Suppress("UnstableApiUsage")
-private class TestEventBus : EventBus() {
-       private val _registeredObjects = mutableListOf<Any>()
-       val registeredObjects: List<Any>
-               get() = _registeredObjects
-
-       override fun register(`object`: Any) {
-               super.register(`object`)
-               _registeredObjects += `object`
-       }
-
-}
-
-private class TestLoaders : Loaders {
-       val requestedTemplatePaths = mutableListOf<String>()
-
-       override fun loadTemplate(path: String) =
-                       Template().also { requestedTemplatePaths += path }
-
-       override fun <REQ : Request> loadStaticPage(basePath: String, prefix: String, mimeType: String) = object : Page<REQ> {
-
-               override fun getPath() = ""
-               override fun isPrefixPage() = false
-               override fun handleRequest(request: REQ, response: Response) = response
-
-       }
-
-       override fun getTemplateProvider() = TemplateProvider { _, _ -> Template() }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedHandlerTest.kt
new file mode 100644 (file)
index 0000000..f7c185a
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Sone - SoneLockedHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.data.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneLockedHandler].
+ */
+@Suppress("UnstableApiUsage")
+class SoneLockedHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = ListNotification<Sone>("", "", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               SoneLockedHandler(notificationManager, notification, executor).also(eventBus::register)
+       }
+
+       @AfterTest
+       fun shutdownExecutor() = executor.shutdown()
+
+       @Test
+       fun `notification is not added before the command is run`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+       @Test
+       fun `sone is added to notification immediately`() {
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(notification.elements, contains(sone))
+       }
+
+       @Test
+       fun `notification is added to notification manager from command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduleds.single().command()
+               assertThat(notificationManager.notifications, contains<Any>(notification))
+       }
+
+       @Test
+       fun `command is registered with a delay of five minutes`() {
+               eventBus.post(SoneLockedEvent(sone))
+               with(executor.scheduleds.single()) {
+                       assertThat(timeUnit.toNanos(delay), equalTo(TimeUnit.MINUTES.toNanos(5)))
+               }
+       }
+
+       @Test
+       fun `unlocking sone after locking will cancel the future`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(executor.scheduleds.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `unlocking sone after locking will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `unlocking sone after showing the notification will remove the sone from the notification`() {
+               eventBus.post(SoneLockedEvent(sone))
+               executor.scheduleds.single().command()
+               eventBus.post(SoneUnlockedEvent(sone))
+               assertThat(notification.elements, emptyIterable())
+       }
+
+       @Test
+       fun `locking two sones will cancel the first command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduleds.first().future.isCancelled, equalTo(true))
+       }
+
+       @Test
+       fun `locking two sones will schedule a second command`() {
+               eventBus.post(SoneLockedEvent(sone))
+               eventBus.post(SoneLockedEvent(sone))
+               assertThat(executor.scheduleds[1], notNullValue())
+       }
+
+}
+
+private val sone: Sone = IdOnlySone("sone")
index 0b9d1e1..36a8836 100644 (file)
@@ -19,10 +19,11 @@ package net.pterodactylus.sone.web.notification
 
 import com.google.common.eventbus.*
 import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.data.*
 import net.pterodactylus.sone.data.impl.*
 import net.pterodactylus.sone.notify.*
-import net.pterodactylus.sone.utils.*
 import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import kotlin.test.*
@@ -35,29 +36,24 @@ class SoneLockedOnStartupHandlerTest {
        @Suppress("UnstableApiUsage")
        private val eventBus = EventBus()
        private val manager = NotificationManager()
-       private val notification by lazy { manager.notifications.single() as ListNotification<*> }
+       private val notification = ListNotification<Sone>("", "", Template())
 
        init {
-               SoneLockedOnStartupHandler(manager, template).also(eventBus::register)
-               eventBus.post(SoneLockedOnStartup(sone))
-       }
-
-       @Test
-       fun `notification has correct id`() {
-               assertThat(notification.id, equalTo("sone-locked-on-startup"))
+               SoneLockedOnStartupHandler(manager, notification).also(eventBus::register)
        }
 
        @Test
        fun `handler adds sone to notification when event is posted`() {
+               eventBus.post(SoneLockedOnStartup(sone))
                assertThat(notification.elements, contains<Any>(sone))
        }
 
        @Test
-       fun `handler creates notification with correct key`() {
-               assertThat(notification.render(), equalTo(listOf(sone).toString()))
+       fun `handler adds notification to manager`() {
+               eventBus.post(SoneLockedOnStartup(sone))
+               assertThat(manager.notifications, contains<Notification>(notification))
        }
 
 }
 
 private val sone = IdOnlySone("sone-id")
-private val template = "<% sones>".asTemplate()
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/StartupHandlerTest.kt
new file mode 100644 (file)
index 0000000..bc2c8e6
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Sone - StartupHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import java.util.concurrent.TimeUnit.*
+import kotlin.test.*
+
+/**
+ * Unit test for [StartupHandler].
+ */
+class StartupHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+       private val executor = TestScheduledThreadPoolExecutor()
+
+       init {
+               eventBus.register(StartupHandler(notificationManager, notification, executor))
+       }
+
+       @AfterTest
+       fun shutdownExecutor() = executor.shutdown()
+
+       @Test
+       fun `handler adds notification to manager on startup`() {
+               eventBus.post(Startup())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler registers command on with 2-minute delay`() {
+               eventBus.post(Startup())
+               assertThat(with(executor.scheduleds.single()) { timeUnit.toNanos(delay) }, equalTo(MINUTES.toNanos(2)))
+       }
+
+       @Test
+       fun `registered command removes notification from manager`() {
+               eventBus.post(Startup())
+               executor.scheduleds.single().command()
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/Testing.kt
new file mode 100644 (file)
index 0000000..12088d0
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * Sone - Testing.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import java.util.concurrent.*
+
+/** Information about a scheduled runnable. */
+data class Scheduled(val command: Runnable, val delay: Long, val timeUnit: TimeUnit, val future: ScheduledFuture<*>)
+
+/**
+ * [ScheduledThreadPoolExecutor] extension that stores parameters and return
+ * values for the [ScheduledThreadPoolExecutor.schedule] method.
+ */
+class TestScheduledThreadPoolExecutor : ScheduledThreadPoolExecutor(1) {
+
+       val scheduleds = mutableListOf<Scheduled>()
+
+       override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> =
+                       super.schedule(command, delay, unit)
+                                       .also { scheduleds += Scheduled(command, delay, unit, it) }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/WebOfTrustHandlerTest.kt
new file mode 100644 (file)
index 0000000..ffd2536
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * Sone - WebOfTrustHandlerTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.notification
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.core.event.*
+import net.pterodactylus.util.notify.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [WebOfTrustHandler].
+ */
+class WebOfTrustHandlerTest {
+
+       private val eventBus = EventBus()
+       private val notificationManager = NotificationManager()
+       private val notification = TemplateNotification("", Template())
+
+       init {
+               eventBus.register(WebOfTrustHandler(notificationManager, notification))
+       }
+
+       @Test
+       fun `handler adds notification if wot goes down`() {
+               eventBus.post(WebOfTrustDisappeared())
+               assertThat(notificationManager.notifications, contains<Notification>(notification))
+       }
+
+       @Test
+       fun `handler removes notification if wot appears`() {
+               notificationManager.addNotification(notification)
+               eventBus.post(WebOfTrustAppeared())
+               assertThat(notificationManager.notifications, emptyIterable())
+       }
+
+}