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