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