From 3da1daa6488c642430858a4baacc0aa986a6ebc1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Wed, 8 Jun 2016 21:38:35 +0200 Subject: [PATCH] Refactor notification filtering --- .../sone/notify/ListNotificationFilters.java | 207 +++------------ .../sone/notify/PostVisibilityFilter.java | 98 +++++++ .../sone/notify/ReplyVisibilityFilter.java | 73 ++++++ .../sone/web/DismissNotificationPage.java | 8 +- .../java/net/pterodactylus/sone/web/IndexPage.java | 23 +- .../java/net/pterodactylus/sone/web/NewPage.java | 19 +- .../pterodactylus/sone/web/SoneTemplatePage.java | 25 +- .../net/pterodactylus/sone/web/WebInterface.java | 56 +++- .../sone/web/ajax/DismissNotificationAjaxPage.java | 10 +- .../sone/web/ajax/GetNotificationsAjaxPage.java | 12 +- .../sone/web/ajax/GetStatusAjaxPage.java | 32 +-- .../sone/notify/ListNotificationFiltersTest.java | 281 +++++++++++++++++++++ .../sone/notify/PostVisibilityFilterTest.java | 203 +++++++++++++++ .../sone/notify/ReplyVisibilityFilterTest.java | 106 ++++++++ 14 files changed, 892 insertions(+), 261 deletions(-) create mode 100644 src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java create mode 100644 src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java create mode 100644 src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java create mode 100644 src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java create mode 100644 src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java index 8a423c2..0a2a518 100644 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java @@ -17,18 +17,18 @@ package net.pterodactylus.sone.notify; -import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.FluentIterable.from; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.inject.Singleton; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.freenet.wot.OwnIdentity; -import net.pterodactylus.sone.freenet.wot.Trust; import net.pterodactylus.util.notify.Notification; import com.google.common.base.Optional; @@ -38,8 +38,18 @@ import com.google.common.base.Optional; * * @author David ‘Bombe’ Roden */ +@Singleton public class ListNotificationFilters { + private final PostVisibilityFilter postVisibilityFilter; + private final ReplyVisibilityFilter replyVisibilityFilter; + + @Inject + public ListNotificationFilters(@Nonnull PostVisibilityFilter postVisibilityFilter, @Nonnull ReplyVisibilityFilter replyVisibilityFilter) { + this.postVisibilityFilter = postVisibilityFilter; + this.replyVisibilityFilter = replyVisibilityFilter; + } + /** * Filters new-post and new-reply notifications in the given list of * notifications. If {@code currentSone} is null, new-post and @@ -49,13 +59,13 @@ public class ListNotificationFilters { * itself will be retained in the notifications. * * @param notifications - * The notifications to filter + * The notifications to filter * @param currentSone - * The current Sone, or {@code null} if not logged in + * The current Sone, or {@code null} if not logged in * @return The filtered notifications */ @SuppressWarnings("unchecked") - public static List filterNotifications(Collection notifications, Sone currentSone) { + public List filterNotifications(Collection notifications, Sone currentSone) { List filteredNotifications = new ArrayList(); for (Notification notification : notifications) { if (notification.getId().equals("new-sone-notification")) { @@ -64,23 +74,30 @@ public class ListNotificationFilters { } filteredNotifications.add(notification); } else if (notification.getId().equals("new-post-notification")) { - if ((currentSone != null) && !currentSone.getOptions().isShowNewPostNotifications()) { + if (currentSone == null) { continue; } - Optional> filteredNotification = filterNewPostNotification((ListNotification) notification, currentSone, true); + if (!currentSone.getOptions().isShowNewPostNotifications()) { + continue; + } + Optional> filteredNotification = filterNewPostNotification((ListNotification) notification, currentSone); if (filteredNotification.isPresent()) { filteredNotifications.add(filteredNotification.get()); } } else if (notification.getId().equals("new-reply-notification")) { - if ((currentSone != null) && !currentSone.getOptions().isShowNewReplyNotifications()) { + if (currentSone == null) { continue; } - Optional> filteredNotification = filterNewReplyNotification((ListNotification) notification, currentSone); + if (!currentSone.getOptions().isShowNewReplyNotifications()) { + continue; + } + Optional> filteredNotification = + filterNewReplyNotification((ListNotification) notification, currentSone); if (filteredNotification.isPresent()) { filteredNotifications.add(filteredNotification.get()); } } else if (notification.getId().equals("mention-notification")) { - Optional> filteredNotification = filterNewPostNotification((ListNotification) notification, null, false); + Optional> filteredNotification = filterNewPostNotification((ListNotification) notification, null); if (filteredNotification.isPresent()) { filteredNotifications.add(filteredNotification.get()); } @@ -99,25 +116,16 @@ public class ListNotificationFilters { * other posts are removed. * * @param newPostNotification - * The new-post notification + * The new-post notification * @param currentSone - * The current Sone, or {@code null} if not logged in - * @param soneRequired - * Whether a non-{@code null} Sone in {@code currentSone} is - * required + * The current Sone, or {@code null} if not logged in * @return The filtered new-post notification, or {@code null} if the - * notification should be removed + * notification should be removed */ - private static Optional> filterNewPostNotification(ListNotification newPostNotification, Sone currentSone, boolean soneRequired) { - if (soneRequired && (currentSone == null)) { - return Optional.absent(); - } - List newPosts = new ArrayList(); - for (Post post : newPostNotification.getElements()) { - if (isPostVisible(currentSone, post)) { - newPosts.add(post); - } - } + @Nonnull + private Optional> filterNewPostNotification(@Nonnull ListNotification newPostNotification, + @Nonnull Sone currentSone) { + List newPosts = from(newPostNotification.getElements()).filter(postVisibilityFilter.isVisible(currentSone)).toList(); if (newPosts.isEmpty()) { return Optional.absent(); } @@ -138,22 +146,15 @@ public class ListNotificationFilters { * replies are removed. * * @param newReplyNotification - * The new-reply notification + * The new-reply notification * @param currentSone - * The current Sone, or {@code null} if not logged in + * The current Sone, or {@code null} if not logged in * @return The filtered new-reply notification, or {@code null} if the - * notification should be removed + * notification should be removed */ - private static Optional> filterNewReplyNotification(ListNotification newReplyNotification, Sone currentSone) { - if (currentSone == null) { - return Optional.absent(); - } - List newReplies = new ArrayList(); - for (PostReply reply : newReplyNotification.getElements()) { - if (isReplyVisible(currentSone, reply)) { - newReplies.add(reply); - } - } + private Optional> filterNewReplyNotification(ListNotification newReplyNotification, + @Nonnull Sone currentSone) { + List newReplies = from(newReplyNotification.getElements()).filter(replyVisibilityFilter.isVisible(currentSone)).toList(); if (newReplies.isEmpty()) { return Optional.absent(); } @@ -166,128 +167,4 @@ public class ListNotificationFilters { return Optional.of(filteredNotification); } - /** - * Filters the given posts, using {@link #isPostVisible(Sone, Post)} to - * decide whether a post should be contained in the returned list. If - * {@code currentSone} is not {@code null} it is used to filter out posts - * that are from Sones that are not followed or not trusted by the given - * Sone. - * - * @param posts - * The posts to filter - * @param currentSone - * The current Sone (may be {@code null}) - * @return The filtered posts - */ - public static List filterPosts(Collection posts, Sone currentSone) { - List filteredPosts = new ArrayList(); - for (Post post : posts) { - if (isPostVisible(currentSone, post)) { - filteredPosts.add(post); - } - } - return filteredPosts; - } - - /** - * Checks whether a post is visible to the given Sone. A post is not - * considered visible if one of the following statements is true: - *
    - *
  • The post does not have a Sone.
  • - *
  • The post’s {@link Post#getTime() time} is in the future.
  • - *
- *

- * If {@code post} is not {@code null} more checks are performed, and the - * post will be invisible if: - *

- *
    - *
  • The Sone of the post is not the given Sone, the given Sone does not - * follow the post’s Sone, and the given Sone is not the recipient of the - * post.
  • - *
  • The trust relationship between the two Sones can not be retrieved.
  • - *
  • The given Sone has explicitely assigned negative trust to the post’s - * Sone.
  • - *
  • The given Sone has not explicitely assigned negative trust to the - * post’s Sone but the implicit trust is negative.
  • - *
- * If none of these statements is true the post is considered visible. - * - * @param sone - * The Sone that checks for a post’s visibility (may be - * {@code null} to skip Sone-specific checks, such as trust) - * @param post - * The post to check for visibility - * @return {@code true} if the post is considered visible, {@code false} - * otherwise - */ - public static boolean isPostVisible(Sone sone, Post post) { - checkNotNull(post, "post must not be null"); - if (!post.isLoaded()) { - return false; - } - Sone postSone = post.getSone(); - if (sone != null) { - Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity()); - if (trust != null) { - if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) { - return false; - } - if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) { - return false; - } - } else { - /* - * a null trust means that the trust updater has not yet - * received a trust value for this relation. if we return false, - * the post feed will stay empty until the trust updater has - * received trust values. to prevent this we simply assume that - * posts are visible if there is no trust. - */ - } - if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.getId().equals(post.getRecipientId().orNull())) { - return false; - } - } - return post.getTime() <= System.currentTimeMillis(); - } - - /** - * Checks whether a reply is visible to the given Sone. A reply is not - * considered visible if one of the following statements is true: - *
    - *
  • The reply does not have a post.
  • - *
  • The reply’s post does not have a Sone.
  • - *
  • The Sone of the reply’s post is not the given Sone, the given Sone - * does not follow the reply’s post’s Sone, and the given Sone is not the - * recipient of the reply’s post.
  • - *
  • The trust relationship between the two Sones can not be retrieved.
  • - *
  • The given Sone has explicitely assigned negative trust to the post’s - * Sone.
  • - *
  • The given Sone has not explicitely assigned negative trust to the - * reply’s post’s Sone but the implicit trust is negative.
  • - *
  • The reply’s post’s {@link Post#getTime() time} is in the future.
  • - *
  • The reply’s {@link Reply#getTime() time} is in the future.
  • - *
- * If none of these statements is true the reply is considered visible. - * - * @param sone - * The Sone that checks for a post’s visibility (may be - * {@code null} to skip Sone-specific checks, such as trust) - * @param reply - * The reply to check for visibility - * @return {@code true} if the reply is considered visible, {@code false} - * otherwise - */ - public static boolean isReplyVisible(Sone sone, PostReply reply) { - checkNotNull(reply, "reply must not be null"); - Optional post = reply.getPost(); - if (!post.isPresent()) { - return false; - } - if (!isPostVisible(sone, post.get())) { - return false; - } - return reply.getTime() <= System.currentTimeMillis(); - } - } diff --git a/src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java b/src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java new file mode 100644 index 0000000..edcd148 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java @@ -0,0 +1,98 @@ +package net.pterodactylus.sone.notify; + +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Singleton; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; +import net.pterodactylus.sone.freenet.wot.Trust; +import net.pterodactylus.util.notify.Notification; + +import com.google.common.base.Predicate; + +/** + * Filters {@link Notification}s involving {@link Post}s. + * + * @author David ‘Bombe’ Roden + */ +@Singleton +public class PostVisibilityFilter { + + /** + * Checks whether a post is visible to the given Sone. A post is not + * considered visible if one of the following statements is true: + *
    + *
  • The post does not have a Sone.
  • + *
  • The post’s {@link Post#getTime() time} is in the future.
  • + *
+ *

+ * If {@code post} is not {@code null} more checks are performed, and the + * post will be invisible if: + *

+ *
    + *
  • The Sone of the post is not the given Sone, the given Sone does not + * follow the post’s Sone, and the given Sone is not the recipient of the + * post.
  • + *
  • The trust relationship between the two Sones can not be retrieved.
  • + *
  • The given Sone has explicitely assigned negative trust to the post’s + * Sone.
  • + *
  • The given Sone has not explicitely assigned negative trust to the + * post’s Sone but the implicit trust is negative.
  • + *
+ * If none of these statements is true the post is considered visible. + * + * @param sone + * The Sone that checks for a post’s visibility (may be + * {@code null} to skip Sone-specific checks, such as trust) + * @param post + * The post to check for visibility + * @return {@code true} if the post is considered visible, {@code false} + * otherwise + */ + boolean isPostVisible(@Nullable Sone sone, @Nonnull Post post) { + checkNotNull(post, "post must not be null"); + if (!post.isLoaded()) { + return false; + } + Sone postSone = post.getSone(); + if (sone != null) { + Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity()); + if (trust != null) { + if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) { + return false; + } + if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) { + return false; + } + } else { + /* + * a null trust means that the trust updater has not yet + * received a trust value for this relation. if we return false, + * the post feed will stay empty until the trust updater has + * received trust values. to prevent this we simply assume that + * posts are visible if there is no trust. + */ + } + if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.getId().equals(post.getRecipientId().orNull())) { + return false; + } + } + return post.getTime() <= System.currentTimeMillis(); + } + + @Nonnull + public Predicate isVisible(@Nullable final Sone currentSone) { + return new Predicate() { + @Nonnull + @Override + public boolean apply(@Nullable Post post) { + return isPostVisible(currentSone, post); + } + }; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java b/src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java new file mode 100644 index 0000000..a76ae25 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java @@ -0,0 +1,73 @@ +package net.pterodactylus.sone.notify; + +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; + +/** + * Filter that checks a {@link PostReply} for visibility. + * + * @author David ‘Bombe’ Roden + */ +@Singleton +public class ReplyVisibilityFilter { + + private final PostVisibilityFilter postVisibilityFilter; + + @Inject + public ReplyVisibilityFilter(@Nonnull PostVisibilityFilter postVisibilityFilter) { + this.postVisibilityFilter = postVisibilityFilter; + } + + /** + * Checks whether a reply is visible to the given Sone. A reply is not + * considered visible if one of the following statements is true: + *
    + *
  • The reply does not have a post.
  • + *
  • The reply’s post {@link PostVisibilityFilter#isPostVisible(Sone, Post) is not visible}.
  • + *
  • The reply’s {@link PostReply#getTime() time} is in the future.
  • + *
+ * If none of these statements is true the reply is considered visible. + * + * @param sone + * The Sone that checks for a post’s visibility (may be + * {@code null} to skip Sone-specific checks, such as trust) + * @param reply + * The reply to check for visibility + * @return {@code true} if the reply is considered visible, {@code false} + * otherwise + */ + boolean isReplyVisible(@Nullable Sone sone, @Nonnull PostReply reply) { + checkNotNull(reply, "reply must not be null"); + Optional post = reply.getPost(); + if (!post.isPresent()) { + return false; + } + if (!postVisibilityFilter.isPostVisible(sone, post.get())) { + return false; + } + return reply.getTime() <= System.currentTimeMillis(); + } + + @Nonnull + public Predicate isVisible(@Nullable final Sone currentSone) { + return new Predicate() { + @Nonnull + @Override + public boolean apply(@Nullable PostReply postReply) { + return isReplyVisible(currentSone, postReply); + } + }; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java b/src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java index d5bb6ea..24b6aaa 100644 --- a/src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java +++ b/src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java @@ -22,6 +22,8 @@ import net.pterodactylus.util.notify.Notification; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; +import com.google.common.base.Optional; + /** * Page that lets the user dismiss a notification. * @@ -52,9 +54,9 @@ public class DismissNotificationPage extends SoneTemplatePage { protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException { super.processTemplate(request, templateContext); String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36); - Notification notification = webInterface.getNotifications().getNotification(notificationId); - if ((notification != null) && notification.isDismissable()) { - notification.dismiss(); + Optional notification = webInterface.getNotification(notificationId); + if (notification.isPresent() && notification.get().isDismissable()) { + notification.get().dismiss(); } String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256); throw new RedirectException(returnPage); diff --git a/src/main/java/net/pterodactylus/sone/web/IndexPage.java b/src/main/java/net/pterodactylus/sone/web/IndexPage.java index c5e80c4..12d88db 100644 --- a/src/main/java/net/pterodactylus/sone/web/IndexPage.java +++ b/src/main/java/net/pterodactylus/sone/web/IndexPage.java @@ -26,14 +26,13 @@ import java.util.List; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.notify.ListNotificationFilters; +import net.pterodactylus.sone.notify.PostVisibilityFilter; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import com.google.common.base.Optional; -import com.google.common.base.Predicate; import com.google.common.collect.Collections2; /** @@ -44,14 +43,11 @@ import com.google.common.collect.Collections2; */ public class IndexPage extends SoneTemplatePage { - /** - * @param template - * The template to render - * @param webInterface - * The Sone web interface - */ - public IndexPage(Template template, WebInterface webInterface) { + private final PostVisibilityFilter postVisibilityFilter; + + public IndexPage(Template template, WebInterface webInterface, PostVisibilityFilter postVisibilityFilter) { super("index.html", template, "Page.Index.Title", webInterface, true); + this.postVisibilityFilter = postVisibilityFilter; } // @@ -81,14 +77,7 @@ public class IndexPage extends SoneTemplatePage { } } } - allPosts = Collections2.filter(allPosts, new Predicate() { - - @Override - public boolean apply(Post post) { - return ListNotificationFilters.isPostVisible(currentSone, post); - } - }); - allPosts = Collections2.filter(allPosts, Post.FUTURE_POSTS_FILTER); + allPosts = Collections2.filter(allPosts, postVisibilityFilter.isVisible(currentSone)); List sortedPosts = new ArrayList(allPosts); Collections.sort(sortedPosts, Post.TIME_COMPARATOR); Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); diff --git a/src/main/java/net/pterodactylus/sone/web/NewPage.java b/src/main/java/net/pterodactylus/sone/web/NewPage.java index ee28a58..6aa07d9 100644 --- a/src/main/java/net/pterodactylus/sone/web/NewPage.java +++ b/src/main/java/net/pterodactylus/sone/web/NewPage.java @@ -21,15 +21,12 @@ import static net.pterodactylus.sone.utils.NumberParsers.parseInt; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; - -import com.google.common.collect.Collections2; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; -import net.pterodactylus.sone.notify.ListNotificationFilters; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.notify.PostVisibilityFilter; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.collection.Pagination; import net.pterodactylus.util.template.Template; @@ -37,8 +34,7 @@ import net.pterodactylus.util.template.TemplateContext; /** * Page that displays all new posts and replies. The posts are filtered using - * {@link ListNotificationFilters#filterPosts(java.util.Collection, net.pterodactylus.sone.data.Sone)} - * and sorted by time. + * {@link PostVisibilityFilter#isPostVisible(Sone, Post)} and sorted by time. * * @author David ‘Bombe’ Roden */ @@ -68,17 +64,16 @@ public class NewPage extends SoneTemplatePage { super.processTemplate(request, templateContext); /* collect new elements from notifications. */ - Set posts = new HashSet(webInterface.getNewPosts()); - for (PostReply reply : Collections2.filter(webInterface.getNewReplies(), PostReply.HAS_POST_FILTER)) { + List posts = new ArrayList(webInterface.getNewPosts(getCurrentSone(request.getToadletContext(), false))); + for (PostReply reply : webInterface.getNewReplies(getCurrentSone(request.getToadletContext(), false))) { posts.add(reply.getPost().get()); } /* filter and sort them. */ - List sortedPosts = ListNotificationFilters.filterPosts(posts, webInterface.getCurrentSone(request.getToadletContext(), false)); - Collections.sort(sortedPosts, Post.TIME_COMPARATOR); + Collections.sort(posts, Post.TIME_COMPARATOR); /* paginate them. */ - Pagination pagination = new Pagination(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); + Pagination pagination = new Pagination(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", pagination); templateContext.set("posts", pagination.getItems()); } diff --git a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java index 15810ca..18836e9 100644 --- a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java @@ -19,6 +19,7 @@ package net.pterodactylus.sone.web; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -27,20 +28,19 @@ import java.util.Map; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.main.SonePlugin; -import net.pterodactylus.sone.notify.ListNotificationFilters; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.sone.web.page.FreenetTemplatePage; import net.pterodactylus.util.notify.Notification; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - import freenet.clients.http.SessionManager.Session; import freenet.clients.http.ToadletContext; import freenet.support.api.HTTPRequest; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + /** * Base page for the Sone web interface. * @@ -65,21 +65,6 @@ public class SoneTemplatePage extends FreenetTemplatePage { * The path of the page * @param template * The template to render - * @param webInterface - * The Sone web interface - */ - public SoneTemplatePage(String path, Template template, WebInterface webInterface) { - this(path, template, null, webInterface, false); - } - - /** - * Creates a new template page for Sone that does not require the user to be - * logged in. - * - * @param path - * The path of the page - * @param template - * The template to render * @param pageTitleKey * The l10n key of the page title * @param webInterface @@ -263,7 +248,7 @@ public class SoneTemplatePage extends FreenetTemplatePage { templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition()); templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion()); templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate()); - List notifications = ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone); + List notifications = new ArrayList(webInterface.getNotifications(currentSone)); Collections.sort(notifications, Notification.CREATED_TIME_SORTER); templateContext.set("notifications", notifications); templateContext.set("notificationHash", notifications.hashCode()); diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 4b4ac88..4e4f6fa 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -17,6 +17,7 @@ package net.pterodactylus.sone.web; +import static com.google.common.collect.FluentIterable.from; import static java.util.logging.Logger.getLogger; import static net.pterodactylus.util.template.TemplateParser.parse; @@ -37,6 +38,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import net.pterodactylus.sone.core.Core; import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent; @@ -72,6 +75,9 @@ import net.pterodactylus.sone.main.Loaders; import net.pterodactylus.sone.main.ReparseFilter; import net.pterodactylus.sone.main.SonePlugin; import net.pterodactylus.sone.notify.ListNotification; +import net.pterodactylus.sone.notify.ListNotificationFilters; +import net.pterodactylus.sone.notify.PostVisibilityFilter; +import net.pterodactylus.sone.notify.ReplyVisibilityFilter; import net.pterodactylus.sone.template.AlbumAccessor; import net.pterodactylus.sone.template.CollectionAccessor; import net.pterodactylus.sone.template.CssClassNameFilter; @@ -147,11 +153,6 @@ import net.pterodactylus.util.template.XmlFilter; import net.pterodactylus.util.web.RedirectPage; import net.pterodactylus.util.web.TemplatePage; -import com.google.common.collect.Collections2; -import com.google.common.collect.ImmutableSet; -import com.google.common.eventbus.Subscribe; -import com.google.inject.Inject; - import freenet.clients.http.SessionManager; import freenet.clients.http.SessionManager.Session; import freenet.clients.http.ToadletContainer; @@ -159,6 +160,12 @@ import freenet.clients.http.ToadletContext; import freenet.l10n.BaseL10n; import freenet.support.api.HTTPRequest; +import com.google.common.base.Optional; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; +import com.google.common.eventbus.Subscribe; +import com.google.inject.Inject; + /** * Bundles functionality that a web interface of a Freenet plugin needs, e.g. * references to l10n helpers. @@ -194,6 +201,10 @@ public class WebInterface { /** The parser filter. */ private final ParserFilter parserFilter; + private final ListNotificationFilters listNotificationFilters; + private final PostVisibilityFilter postVisibilityFilter; + private final ReplyVisibilityFilter replyVisibilityFilter; + /** The “new Sone” notification. */ private final ListNotification newSoneNotification; @@ -243,9 +254,12 @@ public class WebInterface { * The Sone plugin */ @Inject - public WebInterface(SonePlugin sonePlugin, Loaders loaders) { + public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilters listNotificationFilters, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter) { this.sonePlugin = sonePlugin; this.loaders = loaders; + this.listNotificationFilters = listNotificationFilters; + this.postVisibilityFilter = postVisibilityFilter; + this.replyVisibilityFilter = replyVisibilityFilter; formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword(); soneTextParser = new SoneTextParser(getCore(), getCore()); @@ -454,6 +468,16 @@ public class WebInterface { return notificationManager; } + @Nonnull + public Optional getNotification(@Nonnull String notificationId) { + return Optional.fromNullable(notificationManager.getNotification(notificationId)); + } + + @Nonnull + public Collection getNotifications(@Nullable Sone currentSone) { + return listNotificationFilters.filterNotifications(notificationManager.getNotifications(), currentSone); + } + /** * Returns the l10n helper of the node. * @@ -491,6 +515,15 @@ public class WebInterface { return ImmutableSet. builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build(); } + @Nonnull + public Collection getNewPosts(@Nullable Sone currentSone) { + Set allNewPosts = ImmutableSet. builder() + .addAll(newPostNotification.getElements()) + .addAll(localPostNotification.getElements()) + .build(); + return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet(); + } + /** * Returns the replies that have been announced as new in the * {@link #newReplyNotification}. @@ -501,6 +534,15 @@ public class WebInterface { return ImmutableSet. builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build(); } + @Nonnull + public Collection getNewReplies(@Nullable Sone currentSone) { + Set allNewReplies = ImmutableSet.builder() + .addAll(newReplyNotification.getElements()) + .addAll(localReplyNotification.getElements()) + .build(); + 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. @@ -638,7 +680,7 @@ public class WebInterface { PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/"); pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html"))); - pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this, postVisibilityFilter), "Index")); pageToadlets.add(pageToadletFactory.createPageToadlet(new NewPage(newTemplate, this), "New")); pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone")); pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones")); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java index 4e731e8..7ee2005 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java @@ -21,6 +21,8 @@ import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.notify.Notification; +import com.google.common.base.Optional; + /** * AJAX page that lets the user dismiss a notification. * @@ -44,14 +46,14 @@ public class DismissNotificationAjaxPage extends JsonPage { @Override protected JsonReturnObject createJsonObject(FreenetRequest request) { String notificationId = request.getHttpRequest().getParam("notification"); - Notification notification = webInterface.getNotifications().getNotification(notificationId); - if (notification == null) { + Optional notification = webInterface.getNotification(notificationId); + if (!notification.isPresent()) { return createErrorJsonObject("invalid-notification-id"); } - if (!notification.isDismissable()) { + if (!notification.get().isDismissable()) { return createErrorJsonObject("not-dismissable"); } - notification.dismiss(); + notification.get().dismiss(); return createSuccessJsonObject(); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java index ee4683f..512a954 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java @@ -21,13 +21,12 @@ import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance; import java.io.IOException; import java.io.StringWriter; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.main.SonePlugin; -import net.pterodactylus.sone.notify.ListNotificationFilters; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetRequest; import net.pterodactylus.util.notify.Notification; @@ -81,14 +80,13 @@ public class GetNotificationsAjaxPage extends JsonPage { @Override protected JsonReturnObject createJsonObject(FreenetRequest request) { Sone currentSone = getCurrentSone(request.getToadletContext(), false); - Collection notifications = webInterface.getNotifications().getNotifications(); - List filteredNotifications = ListNotificationFilters.filterNotifications(notifications, currentSone); - Collections.sort(filteredNotifications, Notification.CREATED_TIME_SORTER); + List notifications = new ArrayList(webInterface.getNotifications(currentSone)); + Collections.sort(notifications, Notification.CREATED_TIME_SORTER); ArrayNode jsonNotifications = new ArrayNode(instance); - for (Notification notification : filteredNotifications) { + for (Notification notification : notifications) { jsonNotifications.add(createJsonNotification(request, notification)); } - return createSuccessJsonObject().put("notificationHash", filteredNotifications.hashCode()).put("notifications", jsonNotifications).put("options", createJsonOptions(currentSone)); + return createSuccessJsonObject().put("notificationHash", notifications.hashCode()).put("notifications", jsonNotifications).put("options", createJsonOptions(currentSone)); } // diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java index 6c5c0de..afba58f 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java @@ -21,6 +21,7 @@ import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -31,7 +32,8 @@ import java.util.Set; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.sone.notify.ListNotificationFilters; +import net.pterodactylus.sone.notify.PostVisibilityFilter; +import net.pterodactylus.sone.notify.ReplyVisibilityFilter; import net.pterodactylus.sone.template.SoneAccessor; import net.pterodactylus.sone.web.WebInterface; import net.pterodactylus.sone.web.page.FreenetRequest; @@ -40,8 +42,6 @@ import net.pterodactylus.util.notify.Notification; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Predicate; -import com.google.common.collect.Collections2; /** * The “get status” AJAX handler returns all information that is necessary to @@ -88,20 +88,11 @@ public class GetStatusAjaxPage extends JsonPage { jsonSones.add(createJsonSone(sone)); } /* load notifications. */ - List notifications = ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone); + List notifications = new ArrayList(webInterface.getNotifications(currentSone)); Collections.sort(notifications, Notification.CREATED_TIME_SORTER); /* load new posts. */ - Collection newPosts = webInterface.getNewPosts(); - if (currentSone != null) { - newPosts = Collections2.filter(newPosts, new Predicate() { - - @Override - public boolean apply(Post post) { - return ListNotificationFilters.isPostVisible(currentSone, post); - } + Collection newPosts = webInterface.getNewPosts(getCurrentSone(request.getToadletContext(), false)); - }); - } ArrayNode jsonPosts = new ArrayNode(instance); for (Post post : newPosts) { ObjectNode jsonPost = new ObjectNode(instance); @@ -112,19 +103,8 @@ public class GetStatusAjaxPage extends JsonPage { jsonPosts.add(jsonPost); } /* load new replies. */ - Collection newReplies = webInterface.getNewReplies(); - if (currentSone != null) { - newReplies = Collections2.filter(newReplies, new Predicate() { - - @Override - public boolean apply(PostReply reply) { - return ListNotificationFilters.isReplyVisible(currentSone, reply); - } + Collection newReplies = webInterface.getNewReplies(getCurrentSone(request.getToadletContext(), false)); - }); - } - /* remove replies to unknown posts. */ - newReplies = Collections2.filter(newReplies, PostReply.HAS_POST_FILTER); ArrayNode jsonReplies = new ArrayNode(instance); for (PostReply reply : newReplies) { ObjectNode jsonReply = new ObjectNode(instance); diff --git a/src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java b/src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java new file mode 100644 index 0000000..f6b7da5 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java @@ -0,0 +1,281 @@ +package net.pterodactylus.sone.notify; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.data.SoneOptions; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; +import net.pterodactylus.util.notify.Notification; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Unit test for {@link ListNotificationFiltersTest}. + * + * @author David ‘Bombe’ Roden + */ +public class ListNotificationFiltersTest { + + private static final String LOCAL_ID = "local-id"; + + private final PostVisibilityFilter postVisibilityFilter = mock(PostVisibilityFilter.class); + private final ReplyVisibilityFilter replyVisibilityFilter = mock(ReplyVisibilityFilter.class); + private final ListNotificationFilters listNotificationFilters = new ListNotificationFilters(postVisibilityFilter, replyVisibilityFilter); + + private final Sone localSone = mock(Sone.class); + private final SoneOptions soneOptions = mock(SoneOptions.class); + private final OwnIdentity localIdentity = mock(OwnIdentity.class); + private final List> newPostNotifications = Arrays.asList(createNewPostNotification()); + private final List> newReplyNotifications = Arrays.asList(createNewReplyNotification()); + private final List> mentionNotifications = Arrays.asList(createMentionNotification()); + + public ListNotificationFiltersTest() { + when(localSone.getId()).thenReturn(LOCAL_ID); + when(localSone.isLocal()).thenReturn(true); + when(localSone.getIdentity()).thenReturn(localIdentity); + when(localIdentity.getId()).thenReturn(LOCAL_ID); + when(localSone.getOptions()).thenReturn(soneOptions); + } + + @Test + public void filterIsOnlyCreatedOnce() { + Injector injector = Guice.createInjector(); + ListNotificationFilters firstFilter = injector.getInstance(ListNotificationFilters.class); + ListNotificationFilters secondFilter = injector.getInstance(ListNotificationFilters.class); + assertThat(firstFilter, sameInstance(secondFilter)); + } + + @Test + public void newSoneNotificationsAreNotRemovedIfNotLoggedIn() { + List notifications = Arrays.asList(createNewSoneNotification()); + List filteredNotifications = listNotificationFilters.filterNotifications(notifications, null); + assertThat(filteredNotifications, contains(notifications.get(0))); + } + + private Notification createNewSoneNotification() { + ListNotification newSoneNotification = mock(ListNotification.class); + when(newSoneNotification.getId()).thenReturn("new-sone-notification"); + return newSoneNotification; + } + + @Test + public void newSoneNotificationsAreRemovedIfLoggedInAndNewSonesShouldNotBeShown() { + List notifications = Arrays.asList(createNewSoneNotification()); + List filteredNotifications = listNotificationFilters.filterNotifications(notifications, localSone); + assertThat(filteredNotifications, emptyIterable()); + } + + @Test + public void newSoneNotificationsAreNotRemovedIfLoggedInAndNewSonesShouldBeShown() { + List notifications = Arrays.asList(createNewSoneNotification()); + when(soneOptions.isShowNewSoneNotifications()).thenReturn(true); + List filteredNotifications = listNotificationFilters.filterNotifications(notifications, localSone); + assertThat(filteredNotifications, contains(notifications.get(0))); + } + + private ListNotification createNewPostNotification() { + ListNotification newSoneNotification = mock(ListNotification.class); + when(newSoneNotification.getElements()).thenReturn(new ArrayList()); + when(newSoneNotification.getId()).thenReturn("new-post-notification"); + return newSoneNotification; + } + + @Test + public void newPostNotificationIsNotShownIfOptionsSetAccordingly() { + List> notifications = Arrays.asList(createNewPostNotification()); + List filteredNotifications = listNotificationFilters.filterNotifications(notifications, localSone); + assertThat(filteredNotifications, hasSize(0)); + } + + private void activateNewPostNotifications() { + when(soneOptions.isShowNewPostNotifications()).thenReturn(true); + } + + private boolean addPostToPostNotification(List> notifications) { + return notifications.get(0).getElements().add(mock(Post.class)); + } + + private void setPostVisibilityPredicate(Predicate value) { + when(postVisibilityFilter.isVisible(any(Sone.class))).thenReturn(value); + } + + @Test + public void newPostNotificationIsNotShownIfNoNewPostsAreVisible() { + activateNewPostNotifications(); + addPostToPostNotification(newPostNotifications); + setPostVisibilityPredicate(Predicates.alwaysFalse()); + List filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, localSone); + assertThat(filteredNotifications, hasSize(0)); + } + + @Test + public void newPostNotificationIsShownIfNewPostsAreVisible() { + activateNewPostNotifications(); + addPostToPostNotification(newPostNotifications); + setPostVisibilityPredicate(Predicates.alwaysTrue()); + List filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, localSone); + assertThat(filteredNotifications, contains((Notification) newPostNotifications.get(0))); + } + + @Test + public void newPostNotificationIsNotShownIfNewPostsAreVisibleButLocalSoneIsNull() { + activateNewPostNotifications(); + addPostToPostNotification(newPostNotifications); + setPostVisibilityPredicate(Predicates.alwaysTrue()); + List filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, null); + assertThat(filteredNotifications, Matchers.emptyIterable()); + } + + @Test + public void newPostNotificationContainsOnlyVisiblePosts() { + activateNewPostNotifications(); + addPostToPostNotification(newPostNotifications); + addPostToPostNotification(newPostNotifications); + setPostVisibilityPredicate(new Predicate() { + @Override + public boolean apply(@Nullable Post post) { + return post.equals(newPostNotifications.get(0).getElements().get(1)); + } + }); + List filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, localSone); + assertThat(filteredNotifications, hasSize(1)); + assertThat(((ListNotification) filteredNotifications.get(0)).getElements().get(0), is(newPostNotifications.get(0).getElements().get(1))); + } + + private ListNotification createNewReplyNotification() { + ListNotification newReplyNotifications = mock(ListNotification.class); + when(newReplyNotifications.getElements()).thenReturn(new ArrayList()); + when(newReplyNotifications.getId()).thenReturn("new-reply-notification"); + return newReplyNotifications; + } + + private void activateNewReplyNotifications() { + when(soneOptions.isShowNewReplyNotifications()).thenReturn(true); + } + + private void addReplyToNewReplyNotification(List> notifications) { + notifications.get(0).getElements().add(mock(PostReply.class)); + } + + private void setReplyVisibilityPredicate(Predicate value) { + when(replyVisibilityFilter.isVisible(any(Sone.class))).thenReturn(value); + } + + @Test + public void newReplyNotificationContainsOnlyVisibleReplies() { + activateNewReplyNotifications(); + addReplyToNewReplyNotification(newReplyNotifications); + addReplyToNewReplyNotification(newReplyNotifications); + setReplyVisibilityPredicate(new Predicate() { + @Override + public boolean apply(@Nullable PostReply postReply) { + return postReply.equals(newReplyNotifications.get(0).getElements().get(1)); + } + }); + List filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone); + assertThat(filteredNotifications, hasSize(1)); + assertThat(((ListNotification) filteredNotifications.get(0)).getElements().get(0), is(newReplyNotifications.get(0).getElements().get(1))); + } + + @Test + public void newReplyNotificationIsNotModifiedIfAllRepliesAreVisible() { + activateNewReplyNotifications(); + addReplyToNewReplyNotification(newReplyNotifications); + addReplyToNewReplyNotification(newReplyNotifications); + setReplyVisibilityPredicate(Predicates.alwaysTrue()); + List filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone); + assertThat(filteredNotifications, hasSize(1)); + assertThat(filteredNotifications.get(0), is((Notification) newReplyNotifications.get(0))); + assertThat(((ListNotification) filteredNotifications.get(0)).getElements(), hasSize(2)); + } + + @Test + public void newReplyNotificationIsNotShownIfNoRepliesAreVisible() { + activateNewReplyNotifications(); + addReplyToNewReplyNotification(newReplyNotifications); + addReplyToNewReplyNotification(newReplyNotifications); + setReplyVisibilityPredicate(Predicates.alwaysFalse()); + List filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone); + assertThat(filteredNotifications, hasSize(0)); + } + + @Test + public void newReplyNotificationIsNotShownIfDeactivatedInOptions() { + addReplyToNewReplyNotification(newReplyNotifications); + addReplyToNewReplyNotification(newReplyNotifications); + setReplyVisibilityPredicate(Predicates.alwaysTrue()); + List filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone); + assertThat(filteredNotifications, hasSize(0)); + } + + @Test + public void newReplyNotificationIsNotShownIfCurrentSoneIsNull() { + addReplyToNewReplyNotification(newReplyNotifications); + addReplyToNewReplyNotification(newReplyNotifications); + setReplyVisibilityPredicate(Predicates.alwaysTrue()); + List filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, null); + assertThat(filteredNotifications, hasSize(0)); + } + + private ListNotification createMentionNotification() { + ListNotification newSoneNotification = mock(ListNotification.class); + when(newSoneNotification.getElements()).thenReturn(new ArrayList()); + when(newSoneNotification.getId()).thenReturn("mention-notification"); + return newSoneNotification; + } + + @Test + public void mentionNotificationContainsOnlyVisiblePosts() { + addPostToPostNotification(mentionNotifications); + addPostToPostNotification(mentionNotifications); + setPostVisibilityPredicate(new Predicate() { + @Override + public boolean apply(@Nullable Post post) { + return post.equals(mentionNotifications.get(0).getElements().get(1)); + } + }); + List filteredNotifications = listNotificationFilters.filterNotifications(mentionNotifications, localSone); + assertThat(filteredNotifications, hasSize(1)); + assertThat(((ListNotification) filteredNotifications.get(0)).getElements().get(0), is(mentionNotifications.get(0).getElements().get(1))); + } + + @Test + public void mentionNotificationIsNotShownIfNoPostsAreVisible() { + addPostToPostNotification(mentionNotifications); + addPostToPostNotification(mentionNotifications); + setPostVisibilityPredicate(Predicates.alwaysFalse()); + List filteredNotifications = listNotificationFilters.filterNotifications(mentionNotifications, localSone); + assertThat(filteredNotifications, hasSize(0)); + } + + @Test + public void unfilterableNotificationIsNotFiltered() { + Notification notification = mock(Notification.class); + when(notification.getId()).thenReturn("random-notification"); + List notifications = Arrays.asList(notification); + List filteredNotifications = listNotificationFilters.filterNotifications(notifications, null); + assertThat(filteredNotifications, contains(notification)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java b/src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java new file mode 100644 index 0000000..d49e59b --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java @@ -0,0 +1,203 @@ +package net.pterodactylus.sone.notify; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; +import net.pterodactylus.sone.freenet.wot.Trust; + +import com.google.common.base.Optional; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.Test; + +/** + * Unit test for {@link PostVisibilityFilterTest}. + * + * @author David ‘Bombe’ Roden + */ +public class PostVisibilityFilterTest { + + private static final String LOCAL_ID = "local-id"; + private static final String REMOTE_ID = "remote-id"; + + private final PostVisibilityFilter postVisibilityFilter = new PostVisibilityFilter(); + + private final Sone localSone = mock(Sone.class); + private final OwnIdentity localIdentity = mock(OwnIdentity.class); + private final Post post = mock(Post.class); + private final Sone remoteSone = mock(Sone.class); + private final Identity remoteIdentity = mock(Identity.class); + + public PostVisibilityFilterTest() { + when(localSone.getId()).thenReturn(LOCAL_ID); + when(localSone.isLocal()).thenReturn(true); + when(localSone.getIdentity()).thenReturn(localIdentity); + when(localIdentity.getId()).thenReturn(LOCAL_ID); + when(remoteSone.getId()).thenReturn(REMOTE_ID); + when(remoteSone.getIdentity()).thenReturn(remoteIdentity); + when(remoteIdentity.getId()).thenReturn(REMOTE_ID); + when(post.getRecipientId()).thenReturn(Optional.absent()); + } + + @Test + public void postVisibilityFilterIsOnlyCreatedOnce() { + Injector injector = Guice.createInjector(); + PostVisibilityFilter firstFilter = injector.getInstance(PostVisibilityFilter.class); + PostVisibilityFilter secondFilter = injector.getInstance(PostVisibilityFilter.class); + assertThat(firstFilter, sameInstance(secondFilter)); + } + + @Test + public void postIsNotVisibleIfItIsNotLoaded() { + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false)); + } + + private static void makePostLoaded(Post post) { + when(post.isLoaded()).thenReturn(true); + } + + @Test + public void loadedPostIsVisibleWithoutSone() { + makePostLoaded(post); + assertThat(postVisibilityFilter.isPostVisible(null, post), is(true)); + } + + private void makePostComeFromTheFuture() { + when(post.getTime()).thenReturn(System.currentTimeMillis() + 1000); + } + + @Test + public void loadedPostFromTheFutureIsNotVisible() { + makePostLoaded(post); + makePostComeFromTheFuture(); + assertThat(postVisibilityFilter.isPostVisible(null, post), is(false)); + } + + private void makePostFromRemoteSone() { + when(post.getSone()).thenReturn(remoteSone); + } + + private void giveRemoteIdentityNegativeExplicitTrust() { + when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(-1, null, null)); + } + + @Test + public void loadedPostFromExplicitelyNotTrustedSoneIsNotVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + giveRemoteIdentityNegativeExplicitTrust(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false)); + } + + private void giveRemoteIdentityNegativeImplicitTrust() { + when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(null, -1, null)); + } + + @Test + public void loadedPostFromImplicitelyUntrustedSoneIsNotVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + giveRemoteIdentityNegativeImplicitTrust(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false)); + } + + private void makeLocalSoneFollowRemoteSone() { + when(localSone.hasFriend(REMOTE_ID)).thenReturn(true); + } + + private void giveRemoteIdentityPositiveExplicitTrustButNegativeImplicitTrust() { + when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(1, -1, null)); + } + + @Test + public void loadedPostFromExplicitelyTrustedButImplicitelyUntrustedSoneIsVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + makeLocalSoneFollowRemoteSone(); + giveRemoteIdentityPositiveExplicitTrustButNegativeImplicitTrust(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true)); + } + + private void giveTheRemoteIdentityPositiveImplicitTrust() { + when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(null, 1, null)); + } + + @Test + public void loadedPostFromImplicitelyTrustedSoneIsVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + makeLocalSoneFollowRemoteSone(); + giveTheRemoteIdentityPositiveImplicitTrust(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true)); + } + + private void giveTheRemoteIdentityUnknownTrust() { + when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(null, null, null)); + } + + @Test + public void loadedPostFromSoneWithUnknownTrustIsVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + makeLocalSoneFollowRemoteSone(); + giveTheRemoteIdentityUnknownTrust(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true)); + } + + @Test + public void loadedPostFromUnfollowedRemoteSoneThatIsNotDirectedAtLocalSoneIsNotVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false)); + } + + private void makePostFromLocalSone() { + makePostLoaded(post); + when(post.getSone()).thenReturn(localSone); + } + + @Test + public void loadedPostFromLocalSoneIsVisible() { + makePostFromLocalSone(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true)); + } + + @Test + public void loadedPostFromFollowedRemoteSoneThatIsNotDirectedAtLocalSoneIsVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + makeLocalSoneFollowRemoteSone(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true)); + } + + private void makePostDirectedAtLocalId() { + when(post.getRecipientId()).thenReturn(Optional.of(LOCAL_ID)); + } + + @Test + public void loadedPostFromRemoteSoneThatIsDirectedAtLocalSoneIsVisible() { + makePostLoaded(post); + makePostFromRemoteSone(); + makePostDirectedAtLocalId(); + assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true)); + } + + @Test + public void predicateWillCorrectlyRecognizeVisiblePost() { + makePostFromLocalSone(); + assertThat(postVisibilityFilter.isVisible(null).apply(post), is(true)); + } + + @Test + public void predicateWillCorrectlyRecognizeNotVisiblePost() { + assertThat(postVisibilityFilter.isVisible(null).apply(post), is(false)); + } + +} diff --git a/src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java b/src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java new file mode 100644 index 0000000..7187847 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java @@ -0,0 +1,106 @@ +package net.pterodactylus.sone.notify; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; + +import com.google.common.base.Optional; +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.Test; + +/** + * Unit test for {@link ReplyVisibilityFilterTest}. + * + * @author David ‘Bombe’ Roden + */ +public class ReplyVisibilityFilterTest { + + private static final String LOCAL_ID = "local-id"; + + private final PostVisibilityFilter postVisibilityFilter = mock(PostVisibilityFilter.class); + private final ReplyVisibilityFilter replyVisibilityFilter = new ReplyVisibilityFilter(postVisibilityFilter); + + private final Sone localSone = mock(Sone.class); + private final OwnIdentity localIdentity = mock(OwnIdentity.class); + private final Post post = mock(Post.class); + private final PostReply postReply = mock(PostReply.class); + + public ReplyVisibilityFilterTest() { + when(localSone.getId()).thenReturn(LOCAL_ID); + when(localSone.isLocal()).thenReturn(true); + when(localSone.getIdentity()).thenReturn(localIdentity); + when(post.getRecipientId()).thenReturn(Optional.absent()); + } + + @Test + public void replyVisibilityFilterIsOnlyCreatedOnce() { + Injector injector = Guice.createInjector(); + ReplyVisibilityFilter firstFilter = injector.getInstance(ReplyVisibilityFilter.class); + ReplyVisibilityFilter secondFilter = injector.getInstance(ReplyVisibilityFilter.class); + assertThat(firstFilter, sameInstance(secondFilter)); + } + + private void makePostPresent() { + when(postReply.getPost()).thenReturn(Optional.of(post)); + } + + @Test + public void replyIsNotVisibleIfPostIsNotVisible() { + makePostPresent(); + assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(false)); + } + + private void makePostAbsent() { + when(postReply.getPost()).thenReturn(Optional.absent()); + } + + @Test + public void replyIsNotVisibleIfPostIsNotPresent() { + makePostAbsent(); + assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(false)); + } + + private void makePostPresentAndVisible() { + makePostPresent(); + when(postVisibilityFilter.isPostVisible(localSone, post)).thenReturn(true); + } + + private void makeReplyComeFromFuture() { + when(postReply.getTime()).thenReturn(System.currentTimeMillis() + 1000); + } + + @Test + public void replyIsNotVisibleIfItIsFromTheFuture() { + makePostPresentAndVisible(); + makeReplyComeFromFuture(); + assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(false)); + } + + @Test + public void replyIsVisibleIfItIsNotFromTheFuture() { + makePostPresentAndVisible(); + assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(true)); + } + + @Test + public void predicateCorrectlyRecognizesVisibleReply() { + makePostPresentAndVisible(); + assertThat(replyVisibilityFilter.isVisible(localSone).apply(postReply), is(true)); + } + + @Test + public void predicateCorrectlyRecognizesNotVisibleReply() { + makePostPresentAndVisible(); + makeReplyComeFromFuture(); + assertThat(replyVisibilityFilter.isVisible(localSone).apply(postReply), is(false)); + } + +} -- 2.7.4