1 package net.pterodactylus.sone.web.pages
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
23 import java.util.concurrent.TimeUnit
24 import java.util.concurrent.atomic.AtomicInteger
27 * Unit test for [SearchPage].
29 class SearchPageTest: WebPageTest({ template, webInterface, loaders, templateRenderer -> SearchPage(template, webInterface, loaders, templateRenderer, ticker) }) {
32 val ticker = mock<Ticker>()
36 fun `page returns correct path`() {
37 assertThat(page.path, equalTo("search.html"))
41 fun `page does not require login`() {
42 assertThat(page.requiresLogin(), equalTo(false))
46 fun `page returns correct title`() {
47 addTranslation("Page.Search.Title", "search page title")
48 assertThat(page.getPageTitle(soneRequest), equalTo("search page title"))
52 fun `empty query redirects to index page`() {
53 verifyRedirect("index.html")
57 fun `empty search phrases redirect to index page`() {
58 addHttpRequestParameter("query", "\"\"")
59 verifyRedirect("index.html")
63 fun `invalid search phrases redirect to index page`() {
64 addHttpRequestParameter("query", "\"")
65 verifyRedirect("index.html")
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")
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")
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")
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")
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")
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")
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")
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")
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")
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")
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) }
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)
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))
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")
169 assertThat(this["postHits"], contains<Post>(postWithMatch))
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")
187 assertThat(this["postHits"], contains<Post>(postWithMatch))
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))
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 ")
202 assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
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 ")
212 assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
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")
222 assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
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", "+")
232 assertThat(this["postHits"], contains<Post>(postWithMatch))
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", "-")
242 assertThat(this["postHits"], contains<Post>(postWithMatch))
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)
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
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")
271 assertThat(this["postHits"], contains<Post>(postWithMatch))
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")
283 assertThat(this["soneHits"], contains(soneWithProfileField))
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")
294 assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
295 assertThat(this["soneHits"], contains(sones[0], sones[2]))
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")
307 assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
308 assertThat(this["soneHits"], contains(sones[3]))
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")
318 assertThat(this["postPagination"], isOnPage(0).hasPages(2))
319 assertThat(this["postHits"], contains(sones[0], sones[2]))
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")
330 assertThat(this["postPagination"], isOnPage(1).hasPages(2))
331 assertThat(this["postHits"], contains(sones[3]))
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")
344 assertThat(this["postHits"], contains(post))
347 assertThat(callCounter.get(), equalTo(1))
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")
360 assertThat(this["postHits"], contains(post))
362 whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1)
364 assertThat(callCounter.get(), equalTo(2))
368 @Suppress("UNCHECKED_CAST")
369 private operator fun <T> get(key: String): T? = templateContext[key] as? T
372 fun `page can be created by dependency injection`() {
373 assertThat(baseInjector.getInstance<SearchPage>(), notNullValue())
377 fun `page is annotated with correct template path`() {
378 assertThat(page.templatePath, equalTo("/templates/search.html"))