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: WebPageTest2({ template, webInterface -> SearchPage(template, webInterface, ticker) }) {
28 val ticker = mock<Ticker>()
32 fun `page returns correct path`() {
33 assertThat(page.path, equalTo("search.html"))
37 fun `page does not require login`() {
38 assertThat(page.requiresLogin(), equalTo(false))
42 fun `page returns correct title`() {
43 addTranslation("Page.Search.Title", "search page title")
44 assertThat(page.getPageTitle(freenetRequest), equalTo("search page title"))
48 fun `empty query redirects to index page`() {
49 verifyRedirect("index.html")
53 fun `empty search phrases redirect to index page`() {
54 addHttpRequestParameter("query", "\"\"")
55 verifyRedirect("index.html")
59 fun `invalid search phrases redirect to index page`() {
60 addHttpRequestParameter("query", "\"")
61 verifyRedirect("index.html")
65 fun `searching for sone link redirects to view sone page`() {
66 addSone("sone-id", mock<Sone>())
67 addHttpRequestParameter("query", "sone://sone-id")
68 verifyRedirect("viewSone.html?sone=sone-id")
72 fun `searching for sone link without prefix redirects to view sone page`() {
73 addSone("sone-id", mock<Sone>())
74 addHttpRequestParameter("query", "sone-id")
75 verifyRedirect("viewSone.html?sone=sone-id")
79 fun `searching for a post link redirects to post page`() {
80 addPost("post-id", mock<Post>())
81 addHttpRequestParameter("query", "post://post-id")
82 verifyRedirect("viewPost.html?post=post-id")
86 fun `searching for a post ID without prefix redirects to post page`() {
87 addPost("post-id", mock<Post>())
88 addHttpRequestParameter("query", "post-id")
89 verifyRedirect("viewPost.html?post=post-id")
93 fun `searching for a reply link redirects to the post page`() {
94 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
95 addPostReply("reply-id", postReply)
96 addHttpRequestParameter("query", "reply://reply-id")
97 verifyRedirect("viewPost.html?post=post-id")
101 fun `searching for a reply ID redirects to the post page`() {
102 val postReply = mock<PostReply>().apply { whenever(postId).thenReturn("post-id") }
103 addPostReply("reply-id", postReply)
104 addHttpRequestParameter("query", "reply-id")
105 verifyRedirect("viewPost.html?post=post-id")
109 fun `searching for an album link redirects to the image browser`() {
110 addAlbum("album-id", mock<Album>())
111 addHttpRequestParameter("query", "album://album-id")
112 verifyRedirect("imageBrowser.html?album=album-id")
116 fun `searching for an album ID redirects to the image browser`() {
117 addAlbum("album-id", mock<Album>())
118 addHttpRequestParameter("query", "album-id")
119 verifyRedirect("imageBrowser.html?album=album-id")
123 fun `searching for an image link redirects to the image browser`() {
124 addImage("image-id", mock<Image>())
125 addHttpRequestParameter("query", "image://image-id")
126 verifyRedirect("imageBrowser.html?image=image-id")
130 fun `searching for an image ID redirects to the image browser`() {
131 addImage("image-id", mock<Image>())
132 addHttpRequestParameter("query", "image-id")
133 verifyRedirect("imageBrowser.html?image=image-id")
136 private fun createReply(text: String, postId: String? = null, sone: Sone? = null) = mock<PostReply>().apply {
137 whenever(this.text).thenReturn(text)
138 postId?.run { whenever(this@apply.postId).thenReturn(postId) }
139 sone?.run { whenever(this@apply.sone).thenReturn(sone) }
142 private fun createPost(id: String, text: String) = mock<Post>().apply {
143 whenever(this.id).thenReturn(id)
144 whenever(recipient).thenReturn(absent())
145 whenever(this.text).thenReturn(text)
148 private fun createSoneWithPost(post: Post, sone: Sone? = null) = sone?.apply {
149 whenever(posts).thenReturn(listOf(post))
150 } ?: mock<Sone>().apply {
151 whenever(posts).thenReturn(listOf(post))
152 whenever(profile).thenReturn(Profile(this))
156 fun `searching for a single word finds the post`() {
157 val postWithMatch = createPost("post-with-match", "the word here")
158 val postWithoutMatch = createPost("post-without-match", "no match here")
159 val soneWithMatch = createSoneWithPost(postWithMatch)
160 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
161 addSone("sone-with-match", soneWithMatch)
162 addSone("sone-without-match", soneWithoutMatch)
163 addHttpRequestParameter("query", "word")
165 assertThat(this["postHits"], contains<Post>(postWithMatch))
170 fun `searching for a single word locates word in reply`() {
171 val postWithMatch = createPost("post-with-match", "no match here")
172 val postWithoutMatch = createPost("post-without-match", "no match here")
173 val soneWithMatch = createSoneWithPost(postWithMatch)
174 val soneWithoutMatch = createSoneWithPost(postWithoutMatch)
175 val replyWithMatch = createReply("the word here", "post-with-match", soneWithMatch)
176 val replyWithoutMatch = createReply("no match here", "post-without-match", soneWithoutMatch)
177 addPostReply("reply-with-match", replyWithMatch)
178 addPostReply("reply-without-match", replyWithoutMatch)
179 addSone("sone-with-match", soneWithMatch)
180 addSone("sone-without-match", soneWithoutMatch)
181 addHttpRequestParameter("query", "word")
183 assertThat(this["postHits"], contains<Post>(postWithMatch))
187 private fun createSoneWithPost(idPostfix: String, text: String, recipient: Sone? = null, sender: Sone? = null) =
188 createPost("post-$idPostfix", text, recipient).apply {
189 addSone("sone-$idPostfix", createSoneWithPost(this, sender))
193 fun `earlier matches score higher than later matches`() {
194 val postWithEarlyMatch = createSoneWithPost("with-early-match", "optional match")
195 val postWithLaterMatch = createSoneWithPost("with-later-match", "match that is optional")
196 addHttpRequestParameter("query", "optional ")
198 assertThat(this["postHits"], contains<Post>(postWithEarlyMatch, postWithLaterMatch))
203 fun `searching for required word does not return posts without that word`() {
204 val postWithRequiredMatch = createSoneWithPost("with-required-match", "required match")
205 createPost("without-required-match", "not a match")
206 addHttpRequestParameter("query", "+required ")
208 assertThat(this["postHits"], contains<Post>(postWithRequiredMatch))
213 fun `searching for forbidden word does not return posts with that word`() {
214 createSoneWithPost("with-forbidden-match", "forbidden match")
215 val postWithoutForbiddenMatch = createSoneWithPost("without-forbidden-match", "not a match")
216 addHttpRequestParameter("query", "match -forbidden")
218 assertThat(this["postHits"], contains<Post>(postWithoutForbiddenMatch))
223 fun `searching for a plus sign searches for optional plus sign`() {
224 val postWithMatch = createSoneWithPost("with-match", "with + match")
225 createSoneWithPost("without-match", "without match")
226 addHttpRequestParameter("query", "+")
228 assertThat(this["postHits"], contains<Post>(postWithMatch))
233 fun `searching for a minus sign searches for optional minus sign`() {
234 val postWithMatch = createSoneWithPost("with-match", "with - match")
235 createSoneWithPost("without-match", "without match")
236 addHttpRequestParameter("query", "-")
238 assertThat(this["postHits"], contains<Post>(postWithMatch))
242 private fun createPost(id: String, text: String, recipient: Sone?) = mock<Post>().apply {
243 whenever(this.id).thenReturn(id)
244 val recipientId = recipient?.id
245 whenever(this.recipientId).thenReturn(recipientId.asOptional())
246 whenever(this.recipient).thenReturn(recipient.asOptional())
247 whenever(this.text).thenReturn(text)
250 private fun createSone(id: String, firstName: String? = null, middleName: String? = null, lastName: String? = null) = mock<Sone>().apply {
251 whenever(this.id).thenReturn(id)
252 whenever(this.name).thenReturn(id)
253 whenever(this.profile).thenReturn(Profile(this).apply {
254 this.firstName = firstName
255 this.middleName = middleName
256 this.lastName = lastName
261 fun `searching for a recipient finds the correct post`() {
262 val recipient = createSone("recipient", "reci", "pi", "ent")
263 val postWithMatch = createSoneWithPost("with-match", "test", recipient)
264 createSoneWithPost("without-match", "no match")
265 addHttpRequestParameter("query", "recipient")
267 assertThat(this["postHits"], contains<Post>(postWithMatch))
272 fun `searching for a field value finds the correct sone`() {
273 val soneWithProfileField = createSone("sone", "s", "o", "ne")
274 soneWithProfileField.profile.addField("field").value = "value"
275 createSoneWithPost("with-match", "test", sender = soneWithProfileField)
276 createSoneWithPost("without-match", "no match")
277 addHttpRequestParameter("query", "value")
279 assertThat(this["soneHits"], contains(soneWithProfileField))
284 fun `sone hits are paginated correctly`() {
285 core.preferences.postsPerPage = 2
286 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
287 .onEach { addSone(it.id, it) }
288 addHttpRequestParameter("query", "sone")
290 assertThat(this["sonePagination"], isOnPage(0).hasPages(2))
291 assertThat(this["soneHits"], contains(sones[0], sones[2]))
296 fun `sone hits page 2 is shown correctly`() {
297 core.preferences.postsPerPage = 2
298 val sones = listOf(createSone("1Sone"), createSone("Other1"), createSone("22Sone"), createSone("333Sone"), createSone("Other2"))
299 .onEach { addSone(it.id, it) }
300 addHttpRequestParameter("query", "sone")
301 addHttpRequestParameter("sonePage", "1")
303 assertThat(this["sonePagination"], isOnPage(1).hasPages(2))
304 assertThat(this["soneHits"], contains(sones[3]))
309 fun `post hits are paginated correctly`() {
310 core.preferences.postsPerPage = 2
311 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
312 addHttpRequestParameter("query", "sone")
314 assertThat(this["postPagination"], isOnPage(0).hasPages(2))
315 assertThat(this["postHits"], contains(sones[0], sones[2]))
320 fun `post hits page 2 is shown correctly`() {
321 core.preferences.postsPerPage = 2
322 val sones = listOf(createSoneWithPost("match1", "1Sone"), createSoneWithPost("no-match1", "Other1"), createSoneWithPost("match2", "22Sone"), createSoneWithPost("match3", "333Sone"), createSoneWithPost("no-match2", "Other2"))
323 addHttpRequestParameter("query", "sone")
324 addHttpRequestParameter("postPage", "1")
326 assertThat(this["postPagination"], isOnPage(1).hasPages(2))
327 assertThat(this["postHits"], contains(sones[3]))
332 fun `post search results are cached`() {
333 val post = createPost("with-match", "text")
334 val callCounter = AtomicInteger()
335 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
336 val sone = createSoneWithPost(post)
337 addSone("sone", sone)
338 addHttpRequestParameter("query", "text")
340 assertThat(this["postHits"], contains(post))
343 assertThat(callCounter.get(), equalTo(1))
348 fun `post search results are cached for five minutes`() {
349 val post = createPost("with-match", "text")
350 val callCounter = AtomicInteger()
351 whenever(post.text).thenAnswer { callCounter.incrementAndGet(); "text" }
352 val sone = createSoneWithPost(post)
353 addSone("sone", sone)
354 addHttpRequestParameter("query", "text")
356 assertThat(this["postHits"], contains(post))
358 whenever(ticker.read()).thenReturn(TimeUnit.MINUTES.toNanos(5) + 1)
360 assertThat(callCounter.get(), equalTo(2))
364 @Suppress("UNCHECKED_CAST")
365 private operator fun <T> get(key: String): T? = templateContext[key] as? T