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