Replace search page with Kotlin version, add more tests
[Sone.git] / src / test / kotlin / net / pterodactylus / sone / web / pages / SearchPageTest.kt
1 package net.pterodactylus.sone.web.pages
2
3 import com.google.common.base.Optional.absent
4 import com.google.common.base.Ticker
5 import net.pterodactylus.sone.data.Album
6 import net.pterodactylus.sone.data.Image
7 import net.pterodactylus.sone.data.Post
8 import net.pterodactylus.sone.data.PostReply
9 import net.pterodactylus.sone.data.Profile
10 import net.pterodactylus.sone.data.Sone
11 import net.pterodactylus.sone.test.asOptional
12 import net.pterodactylus.sone.test.isOnPage
13 import net.pterodactylus.sone.test.mock
14 import net.pterodactylus.sone.test.whenever
15 import org.hamcrest.MatcherAssert.assertThat
16 import org.hamcrest.Matchers.contains
17 import org.hamcrest.Matchers.equalTo
18 import org.junit.Test
19 import java.util.concurrent.TimeUnit
20 import java.util.concurrent.atomic.AtomicInteger
21
22 /**
23  * Unit test for [SearchPage].
24  */
25 class SearchPageTest : WebPageTest() {
26
27         private val ticker = mock<Ticker>()
28         private val page = SearchPage(template, webInterface, ticker)
29
30         override fun getPage() = page
31
32         @Test
33         fun `page returns correct path`() {
34             assertThat(page.path, equalTo("search.html"))
35         }
36
37         @Test
38         fun `page does not require login`() {
39             assertThat(page.requiresLogin(), equalTo(false))
40         }
41
42         @Test
43         fun `page returns correct title`() {
44             addTranslation("Page.Search.Title", "search page title")
45                 assertThat(page.getPageTitle(freenetRequest), equalTo("search page title"))
46         }
47
48         @Test
49         fun `empty query redirects to index page`() {
50                 verifyRedirect("index.html")
51         }
52
53         @Test
54         fun `empty search phrases redirect to index page`() {
55                 addHttpRequestParameter("query", "\"\"")
56                 verifyRedirect("index.html")
57         }
58
59         @Test
60         fun `invalid search phrases redirect to index page`() {
61                 addHttpRequestParameter("query", "\"")
62                 verifyRedirect("index.html")
63         }
64
65         @Test
66         fun `searching for sone link redirects to view sone page`() {
67                 addSone("sone-id", mock<Sone>())
68                 addHttpRequestParameter("query", "sone://sone-id")
69                 verifyRedirect("viewSone.html?sone=sone-id")
70         }
71
72         @Test
73         fun `searching for sone link without prefix redirects to view sone page`() {
74                 addSone("sone-id", mock<Sone>())
75                 addHttpRequestParameter("query", "sone-id")
76                 verifyRedirect("viewSone.html?sone=sone-id")
77         }
78
79         @Test
80         fun `searching for a post link redirects to post page`() {
81                 addPost("post-id", mock<Post>())
82                 addHttpRequestParameter("query", "post://post-id")
83                 verifyRedirect("viewPost.html?post=post-id")
84         }
85
86         @Test
87         fun `searching for a post ID without prefix redirects to post page`() {
88                 addPost("post-id", mock<Post>())
89                 addHttpRequestParameter("query", "post-id")
90                 verifyRedirect("viewPost.html?post=post-id")
91         }
92
93         @Test
94         fun `searching for a reply link redirects to the post page`() {
95                 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
96                 addPostReply("reply-id", postReply)
97                 addHttpRequestParameter("query", "reply://reply-id")
98                 verifyRedirect("viewPost.html?post=post-id")
99         }
100
101         @Test
102         fun `searching for a reply ID redirects to the post page`() {
103                 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
104                 addPostReply("reply-id", postReply)
105                 addHttpRequestParameter("query", "reply-id")
106                 verifyRedirect("viewPost.html?post=post-id")
107         }
108
109         @Test
110         fun `searching for an album link redirects to the image browser`() {
111                 addAlbum("album-id", mock<Album>())
112                 addHttpRequestParameter("query", "album://album-id")
113                 verifyRedirect("imageBrowser.html?album=album-id")
114         }
115
116         @Test
117         fun `searching for an album ID redirects to the image browser`() {
118                 addAlbum("album-id", mock<Album>())
119                 addHttpRequestParameter("query", "album-id")
120                 verifyRedirect("imageBrowser.html?album=album-id")
121         }
122
123         @Test
124         fun `searching for an image link redirects to the image browser`() {
125                 addImage("image-id", mock<Image>())
126                 addHttpRequestParameter("query", "image://image-id")
127                 verifyRedirect("imageBrowser.html?image=image-id")
128         }
129
130         @Test
131         fun `searching for an image ID redirects to the image browser`() {
132                 addImage("image-id", mock<Image>())
133                 addHttpRequestParameter("query", "image-id")
134                 verifyRedirect("imageBrowser.html?image=image-id")
135         }
136
137         private fun createReply(text: String, postId: String? = null, sone: Sone? = null) = mock<PostReply>().apply {
138                 whenever(this.text).thenReturn(text)
139                 postId?.run { whenever(this@apply.postId).thenReturn(postId) }
140                 sone?.run { whenever(this@apply.sone).thenReturn(sone) }
141         }
142
143         private fun createPost(id: String, text: String) = mock<Post>().apply {
144                 whenever(this.id).thenReturn(id)
145                 whenever(recipient).thenReturn(absent())
146                 whenever(this.text).thenReturn(text)
147         }
148
149         private fun createSoneWithPost(post: Post, sone: Sone? = null) = sone?.apply {
150                 whenever(posts).thenReturn(listOf(post))
151         } ?: mock<Sone>().apply {
152                 whenever(posts).thenReturn(listOf(post))
153                 whenever(profile).thenReturn(Profile(this))
154         }
155
156         @Test
157         fun `searching for a single word finds the post`() {
158                 val postWithMatch = createPost("post-with-match", "the word here")
159                 val postWithoutMatch = createPost("post-without-match", "no match here")
160                 val soneWithMatch = createSoneWithPost(postWithMatch)
161                 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
162                 addSone("sone-with-match", soneWithMatch)
163                 addSone("sone-without-match", soneWithoutMatch)
164                 addHttpRequestParameter("query", "word")
165                 verifyNoRedirect {
166                         assertThat(this["postHits"], contains<Post>(postWithMatch))
167                 }
168         }
169
170         @Test
171         fun `searching for a single word locates word in reply`() {
172                 val postWithMatch = createPost("post-with-match", "no match here")
173                 val postWithoutMatch = createPost("post-without-match", "no match here")
174                 val soneWithMatch = createSoneWithPost(postWithMatch)
175                 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
176                 val replyWithMatch = createReply("the word here", "post-with-match", soneWithMatch)
177                 val replyWithoutMatch = createReply("no match here", "post-without-match", soneWithoutMatch)
178                 addPostReply("reply-with-match", replyWithMatch)
179                 addPostReply("reply-without-match", replyWithoutMatch)
180                 addSone("sone-with-match", soneWithMatch)
181                 addSone("sone-without-match", soneWithoutMatch)
182                 addHttpRequestParameter("query", "word")
183                 verifyNoRedirect {
184                         assertThat(this["postHits"], contains<Post>(postWithMatch))
185                 }
186         }
187
188         private fun createSoneWithPost(idPostfix: String, text: String, recipient: Sone? = null, sender: Sone? = null) =
189                         createPost("post-$idPostfix", text, recipient).apply {
190                                 addSone("sone-$idPostfix", createSoneWithPost(this, sender))
191                         }
192
193         @Test
194         fun `earlier matches score higher than later matches`() {
195                 val postWithEarlyMatch = createSoneWithPost("with-early-match", "optional match")
196                 val postWithLaterMatch = createSoneWithPost("with-later-match", "match that is optional")
197                 addHttpRequestParameter("query", "optional ")
198                 verifyNoRedirect {
199                         assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
200                 }
201         }
202
203         @Test
204         fun `searching for required word does not return posts without that word`() {
205                 val postWithRequiredMatch = createSoneWithPost("with-required-match", "required match")
206                 createPost("without-required-match", "not a match")
207                 addHttpRequestParameter("query", "+required ")
208                 verifyNoRedirect {
209                         assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
210                 }
211         }
212
213         @Test
214         fun `searching for forbidden word does not return posts with that word`() {
215                 createSoneWithPost("with-forbidden-match", "forbidden match")
216                 val postWithoutForbiddenMatch = createSoneWithPost("without-forbidden-match", "not a match")
217                 addHttpRequestParameter("query", "match -forbidden")
218                 verifyNoRedirect {
219                         assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
220                 }
221         }
222
223         @Test
224         fun `searching for a plus sign searches for optional plus sign`() {
225                 val postWithMatch = createSoneWithPost("with-match", "with + match")
226                 createSoneWithPost("without-match", "without match")
227                 addHttpRequestParameter("query", "+")
228                 verifyNoRedirect {
229                         assertThat(this["postHits"], contains<Post>(postWithMatch))
230                 }
231         }
232
233         @Test
234         fun `searching for a minus sign searches for optional minus sign`() {
235                 val postWithMatch = createSoneWithPost("with-match", "with - match")
236                 createSoneWithPost("without-match", "without match")
237                 addHttpRequestParameter("query", "-")
238                 verifyNoRedirect {
239                         assertThat(this["postHits"], contains<Post>(postWithMatch))
240                 }
241         }
242
243         private fun createPost(id: String, text: String, recipient: Sone?) = mock<Post>().apply {
244                 whenever(this.id).thenReturn(id)
245                 val recipientId = recipient?.id
246                 whenever(this.recipientId).thenReturn(recipientId.asOptional())
247                 whenever(this.recipient).thenReturn(recipient.asOptional())
248                 whenever(this.text).thenReturn(text)
249         }
250
251         private fun createSone(id: String, firstName: String? = null, middleName: String? = null, lastName: String? = null) = mock<Sone>().apply {
252                 whenever(this.id).thenReturn(id)
253                 whenever(this.name).thenReturn(id)
254                 whenever(this.profile).thenReturn(Profile(this).apply {
255                         this.firstName = firstName
256                         this.middleName = middleName
257                         this.lastName = lastName
258                 })
259         }
260
261         @Test
262         fun `searching for a recipient finds the correct post`() {
263                 val recipient = createSone("recipient", "reci", "pi", "ent")
264                 val postWithMatch = createSoneWithPost("with-match", "test", recipient)
265                 createSoneWithPost("without-match", "no match")
266                 addHttpRequestParameter("query", "recipient")
267                 verifyNoRedirect {
268                         assertThat(this["postHits"], contains<Post>(postWithMatch))
269                 }
270         }
271
272         @Test
273         fun `searching for a field value finds the correct sone`() {
274                 val soneWithProfileField = createSone("sone", "s", "o", "ne")
275                 soneWithProfileField.profile.addField("field").value = "value"
276                 createSoneWithPost("with-match", "test", sender = soneWithProfileField)
277                 createSoneWithPost("without-match", "no match")
278                 addHttpRequestParameter("query", "value")
279                 verifyNoRedirect {
280                         assertThat(this["soneHits"], contains(soneWithProfileField))
281                 }
282         }
283
284         @Test
285         fun `sone hits are paginated correctly`() {
286                 core.preferences.postsPerPage = 2
287                 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
288                                 .onEach { addSone(it.id, it) }
289                 addHttpRequestParameter("query", "sone")
290                 verifyNoRedirect {
291                         assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
292                         assertThat(this["soneHits"], contains(sones[0], sones[2]))
293                 }
294         }
295
296         @Test
297         fun `sone hits page 2 is shown correctly`() {
298                 core.preferences.postsPerPage = 2
299                 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
300                                 .onEach { addSone(it.id, it) }
301                 addHttpRequestParameter("query", "sone")
302                 addHttpRequestParameter("sonePage", "1")
303                 verifyNoRedirect {
304                         assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
305                         assertThat(this["soneHits"], contains(sones[3]))
306                 }
307         }
308
309         @Test
310         fun `post hits are paginated correctly`() {
311                 core.preferences.postsPerPage = 2
312                 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
313                 addHttpRequestParameter("query", "sone")
314                 verifyNoRedirect {
315                         assertThat(this["postPagination"], isOnPage(0).hasPages(2))
316                         assertThat(this["postHits"], contains(sones[0], sones[2]))
317                 }
318         }
319
320         @Test
321         fun `post hits page 2 is shown correctly`() {
322                 core.preferences.postsPerPage = 2
323                 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
324                 addHttpRequestParameter("query", "sone")
325                 addHttpRequestParameter("postPage", "1")
326                 verifyNoRedirect {
327                         assertThat(this["postPagination"], isOnPage(1).hasPages(2))
328                         assertThat(this["postHits"], contains(sones[3]))
329                 }
330         }
331
332         @Test
333         fun `post search results are cached`() {
334                 val post = createPost("with-match", "text")
335                 val callCounter = AtomicInteger()
336                 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
337             val sone = createSoneWithPost(post)
338                 addSone("sone", sone)
339                 addHttpRequestParameter("query", "text")
340                 verifyNoRedirect {
341                         assertThat(this["postHits"], contains(post))
342                 }
343                 verifyNoRedirect {
344                         assertThat(callCounter.get(), equalTo(1))
345                 }
346         }
347
348         @Test
349         fun `post search results are cached for five minutes`() {
350                 val post = createPost("with-match", "text")
351                 val callCounter = AtomicInteger()
352                 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
353             val sone = createSoneWithPost(post)
354                 addSone("sone", sone)
355                 addHttpRequestParameter("query", "text")
356                 verifyNoRedirect {
357                         assertThat(this["postHits"], contains(post))
358                 }
359                 whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1  )
360                 verifyNoRedirect {
361                         assertThat(callCounter.get(), equalTo(2))
362                 }
363         }
364
365         @Suppress("UNCHECKED_CAST")
366         private operator fun <T> get(key: String): T? = templateContext[key] as? T
367
368 }