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.isOnPage
12 import net.pterodactylus.sone.test.mock
13 import net.pterodactylus.sone.test.whenever
14 import net.pterodactylus.sone.utils.asOptional
15 import org.hamcrest.MatcherAssert.assertThat
16 import org.hamcrest.Matchers.contains
17 import org.hamcrest.Matchers.equalTo
19 import java.util.concurrent.TimeUnit
20 import java.util.concurrent.atomic.AtomicInteger
23 * Unit test for [SearchPage].
25 class SearchPageTest : WebPageTest() {
27 private val ticker = mock<Ticker>()
28 private val page = SearchPage(template, webInterface, ticker)
30 override fun getPage() = page
33 fun `page returns correct path`() {
34 assertThat(page.path, equalTo("search.html"))
38 fun `page does not require login`() {
39 assertThat(page.requiresLogin(), equalTo(false))
43 fun `page returns correct title`() {
44 addTranslation("Page.Search.Title", "search page title")
45 assertThat(page.getPageTitle(freenetRequest), equalTo("search page title"))
49 fun `empty query redirects to index page`() {
50 verifyRedirect("index.html")
54 fun `empty search phrases redirect to index page`() {
55 addHttpRequestParameter("query", "\"\"")
56 verifyRedirect("index.html")
60 fun `invalid search phrases redirect to index page`() {
61 addHttpRequestParameter("query", "\"")
62 verifyRedirect("index.html")
66 fun `searching for sone link redirects to view sone page`() {
67 addSone("sone-id", mock<Sone>())
68 addHttpRequestParameter("query", "sone://sone-id")
69 verifyRedirect("viewSone.html?sone=sone-id")
73 fun `searching for sone link without prefix redirects to view sone page`() {
74 addSone("sone-id", mock<Sone>())
75 addHttpRequestParameter("query", "sone-id")
76 verifyRedirect("viewSone.html?sone=sone-id")
80 fun `searching for a post link redirects to post page`() {
81 addPost("post-id", mock<Post>())
82 addHttpRequestParameter("query", "post://post-id")
83 verifyRedirect("viewPost.html?post=post-id")
87 fun `searching for a post ID without prefix redirects to post page`() {
88 addPost("post-id", mock<Post>())
89 addHttpRequestParameter("query", "post-id")
90 verifyRedirect("viewPost.html?post=post-id")
94 fun `searching for a reply link redirects to the post page`() {
95 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
96 addPostReply("reply-id", postReply)
97 addHttpRequestParameter("query", "reply://reply-id")
98 verifyRedirect("viewPost.html?post=post-id")
102 fun `searching for a reply ID redirects to the post page`() {
103 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
104 addPostReply("reply-id", postReply)
105 addHttpRequestParameter("query", "reply-id")
106 verifyRedirect("viewPost.html?post=post-id")
110 fun `searching for an album link redirects to the image browser`() {
111 addAlbum("album-id", mock<Album>())
112 addHttpRequestParameter("query", "album://album-id")
113 verifyRedirect("imageBrowser.html?album=album-id")
117 fun `searching for an album ID redirects to the image browser`() {
118 addAlbum("album-id", mock<Album>())
119 addHttpRequestParameter("query", "album-id")
120 verifyRedirect("imageBrowser.html?album=album-id")
124 fun `searching for an image link redirects to the image browser`() {
125 addImage("image-id", mock<Image>())
126 addHttpRequestParameter("query", "image://image-id")
127 verifyRedirect("imageBrowser.html?image=image-id")
131 fun `searching for an image ID redirects to the image browser`() {
132 addImage("image-id", mock<Image>())
133 addHttpRequestParameter("query", "image-id")
134 verifyRedirect("imageBrowser.html?image=image-id")
137 private fun createReply(text: String, postId: String? = null, sone: Sone? = null) = mock<PostReply>().apply {
138 whenever(this.text).thenReturn(text)
139 postId?.run { whenever(this@apply.postId).thenReturn(postId) }
140 sone?.run { whenever(this@apply.sone).thenReturn(sone) }
143 private fun createPost(id: String, text: String) = mock<Post>().apply {
144 whenever(this.id).thenReturn(id)
145 whenever(recipient).thenReturn(absent())
146 whenever(this.text).thenReturn(text)
149 private fun createSoneWithPost(post: Post, sone: Sone? = null) = sone?.apply {
150 whenever(posts).thenReturn(listOf(post))
151 } ?: mock<Sone>().apply {
152 whenever(posts).thenReturn(listOf(post))
153 whenever(profile).thenReturn(Profile(this))
157 fun `searching for a single word finds the post`() {
158 val postWithMatch = createPost("post-with-match", "the word here")
159 val postWithoutMatch = createPost("post-without-match", "no match here")
160 val soneWithMatch = createSoneWithPost(postWithMatch)
161 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
162 addSone("sone-with-match", soneWithMatch)
163 addSone("sone-without-match", soneWithoutMatch)
164 addHttpRequestParameter("query", "word")
166 assertThat(this["postHits"], contains<Post>(postWithMatch))
171 fun `searching for a single word locates word in reply`() {
172 val postWithMatch = createPost("post-with-match", "no match here")
173 val postWithoutMatch = createPost("post-without-match", "no match here")
174 val soneWithMatch = createSoneWithPost(postWithMatch)
175 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
176 val replyWithMatch = createReply("the word here", "post-with-match", soneWithMatch)
177 val replyWithoutMatch = createReply("no match here", "post-without-match", soneWithoutMatch)
178 addPostReply("reply-with-match", replyWithMatch)
179 addPostReply("reply-without-match", replyWithoutMatch)
180 addSone("sone-with-match", soneWithMatch)
181 addSone("sone-without-match", soneWithoutMatch)
182 addHttpRequestParameter("query", "word")
184 assertThat(this["postHits"], contains<Post>(postWithMatch))
188 private fun createSoneWithPost(idPostfix: String, text: String, recipient: Sone? = null, sender: Sone? = null) =
189 createPost("post-$idPostfix", text, recipient).apply {
190 addSone("sone-$idPostfix", createSoneWithPost(this, sender))
194 fun `earlier matches score higher than later matches`() {
195 val postWithEarlyMatch = createSoneWithPost("with-early-match", "optional match")
196 val postWithLaterMatch = createSoneWithPost("with-later-match", "match that is optional")
197 addHttpRequestParameter("query", "optional ")
199 assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
204 fun `searching for required word does not return posts without that word`() {
205 val postWithRequiredMatch = createSoneWithPost("with-required-match", "required match")
206 createPost("without-required-match", "not a match")
207 addHttpRequestParameter("query", "+required ")
209 assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
214 fun `searching for forbidden word does not return posts with that word`() {
215 createSoneWithPost("with-forbidden-match", "forbidden match")
216 val postWithoutForbiddenMatch = createSoneWithPost("without-forbidden-match", "not a match")
217 addHttpRequestParameter("query", "match -forbidden")
219 assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
224 fun `searching for a plus sign searches for optional plus sign`() {
225 val postWithMatch = createSoneWithPost("with-match", "with + match")
226 createSoneWithPost("without-match", "without match")
227 addHttpRequestParameter("query", "+")
229 assertThat(this["postHits"], contains<Post>(postWithMatch))
234 fun `searching for a minus sign searches for optional minus sign`() {
235 val postWithMatch = createSoneWithPost("with-match", "with - match")
236 createSoneWithPost("without-match", "without match")
237 addHttpRequestParameter("query", "-")
239 assertThat(this["postHits"], contains<Post>(postWithMatch))
243 private fun createPost(id: String, text: String, recipient: Sone?) = mock<Post>().apply {
244 whenever(this.id).thenReturn(id)
245 val recipientId = recipient?.id
246 whenever(this.recipientId).thenReturn(recipientId.asOptional())
247 whenever(this.recipient).thenReturn(recipient.asOptional())
248 whenever(this.text).thenReturn(text)
251 private fun createSone(id: String, firstName: String? = null, middleName: String? = null, lastName: String? = null) = mock<Sone>().apply {
252 whenever(this.id).thenReturn(id)
253 whenever(this.name).thenReturn(id)
254 whenever(this.profile).thenReturn(Profile(this).apply {
255 this.firstName = firstName
256 this.middleName = middleName
257 this.lastName = lastName
262 fun `searching for a recipient finds the correct post`() {
263 val recipient = createSone("recipient", "reci", "pi", "ent")
264 val postWithMatch = createSoneWithPost("with-match", "test", recipient)
265 createSoneWithPost("without-match", "no match")
266 addHttpRequestParameter("query", "recipient")
268 assertThat(this["postHits"], contains<Post>(postWithMatch))
273 fun `searching for a field value finds the correct sone`() {
274 val soneWithProfileField = createSone("sone", "s", "o", "ne")
275 soneWithProfileField.profile.addField("field").value = "value"
276 createSoneWithPost("with-match", "test", sender = soneWithProfileField)
277 createSoneWithPost("without-match", "no match")
278 addHttpRequestParameter("query", "value")
280 assertThat(this["soneHits"], contains(soneWithProfileField))
285 fun `sone hits are paginated correctly`() {
286 core.preferences.postsPerPage = 2
287 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
288 .onEach { addSone(it.id, it) }
289 addHttpRequestParameter("query", "sone")
291 assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
292 assertThat(this["soneHits"], contains(sones[0], sones[2]))
297 fun `sone hits page 2 is shown correctly`() {
298 core.preferences.postsPerPage = 2
299 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
300 .onEach { addSone(it.id, it) }
301 addHttpRequestParameter("query", "sone")
302 addHttpRequestParameter("sonePage", "1")
304 assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
305 assertThat(this["soneHits"], contains(sones[3]))
310 fun `post hits are paginated correctly`() {
311 core.preferences.postsPerPage = 2
312 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
313 addHttpRequestParameter("query", "sone")
315 assertThat(this["postPagination"], isOnPage(0).hasPages(2))
316 assertThat(this["postHits"], contains(sones[0], sones[2]))
321 fun `post hits page 2 is shown correctly`() {
322 core.preferences.postsPerPage = 2
323 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
324 addHttpRequestParameter("query", "sone")
325 addHttpRequestParameter("postPage", "1")
327 assertThat(this["postPagination"], isOnPage(1).hasPages(2))
328 assertThat(this["postHits"], contains(sones[3]))
333 fun `post search results are cached`() {
334 val post = createPost("with-match", "text")
335 val callCounter = AtomicInteger()
336 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
337 val sone = createSoneWithPost(post)
338 addSone("sone", sone)
339 addHttpRequestParameter("query", "text")
341 assertThat(this["postHits"], contains(post))
344 assertThat(callCounter.get(), equalTo(1))
349 fun `post search results are cached for five minutes`() {
350 val post = createPost("with-match", "text")
351 val callCounter = AtomicInteger()
352 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
353 val sone = createSoneWithPost(post)
354 addSone("sone", sone)
355 addHttpRequestParameter("query", "text")
357 assertThat(this["postHits"], contains(post))
359 whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1 )
361 assertThat(callCounter.get(), equalTo(2))
365 @Suppress("UNCHECKED_CAST")
366 private operator fun <T> get(key: String): T? = templateContext[key] as? T