🔀 Merge 'feature/sone-locked-notification' into 'next'
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Fri, 29 Nov 2019 17:28:46 +0000 (18:28 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Fri, 29 Nov 2019 17:28:46 +0000 (18:28 +0100)
33 files changed:
build.gradle
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java [deleted file]
src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/main/DebugLoaders.java
src/main/java/net/pterodactylus/sone/main/DefaultLoaders.java
src/main/java/net/pterodactylus/sone/main/Loaders.java
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/SoneLockedOnStartup.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/data/Albums.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt
src/main/kotlin/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.kt
src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.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/main/resources/templates/notify/soneLockedOnStartupNotification.html [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java [deleted file]
src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/main/SonePluginTest.kt
src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt
src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt [new file with mode: 0644]

index 47113fc..78c3110 100644 (file)
@@ -39,9 +39,9 @@ dependencies {
     compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8'
     compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.0-RC'
 
-    compile group: 'net.pterodactylus', name: 'utils', version: '0.12.4'
+    compile group: 'net.pterodactylus', name: 'utils', version: '0.13.1'
     compile group: 'com.google.inject', name: 'guice', version: '4.2.2'
-    compile group: 'com.google.guava', name: 'guava', version: '27.0.1-android'
+    compile group: 'com.google.guava', name: 'guava', version: '27.0.1-jre'
     compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.1'
     compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.9.1'
     compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
index 5c1e537..0228951 100644 (file)
@@ -24,6 +24,7 @@ import static com.google.common.primitives.Longs.tryParse;
 import static java.lang.String.format;
 import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
+import static net.pterodactylus.sone.data.AlbumsKt.getAllImages;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -634,9 +635,10 @@ public class Core extends AbstractService implements SoneProvider, PostProvider,
                loadSone(sone);
                database.storeSone(sone);
                sone.setStatus(SoneStatus.idle);
-               if (sone.getPosts().isEmpty() && sone.getReplies().isEmpty()) {
+               if (sone.getPosts().isEmpty() && sone.getReplies().isEmpty() && getAllImages(sone.getRootAlbum()).isEmpty()) {
                        // dirty hack
                        lockSone(sone);
+                       eventBus.post(new SoneLockedOnStartup(sone));
                }
                soneInserter.start();
                return sone;
diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.java
deleted file mode 100644 (file)
index 5fa33e4..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
-import net.pterodactylus.util.config.Configuration;
-import net.pterodactylus.util.config.ConfigurationException;
-
-/**
- * Loads preferences stored in a {@link Configuration} into a {@link
- * Preferences} object.
- */
-public class PreferencesLoader {
-
-       private final Preferences preferences;
-
-       public PreferencesLoader(Preferences preferences) {
-               this.preferences = preferences;
-       }
-
-       public void loadFrom(Configuration configuration) {
-               loadInsertionDelay(configuration);
-               loadPostsPerPage(configuration);
-               loadImagesPerPage(configuration);
-               loadCharactersPerPost(configuration);
-               loadPostCutOffLength(configuration);
-               loadRequireFullAccess(configuration);
-               loadFcpInterfaceActive(configuration);
-               loadFcpFullAccessRequired(configuration);
-       }
-
-       private void loadInsertionDelay(Configuration configuration) {
-               preferences.setNewInsertionDelay(configuration.getIntValue(
-                               "Option/InsertionDelay").getValue(null));
-       }
-
-       private void loadPostsPerPage(Configuration configuration) {
-               preferences.setNewPostsPerPage(
-                               configuration.getIntValue("Option/PostsPerPage")
-                                               .getValue(null));
-       }
-
-       private void loadImagesPerPage(Configuration configuration) {
-               preferences.setNewImagesPerPage(
-                               configuration.getIntValue("Option/ImagesPerPage")
-                                               .getValue(null));
-       }
-
-       private void loadCharactersPerPost(Configuration configuration) {
-               preferences.setNewCharactersPerPost(
-                               configuration.getIntValue("Option/CharactersPerPost")
-                                               .getValue(null));
-       }
-
-       private void loadPostCutOffLength(Configuration configuration) {
-               try {
-                       preferences.setNewPostCutOffLength(
-                                       configuration.getIntValue("Option/PostCutOffLength")
-                                                       .getValue(null));
-               } catch (IllegalArgumentException iae1) {
-                       /* previous versions allowed -1, ignore and use default. */
-               }
-       }
-
-       private void loadRequireFullAccess(Configuration configuration) {
-               preferences.setNewRequireFullAccess(
-                               configuration.getBooleanValue("Option/RequireFullAccess")
-                                               .getValue(null));
-       }
-
-       private void loadFcpInterfaceActive(Configuration configuration) {
-               preferences.setNewFcpInterfaceActive(configuration.getBooleanValue(
-                               "Option/ActivateFcpInterface").getValue(null));
-       }
-
-       private void loadFcpFullAccessRequired(Configuration configuration) {
-               Integer fullAccessRequiredInteger = configuration
-                               .getIntValue("Option/FcpFullAccessRequired").getValue(null);
-               preferences.setNewFcpFullAccessRequired(
-                               (fullAccessRequiredInteger == null) ? null :
-                                               FullAccessRequired.values()[fullAccessRequiredInteger]);
-       }
-
-}
diff --git a/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt b/src/main/java/net/pterodactylus/sone/core/PreferencesLoader.kt
new file mode 100644 (file)
index 0000000..32c35cb
--- /dev/null
@@ -0,0 +1,58 @@
+package net.pterodactylus.sone.core
+
+import net.pterodactylus.sone.fcp.FcpInterface.*
+import net.pterodactylus.util.config.*
+
+/**
+ * Loads preferences stored in a [Configuration] into a [Preferences] object.
+ */
+class PreferencesLoader(private val preferences: Preferences) {
+
+       fun loadFrom(configuration: Configuration) {
+               loadInsertionDelay(configuration)
+               loadPostsPerPage(configuration)
+               loadImagesPerPage(configuration)
+               loadCharactersPerPost(configuration)
+               loadPostCutOffLength(configuration)
+               loadRequireFullAccess(configuration)
+               loadFcpInterfaceActive(configuration)
+               loadFcpFullAccessRequired(configuration)
+       }
+
+       private fun loadInsertionDelay(configuration: Configuration) {
+               preferences.newInsertionDelay = configuration.getIntValue("Option/InsertionDelay").getValue(null)
+       }
+
+       private fun loadPostsPerPage(configuration: Configuration) {
+               preferences.newPostsPerPage = configuration.getIntValue("Option/PostsPerPage").getValue(null)
+       }
+
+       private fun loadImagesPerPage(configuration: Configuration) {
+               preferences.newImagesPerPage = configuration.getIntValue("Option/ImagesPerPage").getValue(null)
+       }
+
+       private fun loadCharactersPerPost(configuration: Configuration) {
+               preferences.newCharactersPerPost = configuration.getIntValue("Option/CharactersPerPost").getValue(null)
+       }
+
+       private fun loadPostCutOffLength(configuration: Configuration) {
+               try {
+                       preferences.newPostCutOffLength = configuration.getIntValue("Option/PostCutOffLength").getValue(null)
+               } catch (iae1: IllegalArgumentException) { /* previous versions allowed -1, ignore and use default. */
+               }
+       }
+
+       private fun loadRequireFullAccess(configuration: Configuration) {
+               preferences.newRequireFullAccess = configuration.getBooleanValue("Option/RequireFullAccess").getValue(null)
+       }
+
+       private fun loadFcpInterfaceActive(configuration: Configuration) {
+               preferences.newFcpInterfaceActive = configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null)
+       }
+
+       private fun loadFcpFullAccessRequired(configuration: Configuration) {
+               val fullAccessRequiredInteger = configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null)
+               preferences.newFcpFullAccessRequired = fullAccessRequiredInteger?.let { FullAccessRequired.values()[it] }
+       }
+
+}
index c42b056..2e18347 100644 (file)
@@ -1,6 +1,7 @@
 package net.pterodactylus.sone.main;
 
 import java.io.File;
+import javax.annotation.Nonnull;
 
 import net.pterodactylus.sone.template.FilesystemTemplate;
 import net.pterodactylus.sone.web.pages.ReloadingPage;
@@ -21,16 +22,19 @@ public class DebugLoaders implements Loaders {
                this.filesystemPath = filesystemPath;
        }
 
+       @Nonnull
        @Override
-       public Template loadTemplate(String path) {
+       public Template loadTemplate(@Nonnull String path) {
                return new FilesystemTemplate(new File(filesystemPath, path).getAbsolutePath());
        }
 
+       @Nonnull
        @Override
-       public <REQ extends Request> Page<REQ> loadStaticPage(String basePath, String prefix, String mimeType) {
+       public <REQ extends Request> Page<REQ> loadStaticPage(@Nonnull String basePath, @Nonnull String prefix, @Nonnull String mimeType) {
                return new ReloadingPage<>(basePath, new File(filesystemPath, prefix).getAbsolutePath(), mimeType);
        }
 
+       @Nonnull
        @Override
        public TemplateProvider getTemplateProvider() {
                return new FilesystemTemplateProvider(new File(filesystemPath, "/templates/").getAbsolutePath());
index 72d8d19..8e02573 100644 (file)
@@ -6,6 +6,7 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.io.UnsupportedEncodingException;
+import javax.annotation.Nonnull;
 
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.io.Closer;
@@ -21,8 +22,9 @@ import net.pterodactylus.util.web.StaticPage;
  */
 public class DefaultLoaders implements Loaders {
 
+       @Nonnull
        @Override
-       public Template loadTemplate(String path) {
+       public Template loadTemplate(@Nonnull String path) {
                InputStream templateInputStream = null;
                Reader reader = null;
                try {
@@ -37,12 +39,14 @@ public class DefaultLoaders implements Loaders {
                }
        }
 
+       @Nonnull
        @Override
-       public <REQ extends Request> Page<REQ> loadStaticPage(String pathPrefix, String basePath, String mimeType) {
+       public <REQ extends Request> Page<REQ> loadStaticPage(@Nonnull String pathPrefix, @Nonnull String basePath, @Nonnull String mimeType) {
                return new StaticPage<REQ>(pathPrefix, basePath, mimeType) {
                };
        }
 
+       @Nonnull
        @Override
        public TemplateProvider getTemplateProvider() {
                return new ClassPathTemplateProvider(WebInterface.class, "/templates/");
index 8ee5132..b07118b 100644 (file)
@@ -1,5 +1,7 @@
 package net.pterodactylus.sone.main;
 
+import javax.annotation.Nonnull;
+
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateProvider;
 import net.pterodactylus.util.web.Page;
@@ -13,8 +15,8 @@ import com.google.inject.ImplementedBy;
 @ImplementedBy(DefaultLoaders.class)
 public interface Loaders {
 
-       Template loadTemplate(String path);
-       <REQ extends Request> Page<REQ> loadStaticPage(String basePath, String prefix, String mimeType);
-       TemplateProvider getTemplateProvider();
+       @Nonnull Template loadTemplate(@Nonnull String path);
+       @Nonnull <REQ extends Request> Page<REQ> loadStaticPage(@Nonnull String basePath, @Nonnull String prefix, @Nonnull String mimeType);
+       @Nonnull TemplateProvider getTemplateProvider();
 
 }
index 7723db1..a3cb385 100644 (file)
@@ -26,6 +26,7 @@ import net.pterodactylus.sone.core.*;
 import net.pterodactylus.sone.fcp.*;
 import net.pterodactylus.sone.freenet.wot.*;
 import net.pterodactylus.sone.web.*;
+import net.pterodactylus.sone.web.notification.NotificationHandler;
 
 import freenet.l10n.BaseL10n.*;
 import freenet.l10n.*;
@@ -196,12 +197,16 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                /* create the web interface. */
                webInterface = injector.getInstance(WebInterface.class);
+               NotificationHandler notificationHandler = injector.getInstance(NotificationHandler.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();
        }
 
        @VisibleForTesting
index e31d9a8..bcc7a11 100644 (file)
@@ -124,7 +124,7 @@ public class WebInterface implements SessionProvider {
        private final Loaders loaders;
 
        /** The notification manager. */
-       private final NotificationManager notificationManager = new NotificationManager();
+       private final NotificationManager notificationManager;
 
        /** The Sone plugin. */
        private final SonePlugin sonePlugin;
@@ -207,7 +207,8 @@ public class WebInterface implements SessionProvider {
                        ParserFilter parserFilter, ShortenFilter shortenFilter,
                        RenderFilter renderFilter,
                        LinkedElementRenderFilter linkedElementRenderFilter,
-                       PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter) {
+                       PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
+                       NotificationManager notificationManager) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
                this.listNotificationFilter = listNotificationFilter;
@@ -223,6 +224,7 @@ public class WebInterface implements SessionProvider {
                this.metricRegistry = metricRegistry;
                this.l10nFilter = l10nFilter;
                this.translation = translation;
+               this.notificationManager = notificationManager;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
                soneTextParser = new SoneTextParser(getCore(), getCore());
 
diff --git a/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt b/src/main/kotlin/net/pterodactylus/sone/core/event/SoneLockedOnStartup.kt
new file mode 100644 (file)
index 0000000..21c5738
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Sone - SoneLockedOnStartup.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
+
+import net.pterodactylus.sone.data.*
+
+/**
+ * Signals that a Sone was locked on startup because it’s empty.
+ */
+class SoneLockedOnStartup(sone: Sone) : SoneEvent(sone)
diff --git a/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt b/src/main/kotlin/net/pterodactylus/sone/data/Albums.kt
new file mode 100644 (file)
index 0000000..f1c7d3b
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Sone - Albums.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.data
+
+/** Returns all images contained in this album and all its albums. */
+val Album.allImages: Collection<Image>
+       get() =
+               images + albums.flatMap { it.allImages }
diff --git a/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt b/src/main/kotlin/net/pterodactylus/sone/utils/Renderables.kt
new file mode 100644 (file)
index 0000000..d641bfc
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Sone - Renderables.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.utils
+
+import net.pterodactylus.util.io.*
+import java.io.*
+
+/**
+ * Renders the [Renderable] into a [String].
+ */
+fun Renderable.render() =
+               StringWriter().use { it.also(::render) }.toString()
index c996e30..63f3fc6 100644 (file)
@@ -1,5 +1,6 @@
 package net.pterodactylus.sone.web
 
+import com.google.common.eventbus.*
 import com.google.inject.*
 import freenet.support.api.*
 import net.pterodactylus.sone.core.*
@@ -10,6 +11,8 @@ 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.*
 import javax.inject.Singleton
@@ -126,4 +129,14 @@ class WebInterfaceModule : AbstractModule() {
        @Named("toadletPathPrefix")
        fun getPathPrefix(): String = "/Sone/"
 
+       @Provides
+       @Singleton
+       fun getNotificationManager() =
+                       NotificationManager()
+
+       @Provides
+       @Singleton
+       fun getNotificationHandler(eventBus: EventBus, loaders: Loaders, notificationManager: NotificationManager) =
+                       NotificationHandler(eventBus, loaders, notificationManager)
+
 }
index db6e5c5..66c8ed2 100644 (file)
@@ -3,8 +3,7 @@ package net.pterodactylus.sone.web.ajax
 import net.pterodactylus.sone.data.Sone
 import net.pterodactylus.sone.data.SoneOptions
 import net.pterodactylus.sone.main.SonePlugin
-import net.pterodactylus.sone.utils.jsonArray
-import net.pterodactylus.sone.utils.jsonObject
+import net.pterodactylus.sone.utils.*
 import net.pterodactylus.sone.web.WebInterface
 import net.pterodactylus.sone.web.page.*
 import net.pterodactylus.util.notify.Notification
@@ -74,5 +73,3 @@ private val SoneOptions?.asJsonObject
                                "ShowNotification/NewReplies" to options.isShowNewReplyNotifications
                )
        } ?: jsonObject {}
-
-private fun Notification.render() = StringWriter().use { it.also { render(it) } }.toString()
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/NotificationHandler.kt
new file mode 100644 (file)
index 0000000..7f81f9a
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Sone - NotificationHandler.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.main.*
+import net.pterodactylus.util.notify.*
+import javax.inject.*
+
+/**
+ * Handler for notifications that can create notifications and register them with an 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)
+       }
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt b/src/main/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandler.kt
new file mode 100644 (file)
index 0000000..d6ec08f
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Sone - SoneLockedOnStartupNotification.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 net.pterodactylus.util.template.*
+
+/**
+ * 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)
+
+       @Subscribe
+       @Suppress("UnstableApiUsage")
+       fun soneLockedOnStartup(soneLockedOnStartup: SoneLockedOnStartup) {
+               notification.add(soneLockedOnStartup.sone)
+               notificationManager.addNotification(notification)
+       }
+
+}
index a2ff8b1..6c91fcd 100644 (file)
@@ -461,3 +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:
index c599f84..1c0f2cb 100644 (file)
@@ -463,3 +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:
index 3a67a59..42a62d5 100644 (file)
@@ -461,4 +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.
-# 55-61, 324–328, 360
+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:
+# 55-61, 324–328, 360, 464
index a0bc86a..fc3717c 100644 (file)
@@ -461,4 +461,5 @@ Notification.Mention.Text=Vous avez été mentionné dans les messages suivants:
 Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant être inséré.
 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é.
-# 55-61, 324–328, 360
+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:
+# 55-61, 324–328, 360, 464
index f9b0483..e2509a7 100644 (file)
@@ -461,4 +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}のインサートに失敗しました。
-# 55-51, 67, 103, 324–328, 360, 455
+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:
+# 55-51, 67, 103, 324–328, 360, 455, 464
index 02bed70..dfb49bb 100644 (file)
@@ -461,4 +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.
-# 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-463
+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:
+# 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-464
index d1b9f5f..0ce5740 100644 (file)
@@ -461,4 +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.
-# 55-61, 324–328, 360, 455
+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:
+# 55-61, 324–328, 360, 455, 464
index e409f02..8604ef6 100644 (file)
@@ -461,4 +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.
-# 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-463
+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:
+# 55-61, 67, 103, 123-124, 305-307, 309-311, 324–328, 360, 455, 461-464
diff --git a/src/main/resources/templates/notify/soneLockedOnStartupNotification.html b/src/main/resources/templates/notify/soneLockedOnStartupNotification.html
new file mode 100644 (file)
index 0000000..8cd4e56
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.SoneLockedOnStartup.Text|l10n|html>
+       <%foreach sones sone>
+               <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
diff --git a/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java b/src/test/java/net/pterodactylus/sone/core/PreferencesLoaderTest.java
deleted file mode 100644 (file)
index 72831fc..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-package net.pterodactylus.sone.core;
-
-import static net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import net.pterodactylus.sone.test.TestValue;
-import net.pterodactylus.util.config.Configuration;
-
-import com.google.common.eventbus.EventBus;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link PreferencesLoader}.
- */
-public class PreferencesLoaderTest {
-
-       private final EventBus eventBus = mock(EventBus.class);
-       private final Preferences preferences = new Preferences(eventBus);
-       private final Configuration configuration = mock(Configuration.class);
-       private final PreferencesLoader preferencesLoader =
-                       new PreferencesLoader(preferences);
-
-       @Before
-       public void setupConfiguration() {
-               setupIntValue("InsertionDelay", 15);
-               setupIntValue("PostsPerPage", 25);
-               setupIntValue("ImagesPerPage", 12);
-               setupIntValue("CharactersPerPost", 150);
-               setupIntValue("PostCutOffLength", 300);
-               setupBooleanValue("RequireFullAccess", true);
-               setupBooleanValue("ActivateFcpInterface", true);
-               setupIntValue("FcpFullAccessRequired", 1);
-       }
-
-       private void setupIntValue(String optionName, int value) {
-               when(configuration.getIntValue("Option/" + optionName)).thenReturn(
-                               TestValue.from(value));
-       }
-
-       private void setupBooleanValue(String optionName, boolean value) {
-               when(configuration.getBooleanValue(
-                               "Option/" + optionName)).thenReturn(
-                               TestValue.from(value));
-       }
-
-       @Test
-       public void configurationIsLoadedCorrectly() {
-               setupConfiguration();
-               preferencesLoader.loadFrom(configuration);
-               assertThat(preferences.getInsertionDelay(), is(15));
-               assertThat(preferences.getPostsPerPage(), is(25));
-               assertThat(preferences.getImagesPerPage(), is(12));
-               assertThat(preferences.getCharactersPerPost(), is(150));
-               assertThat(preferences.getPostCutOffLength(), is(300));
-               assertThat(preferences.getRequireFullAccess(), is(true));
-               assertThat(preferences.getFcpInterfaceActive(), is(true));
-               assertThat(preferences.getFcpFullAccessRequired(), is(WRITING));
-       }
-
-       @Test
-       public void configurationIsLoadedCorrectlyWithCutOffLengthMinusOne() {
-           setupConfiguration();
-               setupIntValue("PostCutOffLength", -1);
-               preferencesLoader.loadFrom(configuration);
-               assertThat(preferences.getPostCutOffLength(), not(is(-1)));
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/PreferencesLoaderTest.kt
new file mode 100644 (file)
index 0000000..0cd7e6d
--- /dev/null
@@ -0,0 +1,61 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.*
+import net.pterodactylus.sone.fcp.FcpInterface.*
+import net.pterodactylus.util.config.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [PreferencesLoader].
+ */
+class PreferencesLoaderTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val preferences = Preferences(eventBus)
+       private val configuration = Configuration(MapConfigurationBackend())
+       private val preferencesLoader = PreferencesLoader(preferences)
+
+       @Before
+       fun setupConfiguration() {
+               setupIntValue("InsertionDelay", 15)
+               setupIntValue("PostsPerPage", 25)
+               setupIntValue("ImagesPerPage", 12)
+               setupIntValue("CharactersPerPost", 150)
+               setupIntValue("PostCutOffLength", 300)
+               setupBooleanValue("RequireFullAccess", true)
+               setupBooleanValue("ActivateFcpInterface", true)
+               setupIntValue("FcpFullAccessRequired", 1)
+       }
+
+       private fun setupIntValue(optionName: String, value: Int) {
+               configuration.getIntValue("Option/$optionName").value = value
+       }
+
+       private fun setupBooleanValue(optionName: String, value: Boolean) {
+               configuration.getBooleanValue("Option/$optionName").value = value
+       }
+
+       @Test
+       fun `configuration is loaded correctly`() {
+               preferencesLoader.loadFrom(configuration)
+               assertThat(preferences.insertionDelay, equalTo(15))
+               assertThat(preferences.postsPerPage, equalTo(25))
+               assertThat(preferences.imagesPerPage, equalTo(12))
+               assertThat(preferences.charactersPerPost, equalTo(150))
+               assertThat(preferences.postCutOffLength, equalTo(300))
+               assertThat(preferences.requireFullAccess, equalTo(true))
+               assertThat(preferences.fcpInterfaceActive, equalTo(true))
+               assertThat(preferences.fcpFullAccessRequired, equalTo(FullAccessRequired.WRITING))
+       }
+
+       @Test
+       fun `configuration is loaded correctly with cut off length minus one`() {
+               setupIntValue("PostCutOffLength", -1)
+               preferencesLoader.loadFrom(configuration)
+               assertThat(preferences.postCutOffLength, not(equalTo(-1)))
+       }
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt b/src/test/kotlin/net/pterodactylus/sone/data/AlbumsTest.kt
new file mode 100644 (file)
index 0000000..9c06a5e
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Sone - AlbumsTest.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.data
+
+import net.pterodactylus.sone.data.impl.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for various helper method in `Albums.kt`.
+ */
+class AlbumsTest {
+
+       @Test
+       fun `recursive list of all images for album is returned correctly`() {
+               val sone = IdOnlySone("sone")
+               val album = AlbumImpl(sone)
+               val firstNestedAlbum = AlbumImpl(sone)
+               val secondNestedAlbum = AlbumImpl(sone)
+               firstNestedAlbum.addImage(createImage(sone, "image-1"))
+               firstNestedAlbum.addImage(createImage(sone, "image-2"))
+               secondNestedAlbum.addImage(createImage(sone, "image-3"))
+               album.addImage(createImage(sone, "image-4"))
+               album.addAlbum(firstNestedAlbum)
+               album.addAlbum(secondNestedAlbum)
+               val images = album.allImages
+               assertThat(images.map(Image::id), containsInAnyOrder("image-1", "image-2", "image-3", "image-4"))
+       }
+
+       private fun createImage(sone: IdOnlySone, id: String) = ImageImpl(id).modify().setSone(sone).update()
+
+}
index 9388cb0..eef312f 100644 (file)
@@ -10,6 +10,7 @@ import net.pterodactylus.sone.fcp.*
 import net.pterodactylus.sone.freenet.wot.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.web.*
+import net.pterodactylus.sone.web.notification.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.mockito.Mockito.*
@@ -64,6 +65,12 @@ class SonePluginTest {
                assertThat(injector.getInstance<WebOfTrustConnector>(), notNullValue())
        }
 
+       @Test
+       fun `notification handler can be created`() {
+               val injector: Injector = runSonePluginWithRealInjector()
+               assertThat(injector.getInstance<NotificationHandler>(), notNullValue())
+       }
+
        private fun runSonePluginWithRealInjector(): Injector {
                lateinit var injector: Injector
                val sonePlugin = SonePlugin {
@@ -83,6 +90,13 @@ class SonePluginTest {
                verify(core).start()
        }
 
+       @Test
+       fun `notification handler is being started`() {
+               sonePlugin.runPlugin(pluginRespirator)
+               val notificationHandler = injector.getInstance<NotificationHandler>()
+               verify(notificationHandler).start()
+       }
+
 }
 
 private fun mockInjector() = mock<Injector>().apply {
diff --git a/src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt b/src/test/kotlin/net/pterodactylus/sone/utils/RenderablesTest.kt
new file mode 100644 (file)
index 0000000..d921270
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Sone - RenderablesTest.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.utils
+
+import net.pterodactylus.util.io.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit tests for tools in `Renderables.kt`.
+ */
+class RenderablesTest {
+
+       @Test
+       fun `render method renders notification`() {
+               val notification = Renderable { writer -> writer.use { it.append("Test!\n") } }
+               assertThat(notification.render(), equalTo("Test!\n"))
+       }
+
+}
index 14427ef..2bb31c9 100644 (file)
@@ -14,7 +14,9 @@ import net.pterodactylus.sone.main.*
 import net.pterodactylus.sone.template.*
 import net.pterodactylus.sone.test.*
 import net.pterodactylus.sone.text.*
+import net.pterodactylus.sone.web.notification.*
 import net.pterodactylus.sone.web.page.*
+import net.pterodactylus.util.notify.*
 import net.pterodactylus.util.template.*
 import net.pterodactylus.util.web.*
 import org.hamcrest.MatcherAssert.*
@@ -283,4 +285,23 @@ class WebInterfaceModuleTest {
            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))
+       }
+
 }
diff --git a/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/NotificationHandlerTest.kt
new file mode 100644 (file)
index 0000000..d145038
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * 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/SoneLockedOnStartupHandlerTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/notification/SoneLockedOnStartupHandlerTest.kt
new file mode 100644 (file)
index 0000000..0b9d1e1
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Sone - SoneLockedOnStartupNotificationTest.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.impl.*
+import net.pterodactylus.sone.notify.*
+import net.pterodactylus.sone.utils.*
+import net.pterodactylus.util.notify.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+/**
+ * Unit test for [SoneLockedOnStartupHandler].
+ */
+class SoneLockedOnStartupHandlerTest {
+
+       @Suppress("UnstableApiUsage")
+       private val eventBus = EventBus()
+       private val manager = NotificationManager()
+       private val notification by lazy { manager.notifications.single() as ListNotification<*> }
+
+       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"))
+       }
+
+       @Test
+       fun `handler adds sone to notification when event is posted`() {
+               assertThat(notification.elements, contains<Any>(sone))
+       }
+
+       @Test
+       fun `handler creates notification with correct key`() {
+               assertThat(notification.render(), equalTo(listOf(sone).toString()))
+       }
+
+}
+
+private val sone = IdOnlySone("sone-id")
+private val template = "<% sones>".asTemplate()