⚡️ Use shell to store reply data
[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(of(RECIPIENT_ID))
181                 memoryDatabase.storePost(postWithRecipient)
182                 val postWithoutRecipient = createPost(absent())
183                 memoryDatabase.storePost(postWithoutRecipient)
184                 assertThat(memoryDatabase.getDirectedPosts(RECIPIENT_ID), contains(postWithRecipient))
185         }
186
187         private fun createPost(recipient: Optional<String>): Post {
188                 val postWithRecipient = mock<Post>()
189                 whenever(postWithRecipient.id).thenReturn(randomUUID().toString())
190                 whenever(postWithRecipient.sone).thenReturn(sone)
191                 whenever(postWithRecipient.recipientId).thenReturn(recipient)
192                 return postWithRecipient
193         }
194
195         @Test
196         fun `post replies are managed correctly`() {
197                 val firstPost = createPost(absent())
198                 val firstPostFirstReply = createPostReply(id = "p1r1", post = firstPost, time = 1000L)
199                 val secondPost = createPost(absent())
200                 val secondPostFirstReply = createPostReply(id = "p2r1", post = secondPost, time = 1000L)
201                 val secondPostSecondReply = createPostReply(id = "p2r2", post = secondPost, time = 2000L)
202                 memoryDatabase.storePost(firstPost)
203                 memoryDatabase.storePost(secondPost)
204                 memoryDatabase.storePostReply(firstPostFirstReply)
205                 memoryDatabase.storePostReply(secondPostFirstReply)
206                 memoryDatabase.storePostReply(secondPostSecondReply)
207                 assertThat(memoryDatabase.getReplies(firstPost.id).map(PostReply::id), Matchers.contains("p1r1"))
208                 assertThat(memoryDatabase.getReplies(secondPost.id).map(PostReply::id), contains("p2r1", "p2r2"))
209         }
210
211         @Test
212         fun `test basic album functionality`() {
213                 val newAlbum = AlbumImpl(mock())
214                 assertThat(memoryDatabase.getAlbum(newAlbum.id), nullValue())
215                 memoryDatabase.storeAlbum(newAlbum)
216                 assertThat(memoryDatabase.getAlbum(newAlbum.id), equalTo<Album>(newAlbum))
217                 memoryDatabase.removeAlbum(newAlbum)
218                 assertThat(memoryDatabase.getAlbum(newAlbum.id), nullValue())
219         }
220
221         private fun initializeFriends() {
222                 whenever(configuration.getStringValue("Sone/$SONE_ID/Friends/0/ID")).thenReturn(TestValue.from("Friend1"))
223                 whenever(configuration.getStringValue("Sone/$SONE_ID/Friends/1/ID")).thenReturn(TestValue.from("Friend2"))
224                 whenever(configuration.getStringValue("Sone/$SONE_ID/Friends/2/ID")).thenReturn(TestValue.from(null))
225         }
226
227         @Test
228         fun `friends are returned correctly`() {
229                 initializeFriends()
230                 whenever(sone.isLocal).thenReturn(true)
231                 val friends = memoryDatabase.getFriends(sone)
232                 assertThat(friends, containsInAnyOrder("Friend1", "Friend2"))
233         }
234
235         @Test
236         fun `friends are only loaded once from configuration`() {
237                 initializeFriends()
238                 whenever(sone.isLocal).thenReturn(true)
239                 memoryDatabase.getFriends(sone)
240                 memoryDatabase.getFriends(sone)
241                 verify(configuration, times(1)).getStringValue("Sone/$SONE_ID/Friends/0/ID")
242         }
243
244         @Test
245         fun `friends are only returned for local sones`() {
246                 val friends = memoryDatabase.getFriends(sone)
247                 assertThat(friends, emptyIterable<Any>())
248                 verify(configuration, never()).getStringValue("Sone/$SONE_ID/Friends/0/ID")
249         }
250
251         @Test
252         fun `checking for a friend returns true`() {
253                 initializeFriends()
254                 whenever(sone.isLocal).thenReturn(true)
255                 assertThat(memoryDatabase.isFriend(sone, "Friend1"), equalTo(true))
256         }
257
258         @Test
259         fun `checking for a friend that is not a friend returns false`() {
260                 initializeFriends()
261                 whenever(sone.isLocal).thenReturn(true)
262                 assertThat(memoryDatabase.isFriend(sone, "FriendX"), equalTo(false))
263         }
264
265         @Test
266         fun `checking for a friend of remote sone returns false`() {
267                 initializeFriends()
268                 assertThat(memoryDatabase.isFriend(sone, "Friend1"), equalTo(false))
269         }
270
271         private fun prepareConfigurationValues(): Map<String, Value<*>> =
272                         mutableMapOf<String, Value<*>>().also { configurationValues ->
273                                 whenever(configuration.getStringValue(anyString())).thenAnswer(createAndCacheValue(configurationValues))
274                                 whenever(configuration.getLongValue(anyString())).thenAnswer(createAndCacheValue(configurationValues))
275                         }
276
277         private fun createAndCacheValue(configurationValues: MutableMap<String, Value<*>>) =
278                         { invocation: InvocationOnMock ->
279                                 configurationValues[invocation[0]]
280                                                 ?: TestValue.from(null).also {
281                                                         configurationValues[invocation[0]] = it
282                                                 }
283                         }
284
285         @Test
286         fun `friend is added correctly to local sone`() {
287                 val configurationValues = prepareConfigurationValues()
288                 whenever(sone.isLocal).thenReturn(true)
289                 memoryDatabase.addFriend(sone, "Friend1")
290                 assertThat(configurationValues["Sone/$SONE_ID/Friends/0/ID"], equalTo<Value<*>>(TestValue.from("Friend1")))
291                 assertThat(configurationValues["Sone/$SONE_ID/Friends/1/ID"], equalTo<Value<*>>(TestValue.from(null)))
292         }
293
294         @Test
295         fun `friend is not added to remote sone`() {
296                 memoryDatabase.addFriend(sone, "Friend1")
297                 verify(configuration, never()).getStringValue(anyString())
298         }
299
300         @Test
301         fun `friend is removed correctly from local sone`() {
302                 val configurationValues = prepareConfigurationValues()
303                 whenever(sone.isLocal).thenReturn(true)
304                 memoryDatabase.addFriend(sone, "Friend1")
305                 memoryDatabase.removeFriend(sone, "Friend1")
306                 assertThat(configurationValues["Sone/$SONE_ID/Friends/0/ID"], equalTo<Value<*>>(TestValue.from(null)))
307                 assertThat(configurationValues["Sone/$SONE_ID/Friends/1/ID"], equalTo<Value<*>>(TestValue.from(null)))
308         }
309
310         @Test
311         fun `configuration is not written when a non-friend is removed`() {
312                 prepareConfigurationValues()
313                 whenever(sone.isLocal).thenReturn(true)
314                 memoryDatabase.removeFriend(sone, "Friend1")
315                 verify(configuration).getStringValue(anyString())
316         }
317
318         @Test
319         fun `sone following time is returned correctly`() {
320                 prepareConfigurationValues()
321                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "sone"
322                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
323                 assertThat(memoryDatabase.getFollowingTime("sone"), equalTo(1000L))
324         }
325
326         @Test
327         fun `null is returned when sone is not followed`() {
328                 prepareConfigurationValues()
329                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "otherSone"
330                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
331                 assertThat(memoryDatabase.getFollowingTime("sone"), nullValue())
332         }
333
334         @Test
335         fun `time is stored in configuration when a sone is followed`() {
336                 prepareConfigurationValues()
337                 whenever(sone.isLocal).thenReturn(true)
338                 memoryDatabase.addFriend(sone, "Friend")
339                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, equalTo("Friend"))
340                 assertThat(System.currentTimeMillis() - configuration.getLongValue("SoneFollowingTimes/0/Time").value, lessThan(1000L))
341                 assertThat(configuration.getStringValue("SoneFollowingTimes/1/Sone").value, nullValue())
342         }
343
344         @Test
345         fun `existing time is not overwritten when a sone is followed`() {
346                 prepareConfigurationValues()
347                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "Friend"
348                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
349                 whenever(sone.isLocal).thenReturn(true)
350                 memoryDatabase.addFriend(sone, "Friend")
351                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, equalTo("Friend"))
352                 assertThat(configuration.getLongValue("SoneFollowingTimes/0/Time").value, equalTo(1000L))
353                 assertThat(configuration.getStringValue("SoneFollowingTimes/1/Sone").value, nullValue())
354         }
355
356         @Test
357         fun `unfollowing a sone removes the following time`() {
358                 prepareConfigurationValues()
359                 configuration.getStringValue("Sone/sone/Friends/0/ID").value = "Friend"
360                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "Friend"
361                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
362                 whenever(sone.isLocal).thenReturn(true)
363                 memoryDatabase.removeFriend(sone, "Friend")
364                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, nullValue())
365         }
366
367         @Test
368         fun `unfollowing a sone does not remove the following time if another local sone follows it`() {
369                 prepareConfigurationValues()
370                 configuration.getStringValue("Sone/sone/Friends/0/ID").value = "Friend"
371                 configuration.getStringValue("Sone/other-sone/Friends/0/ID").value = "Friend"
372                 configuration.getStringValue("SoneFollowingTimes/0/Sone").value = "Friend"
373                 configuration.getLongValue("SoneFollowingTimes/0/Time").value = 1000L
374                 val otherSone = mock<Sone>()
375                 whenever(otherSone.isLocal).thenReturn(true)
376                 whenever(otherSone.id).thenReturn("other-sone")
377                 memoryDatabase.getFriends(otherSone)
378                 whenever(sone.isLocal).thenReturn(true)
379                 memoryDatabase.removeFriend(sone, "Friend")
380                 assertThat(configuration.getStringValue("SoneFollowingTimes/0/Sone").value, equalTo("Friend"))
381                 assertThat(configuration.getLongValue("SoneFollowingTimes/0/Time").value, equalTo(1000L))
382         }
383
384         @Test
385         fun `marking a post as known saves configuration`() {
386                 prepareConfigurationValues()
387                 val post = mock<Post>()
388                 whenever(post.id).thenReturn("post-id")
389                 memoryDatabase.setPostKnown(post, true)
390                 assertThat(configuration.getStringValue("KnownPosts/0/ID").value, equalTo("post-id"))
391                 assertThat(configuration.getStringValue("KnownPosts/1/ID").value, equalTo<Any>(null))
392         }
393
394         @Test
395         fun `marking a post reply as known saves configuration`() {
396                 prepareConfigurationValues()
397                 val postReply = mock<PostReply>()
398                 whenever(postReply.id).thenReturn("post-reply-id")
399                 memoryDatabase.setPostReplyKnown(postReply)
400                 assertThat(configuration.getStringValue("KnownReplies/0/ID").value, equalTo("post-reply-id"))
401                 assertThat(configuration.getStringValue("KnownReplies/1/ID").value, equalTo<Any>(null))
402         }
403
404         @Test
405         @Dirty("the rate limiter should be mocked")
406         fun `saving the database twice in a row only saves it once`() {
407                 memoryDatabase.save()
408                 memoryDatabase.save()
409                 verify(configuration.getStringValue("KnownPosts/0/ID"), times(1)).value = null
410         }
411
412         @Test
413         @Dirty("the rate limiter should be mocked")
414         fun `setting posts as knows twice in a row only saves the database once`() {
415                 prepareConfigurationValues()
416                 val post = mock<Post>()
417                 whenever(post.id).thenReturn("post-id")
418                 memoryDatabase.setPostKnown(post, true)
419                 memoryDatabase.setPostKnown(post, true)
420                 verify(configuration, times(1)).getStringValue("KnownPosts/1/ID")
421         }
422
423         @Test
424         @Dirty("the rate limiter should be mocked")
425         fun `setting post replies as knows twice in a row only saves the database once`() {
426                 prepareConfigurationValues()
427                 val postReply = mock<PostReply>()
428                 whenever(postReply.id).thenReturn("post-reply-id")
429                 memoryDatabase.setPostReplyKnown(postReply)
430                 memoryDatabase.setPostReplyKnown(postReply)
431                 verify(configuration, times(1)).getStringValue("KnownReplies/1/ID")
432         }
433
434 }
435
436 private const val SONE_ID = "sone"
437 private const val RECIPIENT_ID = "recipient"