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