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