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