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 org.hamcrest.MatcherAssert.assertThat
18 import org.hamcrest.Matchers.contains
19 import org.hamcrest.Matchers.equalTo
20 import org.hamcrest.Matchers.notNullValue
22 import java.util.concurrent.TimeUnit
23 import java.util.concurrent.atomic.AtomicInteger
26 * Unit test for [SearchPage].
28 class SearchPageTest: WebPageTest({ template, webInterface -> SearchPage(template, webInterface, ticker) }) {
31 val ticker = mock<Ticker>()
35 fun `page returns correct path`() {
36 assertThat(page.path, equalTo("search.html"))
40 fun `page does not require login`() {
41 assertThat(page.requiresLogin(), equalTo(false))
45 fun `page returns correct title`() {
46 addTranslation("Page.Search.Title", "search page title")
47 assertThat(page.getPageTitle(freenetRequest), equalTo("search page title"))
51 fun `empty query redirects to index page`() {
52 verifyRedirect("index.html")
56 fun `empty search phrases redirect to index page`() {
57 addHttpRequestParameter("query", "\"\"")
58 verifyRedirect("index.html")
62 fun `invalid search phrases redirect to index page`() {
63 addHttpRequestParameter("query", "\"")
64 verifyRedirect("index.html")
68 fun `searching for sone link redirects to view sone page`() {
69 addSone("Sone-ID", mock())
70 addHttpRequestParameter("query", "sone://Sone-ID")
71 verifyRedirect("viewSone.html?sone=Sone-ID")
75 fun `searching for sone link without prefix redirects to view sone page`() {
76 addSone("sone-id", mock())
77 addHttpRequestParameter("query", "sone-id")
78 verifyRedirect("viewSone.html?sone=sone-id")
82 fun `searching for a post link redirects to post page`() {
83 addPost("Post-id", mock())
84 addHttpRequestParameter("query", "post://Post-id")
85 verifyRedirect("viewPost.html?post=Post-id")
89 fun `searching for a post ID without prefix redirects to post page`() {
90 addPost("post-id", mock())
91 addHttpRequestParameter("query", "post-id")
92 verifyRedirect("viewPost.html?post=post-id")
96 fun `searching for a reply link redirects to the post page`() {
97 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
98 addPostReply("Reply-id", postReply)
99 addHttpRequestParameter("query", "reply://Reply-id")
100 verifyRedirect("viewPost.html?post=post-id")
104 fun `searching for a reply ID redirects to the post page`() {
105 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
106 addPostReply("reply-id", postReply)
107 addHttpRequestParameter("query", "reply-id")
108 verifyRedirect("viewPost.html?post=post-id")
112 fun `searching for an album link redirects to the image browser`() {
113 addAlbum("album-id", mock())
114 addHttpRequestParameter("query", "album://album-id")
115 verifyRedirect("imageBrowser.html?album=album-id")
119 fun `searching for an album ID redirects to the image browser`() {
120 addAlbum("album-id", mock())
121 addHttpRequestParameter("query", "album-id")
122 verifyRedirect("imageBrowser.html?album=album-id")
126 fun `searching for an image link redirects to the image browser`() {
127 addImage("image-id", mock())
128 addHttpRequestParameter("query", "image://image-id")
129 verifyRedirect("imageBrowser.html?image=image-id")
133 fun `searching for an image ID redirects to the image browser`() {
134 addImage("image-id", mock())
135 addHttpRequestParameter("query", "image-id")
136 verifyRedirect("imageBrowser.html?image=image-id")
139 private fun createReply(text: String, postId: String? = null, sone: Sone? = null) = mock<PostReply>().apply {
140 whenever(this.text).thenReturn(text)
141 postId?.run { whenever(this@apply.postId).thenReturn(postId) }
142 sone?.run { whenever(this@apply.sone).thenReturn(sone) }
145 private fun createPost(id: String, text: String) = mock<Post>().apply {
146 whenever(this.id).thenReturn(id)
147 whenever(recipient).thenReturn(absent())
148 whenever(this.text).thenReturn(text)
151 private fun createSoneWithPost(post: Post, sone: Sone? = null) = sone?.apply {
152 whenever(posts).thenReturn(listOf(post))
153 } ?: mock<Sone>().apply {
154 whenever(posts).thenReturn(listOf(post))
155 whenever(profile).thenReturn(Profile(this))
159 fun `searching for a single word finds the post`() {
160 val postWithMatch = createPost("post-with-match", "the word here")
161 val postWithoutMatch = createPost("post-without-match", "no match here")
162 val soneWithMatch = createSoneWithPost(postWithMatch)
163 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
164 addSone("sone-with-match", soneWithMatch)
165 addSone("sone-without-match", soneWithoutMatch)
166 addHttpRequestParameter("query", "word")
168 assertThat(this["postHits"], contains<Post>(postWithMatch))
173 fun `searching for a single word locates word in reply`() {
174 val postWithMatch = createPost("post-with-match", "no match here")
175 val postWithoutMatch = createPost("post-without-match", "no match here")
176 val soneWithMatch = createSoneWithPost(postWithMatch)
177 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
178 val replyWithMatch = createReply("the word here", "post-with-match", soneWithMatch)
179 val replyWithoutMatch = createReply("no match here", "post-without-match", soneWithoutMatch)
180 addPostReply("reply-with-match", replyWithMatch)
181 addPostReply("reply-without-match", replyWithoutMatch)
182 addSone("sone-with-match", soneWithMatch)
183 addSone("sone-without-match", soneWithoutMatch)
184 addHttpRequestParameter("query", "word")
186 assertThat(this["postHits"], contains<Post>(postWithMatch))
190 private fun createSoneWithPost(idPostfix: String, text: String, recipient: Sone? = null, sender: Sone? = null) =
191 createPost("post-$idPostfix", text, recipient).apply {
192 addSone("sone-$idPostfix", createSoneWithPost(this, sender))
196 fun `earlier matches score higher than later matches`() {
197 val postWithEarlyMatch = createSoneWithPost("with-early-match", "optional match")
198 val postWithLaterMatch = createSoneWithPost("with-later-match", "match that is optional")
199 addHttpRequestParameter("query", "optional ")
201 assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
206 fun `searching for required word does not return posts without that word`() {
207 val postWithRequiredMatch = createSoneWithPost("with-required-match", "required match")
208 createPost("without-required-match", "not a match")
209 addHttpRequestParameter("query", "+required ")
211 assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
216 fun `searching for forbidden word does not return posts with that word`() {
217 createSoneWithPost("with-forbidden-match", "forbidden match")
218 val postWithoutForbiddenMatch = createSoneWithPost("without-forbidden-match", "not a match")
219 addHttpRequestParameter("query", "match -forbidden")
221 assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
226 fun `searching for a plus sign searches for optional plus sign`() {
227 val postWithMatch = createSoneWithPost("with-match", "with + match")
228 createSoneWithPost("without-match", "without match")
229 addHttpRequestParameter("query", "+")
231 assertThat(this["postHits"], contains<Post>(postWithMatch))
236 fun `searching for a minus sign searches for optional minus sign`() {
237 val postWithMatch = createSoneWithPost("with-match", "with - match")
238 createSoneWithPost("without-match", "without match")
239 addHttpRequestParameter("query", "-")
241 assertThat(this["postHits"], contains<Post>(postWithMatch))
245 private fun createPost(id: String, text: String, recipient: Sone?) = mock<Post>().apply {
246 whenever(this.id).thenReturn(id)
247 val recipientId = recipient?.id
248 whenever(this.recipientId).thenReturn(recipientId.asOptional())
249 whenever(this.recipient).thenReturn(recipient.asOptional())
250 whenever(this.text).thenReturn(text)
253 private fun createSone(id: String, firstName: String? = null, middleName: String? = null, lastName: String? = null) = mock<Sone>().apply {
254 whenever(this.id).thenReturn(id)
255 whenever(this.name).thenReturn(id)
256 whenever(this.profile).thenReturn(Profile(this).apply {
257 this.firstName = firstName
258 this.middleName = middleName
259 this.lastName = lastName
264 fun `searching for a recipient finds the correct post`() {
265 val recipient = createSone("recipient", "reci", "pi", "ent")
266 val postWithMatch = createSoneWithPost("with-match", "test", recipient)
267 createSoneWithPost("without-match", "no match")
268 addHttpRequestParameter("query", "recipient")
270 assertThat(this["postHits"], contains<Post>(postWithMatch))
275 fun `searching for a field value finds the correct sone`() {
276 val soneWithProfileField = createSone("sone", "s", "o", "ne")
277 soneWithProfileField.profile.addField("field").value = "value"
278 createSoneWithPost("with-match", "test", sender = soneWithProfileField)
279 createSoneWithPost("without-match", "no match")
280 addHttpRequestParameter("query", "value")
282 assertThat(this["soneHits"], contains(soneWithProfileField))
287 fun `sone hits are paginated correctly`() {
288 core.preferences.newPostsPerPage = 2
289 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
290 .onEach { addSone(it.id, it) }
291 addHttpRequestParameter("query", "sone")
293 assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
294 assertThat(this["soneHits"], contains(sones[0], sones[2]))
299 fun `sone hits page 2 is shown correctly`() {
300 core.preferences.newPostsPerPage = 2
301 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
302 .onEach { addSone(it.id, it) }
303 addHttpRequestParameter("query", "sone")
304 addHttpRequestParameter("sonePage", "1")
306 assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
307 assertThat(this["soneHits"], contains(sones[3]))
312 fun `post hits are paginated correctly`() {
313 core.preferences.newPostsPerPage = 2
314 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
315 addHttpRequestParameter("query", "sone")
317 assertThat(this["postPagination"], isOnPage(0).hasPages(2))
318 assertThat(this["postHits"], contains(sones[0], sones[2]))
323 fun `post hits page 2 is shown correctly`() {
324 core.preferences.newPostsPerPage = 2
325 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
326 addHttpRequestParameter("query", "sone")
327 addHttpRequestParameter("postPage", "1")
329 assertThat(this["postPagination"], isOnPage(1).hasPages(2))
330 assertThat(this["postHits"], contains(sones[3]))
335 fun `post search results are cached`() {
336 val post = createPost("with-match", "text")
337 val callCounter = AtomicInteger()
338 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
339 val sone = createSoneWithPost(post)
340 addSone("sone", sone)
341 addHttpRequestParameter("query", "text")
343 assertThat(this["postHits"], contains(post))
346 assertThat(callCounter.get(), equalTo(1))
351 fun `post search results are cached for five minutes`() {
352 val post = createPost("with-match", "text")
353 val callCounter = AtomicInteger()
354 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
355 val sone = createSoneWithPost(post)
356 addSone("sone", sone)
357 addHttpRequestParameter("query", "text")
359 assertThat(this["postHits"], contains(post))
361 whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1)
363 assertThat(callCounter.get(), equalTo(2))
367 @Suppress("UNCHECKED_CAST")
368 private operator fun <T> get(key: String): T? = templateContext[key] as? T
371 fun `page can be created by dependency injection`() {
372 assertThat(baseInjector.getInstance<SearchPage>(), notNullValue())