Add processor for updated Sones
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 3 Mar 2018 16:31:50 +0000 (17:31 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 3 Mar 2018 16:31:50 +0000 (17:31 +0100)
src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt [new file with mode: 0644]

diff --git a/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt b/src/main/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessor.kt
new file mode 100644 (file)
index 0000000..17948c5
--- /dev/null
@@ -0,0 +1,68 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.EventBus
+import com.google.inject.ImplementedBy
+import net.pterodactylus.sone.core.event.NewPostFoundEvent
+import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent
+import net.pterodactylus.sone.core.event.PostRemovedEvent
+import net.pterodactylus.sone.core.event.PostReplyRemovedEvent
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.data.Sone.SoneStatus
+import net.pterodactylus.sone.database.Database
+import net.pterodactylus.sone.utils.ifFalse
+import net.pterodactylus.util.logging.Logging
+import javax.inject.Inject
+
+/**
+ * An `UpdatedSoneProcessor` is called to process a [Sone] after it has been
+ * downloaded from Freenet.
+ */
+@ImplementedBy(DefaultUpdateSoneProcessor::class)
+interface UpdatedSoneProcessor {
+
+       fun updateSone(sone: Sone)
+
+}
+
+abstract class BasicUpdateSoneProcessor(private val database: Database, private val eventBus: EventBus) :
+               UpdatedSoneProcessor {
+
+       private val logger = Logging.getLogger(UpdatedSoneProcessor::javaClass.name)!!
+
+       override fun updateSone(sone: Sone) {
+               val storedSone = database.getSone(sone.id) ?: return
+               if (!soneCanBeUpdated(storedSone, sone)) {
+                       logger.fine { "Downloaded Sone $sone can not update stored Sone $storedSone." }
+                       return
+               }
+               collectEventsForChanges(storedSone, sone)
+                               .also { database.storeSone(sone) }
+                               .forEach(eventBus::post)
+               sone.options = storedSone.options
+               sone.isKnown = storedSone.isKnown
+               sone.status = if (sone.time != 0L) SoneStatus.idle else SoneStatus.unknown
+       }
+
+       protected abstract fun soneCanBeUpdated(storedSone: Sone, newSone: Sone): Boolean
+
+       private val Sone.followingTime get() = database.getFollowingTime(id) ?: 0
+
+       private fun collectEventsForChanges(oldSone: Sone, newSone: Sone): List<Any> =
+                       SoneChangeCollector(oldSone)
+                                       .onNewPost { post -> if (post.time <= newSone.followingTime) post.isKnown = true }
+                                       .newPostEvent { post -> post.isKnown.ifFalse { NewPostFoundEvent(post) } }
+                                       .removedPostEvent { PostRemovedEvent(it) }
+                                       .onNewPostReply { postReply -> if (postReply.time <= newSone.followingTime) postReply.isKnown = true }
+                                       .newPostReplyEvent { postReply -> postReply.isKnown.ifFalse { NewPostReplyFoundEvent(postReply) } }
+                                       .onRemovedPostReply { PostReplyRemovedEvent(it) }
+                                       .detectChanges(newSone)
+
+}
+
+class DefaultUpdateSoneProcessor @Inject constructor(database: Database, eventBus: EventBus) :
+               BasicUpdateSoneProcessor(database, eventBus) {
+
+       override fun soneCanBeUpdated(storedSone: Sone, newSone: Sone) =
+                       newSone.time > storedSone.time
+
+}
diff --git a/src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/core/UpdatedSoneProcessorTest.kt
new file mode 100644 (file)
index 0000000..e7afc74
--- /dev/null
@@ -0,0 +1,151 @@
+package net.pterodactylus.sone.core
+
+import com.google.common.eventbus.EventBus
+import net.pterodactylus.sone.core.event.NewPostFoundEvent
+import net.pterodactylus.sone.core.event.NewPostReplyFoundEvent
+import net.pterodactylus.sone.core.event.PostRemovedEvent
+import net.pterodactylus.sone.core.event.PostReplyRemovedEvent
+import net.pterodactylus.sone.data.Post
+import net.pterodactylus.sone.data.PostReply
+import net.pterodactylus.sone.data.Sone
+import net.pterodactylus.sone.database.Database
+import net.pterodactylus.sone.test.argumentCaptor
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.isProvidedByMock
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.test.whenever
+import net.pterodactylus.sone.web.baseInjector
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.containsInAnyOrder
+import org.hamcrest.Matchers.not
+import org.hamcrest.Matchers.notNullValue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+/**
+ * Unit test for all [UpdatedSoneProcessor] implementations.
+ */
+class UpdatedSoneProcessorTest {
+
+       private val database = mock<Database>()
+       private val eventBus = mock<EventBus>()
+       private val updatedSoneProcessor = DefaultUpdateSoneProcessor(database, eventBus)
+       private val storedSone = mock<Sone>()
+       private val newSone = mock<Sone>()
+       private val posts = listOf(mock<Post>(), mock(), mock())
+       private val postReplies = listOf(mock<PostReply>(), mock(), mock())
+
+       private val events = argumentCaptor<Any>()
+
+       @Before
+       fun setupPostsAndReplies() {
+               posts.forEachIndexed { index, post -> whenever(post.time).thenReturn((index + 1) * 1000L + 100) }
+               postReplies.forEachIndexed { index, postReply -> whenever(postReply.time).thenReturn((index + 1) * 1000L + 200) }
+       }
+
+       @Before
+       fun setupSones() {
+               whenever(storedSone.time).thenReturn(1000L)
+               whenever(storedSone.posts).thenReturn(posts.slice(0..1))
+               whenever(storedSone.replies).thenReturn(postReplies.slice(0..1).toSet())
+               whenever(newSone.id).thenReturn("sone")
+               whenever(newSone.time).thenReturn(2000L)
+               whenever(newSone.posts).thenReturn(posts.slice(1..2))
+               whenever(newSone.replies).thenReturn(postReplies.slice(1..2).toSet())
+       }
+
+       @Before
+       fun setupDatabase() {
+               whenever(database.getSone("sone")).thenReturn(storedSone)
+               whenever(database.getFollowingTime("sone")).thenReturn(500L)
+       }
+
+       @Test
+       fun `updated Sone processor emits no event if no stored sone exists`() {
+               whenever(database.getSone("sone")).thenReturn(null)
+               updatedSoneProcessor.updateSone(newSone)
+               verify(eventBus, never()).post(any())
+       }
+
+       @Test
+       fun `updated Sone processor emits no event if new Sone is older than stored Sone`() {
+               whenever(newSone.time).thenReturn(500L)
+               updatedSoneProcessor.updateSone(newSone)
+               verify(eventBus, never()).post(any())
+       }
+
+       @Test
+       fun `updated Sone processor emits correct events when new Sone is newer`() {
+               updatedSoneProcessor.updateSone(newSone)
+               verify(eventBus, times(4)).post(events.capture())
+               assertThat(events.allValues, containsInAnyOrder(
+                               NewPostFoundEvent(posts[2]),
+                               PostRemovedEvent(posts[0]),
+                               NewPostReplyFoundEvent(postReplies[2]),
+                               PostReplyRemovedEvent(postReplies[0])
+               ))
+       }
+
+       @Test
+       fun `updated Sone processor does not mark new post as known if sone was not followed after post`() {
+               updatedSoneProcessor.updateSone(newSone)
+               verify(posts[2], never()).isKnown = true
+       }
+
+       @Test
+       fun `updated Sone processor does not mark new posts as known if Sone is not followed`() {
+               whenever(database.getFollowingTime("sone")).thenReturn(null)
+               updatedSoneProcessor.updateSone(newSone)
+               posts.forEach { verify(it, never()).isKnown = true }
+       }
+
+       @Test
+       fun `updated Sone processor marks new post as known if sone was followed after post`() {
+               whenever(database.getFollowingTime("sone")).thenReturn(3500L)
+               updatedSoneProcessor.updateSone(newSone)
+               verify(posts[2]).isKnown = true
+       }
+
+       @Test
+       fun `updated Sone processor does not emit event for post if it is already known`() {
+               whenever(posts[2].isKnown).thenReturn(true)
+               updatedSoneProcessor.updateSone(newSone)
+               verify(eventBus, atLeastOnce()).post(events.capture())
+               assertThat(events.allValues, not(contains<Any>(NewPostFoundEvent(posts[2]))))
+       }
+
+       @Test
+       fun `updated Sone processor does not mark new reply as known if sone was not followed after reply`() {
+               updatedSoneProcessor.updateSone(newSone)
+               verify(postReplies[2], never()).isKnown = true
+       }
+
+       @Test
+       fun `updated Sone processor marks new reply as known if sone was followed after reply`() {
+               whenever(database.getFollowingTime("sone")).thenReturn(3500L)
+               updatedSoneProcessor.updateSone(newSone)
+               verify(postReplies[2]).isKnown = true
+       }
+
+       @Test
+       fun `updated Sone processor does not emit event for reply if it is already known`() {
+               whenever(postReplies[2].isKnown).thenReturn(true)
+               updatedSoneProcessor.updateSone(newSone)
+               verify(eventBus, atLeastOnce()).post(events.capture())
+               assertThat(events.allValues, not(contains<Any>(NewPostReplyFoundEvent(postReplies[2]))))
+       }
+
+       @Test
+       fun `default updated Sone processor can be created by dependency injection`() {
+               assertThat(baseInjector.createChildInjector(
+                               Database::class.isProvidedByMock()
+               ).getInstance<UpdatedSoneProcessor>(), notNullValue())
+       }
+
+}