1 package net.pterodactylus.sone.web.pages
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.*
13 import java.util.concurrent.*
14 import java.util.concurrent.atomic.*
17 * Unit test for [SearchPage].
19 class SearchPageTest : WebPageTest({ webInterface, loaders, templateRenderer -> SearchPage(webInterface, loaders, templateRenderer, ticker) }) {
22 val ticker = mock<Ticker>()
26 fun `page returns correct path`() {
27 assertThat(page.path, equalTo("search.html"))
31 fun `page does not require login`() {
32 assertThat(page.requiresLogin(), equalTo(false))
36 fun `page returns correct title`() {
37 addTranslation("Page.Search.Title", "search page title")
38 assertThat(page.getPageTitle(soneRequest), equalTo("search page title"))
42 fun `empty query redirects to index page`() {
43 verifyRedirect("index.html")
47 fun `empty search phrases redirect to index page`() {
48 addHttpRequestParameter("query", "\"\"")
49 verifyRedirect("index.html")
53 fun `invalid search phrases redirect to index page`() {
54 addHttpRequestParameter("query", "\"")
55 verifyRedirect("index.html")
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")
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")
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")
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")
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")
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")
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")
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")
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")
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")
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) }
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)
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))
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")
159 assertThat(this["postHits"], contains<Post>(postWithMatch))
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")
177 assertThat(this["postHits"], contains<Post>(postWithMatch))
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))
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 ")
192 assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
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 ")
202 assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
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")
212 assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
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", "+")
222 assertThat(this["postHits"], contains<Post>(postWithMatch))
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", "-")
232 assertThat(this["postHits"], contains<Post>(postWithMatch))
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)
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
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")
261 assertThat(this["postHits"], contains<Post>(postWithMatch))
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")
273 assertThat(this["soneHits"], contains(soneWithProfileField))
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")
284 assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
285 assertThat(this["soneHits"], contains(sones[0], sones[2]))
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")
297 assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
298 assertThat(this["soneHits"], contains(sones[3]))
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")
308 assertThat(this["postPagination"], isOnPage(0).hasPages(2))
309 assertThat(this["postHits"], contains(sones[0], sones[2]))
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")
320 assertThat(this["postPagination"], isOnPage(1).hasPages(2))
321 assertThat(this["postHits"], contains(sones[3]))
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")
334 assertThat(this["postHits"], contains(post))
337 assertThat(callCounter.get(), equalTo(1))
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")
350 assertThat(this["postHits"], contains(post))
352 whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1)
354 assertThat(callCounter.get(), equalTo(2))
358 @Suppress("UNCHECKED_CAST")
359 private operator fun <T> get(key: String): T? = templateContext[key] as? T
362 fun `page can be created by dependency injection`() {
363 assertThat(baseInjector.getInstance<SearchPage>(), notNullValue())
367 fun `page is annotated with correct template path`() {
368 assertThat(page.templatePath, equalTo("/templates/search.html"))