🎨 Store shells instead of full posts in in-memory database
[Sone.git] / src / test / kotlin / net / pterodactylus / sone / database / memory / MemoryDatabaseTest.kt
1 /*
2  * Sone - MemoryDatabaseTest.kt - Copyright Â© 2013–2020 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.database.memory
19
20 import com.google.common.base.*
21 import com.google.common.base.Optional.*
22 import net.pterodactylus.sone.data.*
23 import net.pterodactylus.sone.data.impl.*
24 import net.pterodactylus.sone.test.*
25 import net.pterodactylus.sone.test.Matchers.*
26 import net.pterodactylus.util.config.*
27 import org.hamcrest.MatcherAssert.*
28 import org.hamcrest.Matchers
29 import org.hamcrest.Matchers.*
30 import org.mockito.ArgumentMatchers.anyString
31 import org.mockito.Mockito.*
32 import org.mockito.invocation.*
33 import java.util.Arrays.*
34 import java.util.UUID.*
35 import kotlin.test.*
36
37 /**
38  * Tests for [MemoryDatabase].
39  */
40 class MemoryDatabaseTest {
41
42         private val configuration = deepMock<Configuration>()
43         private val memoryDatabase = MemoryDatabase(configuration)
44         private val sone = mock<Sone>()
45
46         @BeforeTest
47         fun setupSone() {
48                 whenever(sone.id).thenReturn(SONE_ID)
49         }
50
51         @Test
52         fun `stored sone is made available`() {
53                 storeSone()
54                 assertThat(memoryDatabase.getPost("post1"), isPost("post1", 1000L, "post1", null))
55                 assertThat(memoryDatabase.getPost("post2"), isPost("post2", 2000L, "post2", RECIPIENT_ID))
56                 assertThat(memoryDatabase.getPost("post3"), nullValue())
57                 assertThat(memoryDatabase.getPostReply("reply1"), isPostReply("reply1", "post1", 3000L, "reply1"))
58                 assertThat(memoryDatabase.getPostReply("reply2"), isPostReply("reply2", "post2", 4000L, "reply2"))
59                 assertThat(memoryDatabase.getPostReply("reply3"), isPostReply("reply3", "post1", 5000L, "reply3"))
60                 assertThat(memoryDatabase.getPostReply("reply4"), nullValue())
61                 assertThat(memoryDatabase.getAlbum("album1"), isAlbum("album1", "root", "album1", "album-description1"))
62                 assertThat(memoryDatabase.getAlbum("album2"), isAlbum("album2", "root", "album2", "album-description2"))
63                 assertThat(memoryDatabase.getAlbum("album3"), isAlbum("album3", "album1", "album3", "album-description3"))
64                 assertThat(memoryDatabase.getAlbum("album4"), nullValue())
65                 assertThat(memoryDatabase.getImage("image1"), isImage("image1", 1000L, "KSK@image1", "image1", "image-description1", 16, 9))
66                 assertThat(memoryDatabase.getImage("image2"), isImage("image2", 2000L, "KSK@image2", "image2", "image-description2", 32, 18))
67                 assertThat(memoryDatabase.getImage("image3"), isImage("image3", 3000L, "KSK@image3", "image3", "image-description3", 48, 27))
68                 assertThat(memoryDatabase.getImage("image4"), nullValue())
69         }
70
71         private fun storeSone() {
72                 val firstPost = TestPostBuilder().withId("post1")
73                                 .from(SONE_ID)
74                                 .withTime(1000L)
75                                 .withText("post1")
76                                 .build()
77                 val secondPost = TestPostBuilder().withId("post2")
78                                 .from(SONE_ID)
79                                 .withTime(2000L)
80                                 .withText("post2")
81                                 .to(RECIPIENT_ID)
82                                 .build()
83                 val posts = asList(firstPost, secondPost)
84                 whenever(sone.posts).thenReturn(posts)
85                 val firstPostFirstReply = TestPostReplyBuilder().withId("reply1")
86                                 .from(SONE_ID)
87                                 .to(firstPost.id)
88                                 .withTime(3000L)
89                                 .withText("reply1")
90                                 .build()
91                 val firstPostSecondReply = TestPostReplyBuilder().withId("reply3")
92                                 .from(RECIPIENT_ID)
93                                 .to(firstPost.id)
94                                 .withTime(5000L)
95                                 .withText("reply3")
96                                 .build()
97                 val secondPostReply = TestPostReplyBuilder().withId("reply2")
98                                 .from(SONE_ID)
99                                 .to(secondPost.id)
100                                 .withTime(4000L)
101                                 .withText("reply2")
102                                 .build()
103                 val postReplies = setOf(firstPostFirstReply, firstPostSecondReply, secondPostReply)
104                 whenever(sone.replies).thenReturn(postReplies)
105                 val firstAlbum = TestAlbumBuilder().withId("album1")
106                                 .by(sone)
107                                 .build()
108                                 .modify()
109                                 .setTitle("album1")
110                                 .setDescription("album-description1")
111                                 .update()
112                 val secondAlbum = TestAlbumBuilder().withId("album2")
113                                 .by(sone)
114                                 .build()
115                                 .modify()
116                                 .setTitle("album2")
117                                 .setDescription("album-description2")
118                                 .update()
119                 val thirdAlbum = TestAlbumBuilder().withId("album3")
120                                 .by(sone)
121                                 .build()
122                                 .modify()
123                                 .setTitle("album3")
124                                 .setDescription("album-description3")
125                                 .update()
126                 firstAlbum.addAlbum(thirdAlbum)
127                 val rootAlbum = AlbumImpl(sone, "root").also {
128                         it.addAlbum(firstAlbum)
129                         it.addAlbum(secondAlbum)
130                 }
131                 whenever(sone.rootAlbum).thenReturn(rootAlbum)
132                 val firstImage = TestImageBuilder().withId("image1")
133                                 .build()
134                                 .modify()
135                                 .setSone(sone)
136                                 .setCreationTime(1000L)
137                                 .setKey("KSK@image1")
138                                 .setTitle("image1")
139                                 .setDescription("image-description1")
140                                 .setWidth(16)
141                                 .setHeight(9)
142                                 .update()
143                 val secondImage = TestImageBuilder().withId("image2")
144                                 .build()
145                                 .modify()
146                                 .setSone(sone)
147                                 .setCreationTime(2000L)
148                                 .setKey("KSK@image2")
149                                 .setTitle("image2")
150                                 .setDescription("image-description2")
151                                 .setWidth(32)
152                                 .setHeight(18)
153                                 .update()
154                 val thirdImage = TestImageBuilder().withId("image3")
155                                 .build()
156                                 .modify()
157                                 .setSone(sone)
158                                 .setCreationTime(3000L)
159                                 .setKey("KSK@image3")
160                                 .setTitle("image3")
161                                 .setDescription("image-description3")
162                                 .setWidth(48)
163                                 .setHeight(27)
164                                 .update()
165                 firstAlbum.addImage(firstImage)
166                 firstAlbum.addImage(thirdImage)
167                 secondAlbum.addImage(secondImage)
168                 memoryDatabase.storeSone(sone)
169         }
170
171         @Test
172         fun `stored and removed sone is not available`() {
173                 storeSone()
174                 memoryDatabase.removeSone(sone)
175                 assertThat(memoryDatabase.sones, empty())
176         }
177
178         @Test
179         fun `post recipients are detected correctly`() {
180                 val postWithRecipient = createPost(id = "p1", recipient = createRemoteSone(RECIPIENT_ID))
181                 memoryDatabase.storePost(postWithRecipient)
182                 val postWithoutRecipient = createPost(id = "p2", recipient = null)
183                 memoryDatabase.storePost(postWithoutRecipient)
184                 assertThat(memoryDatabase.getDirectedPosts(RECIPIENT_ID), contains(isPost(isRecipientId = equalTo(RECIPIENT_ID))))
185         }
186
187         @Test
188         fun `post replies are managed correctly`() {
189                 val firstPost = createPost()
190                 val firstPostFirstReply = createPostReply(id = "p1r1", post = firstPost, time = 1000L)
191                 val secondPost = createPost()
192                 val secondPostFirstReply = createPostReply(id = "p2r1", post = secondPost, time = 1000L)
193                 val secondPostSecondReply = createPostReply(id = "p2r2", post = secondPost, time = 2000L)
194                 memoryDatabase.storePost(firstPost)
195                 memoryDatabase.storePost(secondPost)
196                 memoryDatabase.storePostReply(firstPostFirstReply)
197                 memoryDatabase.storePostReply(secondPostFirstReply)
198                 memoryDatabase.storePostReply(secondPostSecondReply)
199                 assertThat(memoryDatabase.getReplies(firstPost.id).map(PostReply::id), Matchers.contains("p1r1"))
200                 assertThat(memoryDatabase.getReplies(secondPost.id).map(PostReply::id), contains("p2r1", "p2r2"))
201         }
202
203         @Test
204         fun `test basic album functionality`() {
205                 val newAlbum = AlbumImpl(mock())
206                 assertThat(memoryDatabase.getAlbum(newAlbum.id), nullValue())
207                 memoryDatabase.storeAlbum(newAlbum)
208                 assertThat(memoryDatabase.getAlbum(newAlbum.id), equalTo<Album>(newAlbum))
209                 memoryDatabase.removeAlbum(newAlbum)
210                 assertThat(memoryDatabase.getAlbum(newAlbum.id), nullValue())
211         }
212
213         private fun initializeFriends() {
214                 whenever(configuration.getStringValue("Sone/$SONE_ID/Friends/0/ID")).thenReturn(TestValue.from("Friend1"))
215                 whenever(configuration.getStringValue("Sone/$SONE_ID/Friends/1/ID")).thenReturn(TestValue.from("Friend2"))
216                 whenever(configuration.getStringValue("Sone/$SONE_ID/Friends/2/ID")).thenReturn(TestValue.from(null))
217         }
218
219         @Test
220         fun `friends are returned correctly`() {
221                 initializeFriends()
222                 whenever(sone.isLocal).thenReturn(true)
223                 val friends = memoryDatabase.getFriends(sone)
224                 assertThat(friends, containsInAnyOrder("Friend1", "Friend2"))
225         }
226
227         @Test
228         fun `friends are only loaded once from configuration`() {
229                 initializeFriends()
230                 whenever(sone.isLocal).thenReturn(true)
231                 memoryDatabase.getFriends(sone)
232                 memoryDatabase.getFriends(sone)
233                 verify(configuration, times(1)).getStringValue("Sone/$SONE_ID/Friends/0/ID")
234         }
235
236         @Test
237         fun `friends are only returned for local sones`() {
238                 val friends = memoryDatabase.getFriends(sone)
239                 assertThat(friends, emptyIterable<Any>())
240                 verify(configuration, never()).getStringValue("Sone/$SONE_ID/Friends/0/ID")
241         }
242
243         @Test
244         fun `checking for a friend returns true`() {
245                 initializeFriends()
246                 whenever(sone.isLocal).thenReturn(true)
247                 assertThat(memoryDatabase.isFriend(sone, "Friend1"), equalTo(true))
248         }
249
250         @Test
251         fun `checking for a friend that is not a friend returns false`() {
252                 initializeFriends()
253                 whenever(sone.isLocal).thenReturn(true)
254                 assertThat(memoryDatabase.isFriend(sone, "FriendX"), equalTo(false))
255         }
256
257         @Test
258         fun `checking for a friend of remote sone returns false`() {
259                 initializeFriends()
260                 assertThat(memoryDatabase.isFriend(sone, "Friend1"), equalTo(false))
261         }
262
263         private fun prepareConfigurationValues(): Map<String, Value<*>> =
264                         mutableMapOf<String, Value<*>>().also { configurationValues ->
265                                 whenever(configuration.getStringValue(anyString())).thenAnswer(createAndCacheValue(configurationValues))
266                                 whenever(configuration.getLongValue(anyString())).thenAnswer(createAndCacheValue(configurationValues))
267                         }
268
269         private fun createAndCacheValue(configurationValues: MutableMap<String, Value<*>>) =
270                         { invocation: InvocationOnMock ->
271                                 configurationValues[invocation[0]]
272                                                 ?: TestValue.from(null).also {
273                                                         configurationValues[invocation[0]] = it
274                                                 }
275                         }
276
277         @Test
278         fun `friend is added correctly to local sone`() {
279                 val configurationValues = prepareConfigurationValues()
280                 whenever(sone.isLocal).thenReturn(true)
281                 memoryDatabase.addFriend(sone, "Friend1")
282                 assertThat(configurationValues["Sone/$SONE_ID/Friends/0/ID"], equalTo<Value<*>>(TestValue.from("Friend1")))
283                 assertThat(configurationValues["Sone/$SONE_ID/Friends/1/ID"], equalTo<Value<*>>(TestValue.from(null)))
284         }
285
286         @Test
287         fun `friend is not added to remote sone`() {
288                 memoryDatabase.addFriend(sone, "Friend1")
289                 verify(configuration, never()).getStringValue(anyString())
290         }
291
292         @Test
293         fun `friend is removed correctly from local sone`() {
294                 val configurationValues = prepareConfigurationValues()
295                 whenever(sone.isLocal).thenReturn(true)
296                 memoryDatabase.addFriend(sone, "Friend1")
297                 memoryDatabase.removeFriend(sone, "Friend1")
298                 assertThat(configurationValues["Sone/$SONE_ID/Friends/0/ID"], equalTo<Value<*>>(TestValue.from(null)))
299                 assertThat(configurationValues["Sone/$SONE_ID/Friends/1/ID"], equalTo<Value<*>>(TestValue.from(null)))
300         }
301
302         @Test
303         fun `configuration is not written when a non-friend is removed`() {
304                 prepareConfigurationValues()
305                 whenever(sone.isLocal).thenReturn(true)
306                 memoryDatabase.removeFriend(sone, "Friend1")
307                 verify(configuration).getStringValue(anyString())
308         }
309
310         @Test
311         fun `sone following time is returned correctly`() {
312                 prepareConfigurationValues()
313                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "sone"
314                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
315                 assertThat(memoryDatabase.getFollowingTime("sone"), equalTo(1000L))
316         }
317
318         @Test
319         fun `null is returned when sone is not followed`() {
320                 prepareConfigurationValues()
321                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "otherSone"
322                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
323                 assertThat(memoryDatabase.getFollowingTime("sone"), nullValue())
324         }
325
326         @Test
327         fun `time is stored in configuration when a sone is followed`() {
328                 prepareConfigurationValues()
329                 whenever(sone.isLocal).thenReturn(true)
330                 memoryDatabase.addFriend(sone, "Friend")
331                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, equalTo("Friend"))
332                 assertThat(System.currentTimeMillis() - configuration.getLongValue("SoneFollowingTimes/0/Time").value, lessThan(1000L))
333                 assertThat(configuration.getStringValue("SoneFollowingTimes/1/Sone").value, nullValue())
334         }
335
336         @Test
337         fun `existing time is not overwritten when a sone is followed`() {
338                 prepareConfigurationValues()
339                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "Friend"
340                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
341                 whenever(sone.isLocal).thenReturn(true)
342                 memoryDatabase.addFriend(sone, "Friend")
343                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, equalTo("Friend"))
344                 assertThat(configuration.getLongValue("SoneFollowingTimes/0/Time").value, equalTo(1000L))
345                 assertThat(configuration.getStringValue("SoneFollowingTimes/1/Sone").value, nullValue())
346         }
347
348         @Test
349         fun `unfollowing a sone removes the following time`() {
350                 prepareConfigurationValues()
351                 configuration.getStringValue("Sone/sone/Friends/0/ID").value = "Friend"
352                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "Friend"
353                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
354                 whenever(sone.isLocal).thenReturn(true)
355                 memoryDatabase.removeFriend(sone, "Friend")
356                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, nullValue())
357         }
358
359         @Test
360         fun `unfollowing a sone does not remove the following time if another local sone follows it`() {
361                 prepareConfigurationValues()
362                 configuration.getStringValue("Sone/sone/Friends/0/ID").value = "Friend"
363                 configuration.getStringValue("Sone/other-sone/Friends/0/ID").value = "Friend"
364                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "Friend"
365                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
366                 val otherSone = mock<Sone>()
367                 whenever(otherSone.isLocal).thenReturn(true)
368                 whenever(otherSone.id).thenReturn("other-sone")
369                 memoryDatabase.getFriends(otherSone)
370                 whenever(sone.isLocal).thenReturn(true)
371                 memoryDatabase.removeFriend(sone, "Friend")
372                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, equalTo("Friend"))
373                 assertThat(configuration.getLongValue("SoneFollowingTimes/0/Time").value, equalTo(1000L))
374         }
375
376         @Test
377         fun `marking a post as known saves configuration`() {
378                 prepareConfigurationValues()
379                 val post = mock<Post>()
380                 whenever(post.id).thenReturn("post-id")
381                 memoryDatabase.setPostKnown(post, true)
382                 assertThat(configuration.getStringValue("KnownPosts/0/ID").value, equalTo("post-id"))
383                 assertThat(configuration.getStringValue("KnownPosts/1/ID").value, equalTo<Any>(null))
384         }
385
386         @Test
387         fun `marking a post reply as known saves configuration`() {
388                 prepareConfigurationValues()
389                 val postReply = mock<PostReply>()
390                 whenever(postReply.id).thenReturn("post-reply-id")
391                 memoryDatabase.setPostReplyKnown(postReply)
392                 assertThat(configuration.getStringValue("KnownReplies/0/ID").value, equalTo("post-reply-id"))
393                 assertThat(configuration.getStringValue("KnownReplies/1/ID").value, equalTo<Any>(null))
394         }
395
396         @Test
397         @Dirty("the rate limiter should be mocked")
398         fun `saving the database twice in a row only saves it once`() {
399                 memoryDatabase.save()
400                 memoryDatabase.save()
401                 verify(configuration.getStringValue("KnownPosts/0/ID"), times(1)).value = null
402         }
403
404         @Test
405         @Dirty("the rate limiter should be mocked")
406         fun `setting posts as knows twice in a row only saves the database once`() {
407                 prepareConfigurationValues()
408                 val post = mock<Post>()
409                 whenever(post.id).thenReturn("post-id")
410                 memoryDatabase.setPostKnown(post, true)
411                 memoryDatabase.setPostKnown(post, true)
412                 verify(configuration, times(1)).getStringValue("KnownPosts/1/ID")
413         }
414
415         @Test
416         @Dirty("the rate limiter should be mocked")
417         fun `setting post replies as knows twice in a row only saves the database once`() {
418                 prepareConfigurationValues()
419                 val postReply = mock<PostReply>()
420                 whenever(postReply.id).thenReturn("post-reply-id")
421                 memoryDatabase.setPostReplyKnown(postReply)
422                 memoryDatabase.setPostReplyKnown(postReply)
423                 verify(configuration, times(1)).getStringValue("KnownReplies/1/ID")
424         }
425
426 }
427
428 private const val SONE_ID = "sone"
429 private const val RECIPIENT_ID = "recipient"