8a3d40f396aba65d901b8296f009384f6bcaf6b8
[Sone.git] / src / test / kotlin / net / pterodactylus / sone / core / DefaultElementLoaderTest.kt
1 package net.pterodactylus.sone.core
2
3 import com.google.common.base.Ticker
4 import freenet.keys.FreenetURI
5 import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
6 import net.pterodactylus.sone.test.*
7 import org.hamcrest.Description
8 import org.hamcrest.Matcher
9 import org.hamcrest.MatcherAssert.assertThat
10 import org.hamcrest.Matchers.allOf
11 import org.hamcrest.Matchers.contains
12 import org.hamcrest.Matchers.equalTo
13 import org.hamcrest.Matchers.hasEntry
14 import org.hamcrest.TypeSafeDiagnosingMatcher
15 import org.junit.Rule
16 import org.junit.Test
17 import java.io.ByteArrayOutputStream
18 import java.util.concurrent.TimeUnit.MINUTES
19 import java.util.concurrent.atomic.AtomicReference
20 import kotlin.math.min
21
22 /**
23  * Unit test for [DefaultElementLoaderTest].
24  */
25 class DefaultElementLoaderTest {
26
27         @Test
28         fun `image loader starts request for link that is not known`() {
29                 runWithCallback(IMAGE_ID) { _, _, _, fetchedUris ->
30                         assertThat(fetchedUris, contains(freenetURI))
31                 }
32         }
33
34         @Test
35         fun `element loader only starts request once`() {
36                 runWithCallback(IMAGE_ID) { elementLoader, _, _, fetchedUris ->
37                         elementLoader.loadElement(IMAGE_ID)
38                         assertThat(fetchedUris, contains(freenetURI))
39                 }
40         }
41
42         @Test
43         fun `element loader returns loading element on first call`() {
44                 runWithCallback(IMAGE_ID) { _, linkedElement, _, _ ->
45                         assertThat(linkedElement.loading, equalTo(true))
46                 }
47         }
48
49         @Test
50         fun `element loader does not cancel on image mime type with 2 mib size`() {
51                 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
52                         assertThat(callback.shouldCancel(freenetURI, "image/png", sizeOkay), equalTo(false))
53                 }
54         }
55
56         @Test
57         fun `element loader does cancel on image mime type with more than 2 mib size`() {
58                 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
59                         assertThat(callback.shouldCancel(freenetURI, "image/png", sizeNotOkay), equalTo(true))
60                 }
61         }
62
63         @Test
64         fun `element loader does cancel on audio mime type`() {
65                 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
66                         assertThat(callback.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), equalTo(true))
67                 }
68         }
69
70         @Test
71         fun `element loader does cancel on video mime type`() {
72                 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
73                         assertThat(callback.shouldCancel(freenetURI, "video/mkv", sizeOkay), equalTo(true))
74                 }
75         }
76
77         @Test
78         fun `element loader does cancel on text mime type`() {
79                 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
80                         assertThat(callback.shouldCancel(freenetURI, "text/plain", sizeOkay), equalTo(true))
81                 }
82         }
83
84         @Test
85         fun `element loader does not cancel on text html mime type`() {
86                 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
87                         assertThat(callback.shouldCancel(freenetURI, "text/html", sizeOkay), equalTo(false))
88                 }
89         }
90
91         @Test
92         fun `image loader can load image`() {
93                 runWithCallback(decomposedKey) { elementLoader, _, callback, _ ->
94                         callback.loaded(FreenetURI(normalizedKey), "image/png", read("/static/images/unknown-image-0.png"))
95                         val linkedElement = elementLoader.loadElement(decomposedKey)
96                         assertThat(linkedElement, isLinkedElement(equalTo(normalizedKey), allOf(
97                                 hasEntry("type", "image"), hasEntry("size", 2451), hasEntry("sizeHuman", "2 KiB"),
98                         )))
99                 }
100         }
101
102         @Test
103         fun `element loader can extract description from description header`() {
104                 runWithCallback(textKey) { elementLoader, _, callback, _ ->
105                         callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader.html"))
106                         val linkedElement = elementLoader.loadElement(textKey)
107                         assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
108                                 hasEntry("type", "html"), hasEntry("size", 266), hasEntry("sizeHuman", "266 B"),
109                                 hasEntry("title", "Some Nice Page Title"),
110                                 hasEntry("description", "This is an example of a very nice freesite.")
111                         )))
112                 }
113         }
114
115         @Test
116         fun `element loader can extract description from first non-heading paragraph`() {
117                 runWithCallback(textKey) { elementLoader, _, callback, _ ->
118                         callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader2.html"))
119                         val linkedElement = elementLoader.loadElement(textKey)
120                         assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
121                                 hasEntry("type", "html"), hasEntry("size", 185), hasEntry("sizeHuman", "185 B"),
122                                 hasEntry("title", "Some Nice Page Title"),
123                                 hasEntry("description", "This is the first paragraph of the very nice freesite.")
124                         )))
125                 }
126         }
127
128         @Test
129         fun `element loader can extract description if html is more complicated`() {
130                 runWithCallback(textKey) { elementLoader, _, callback, _ ->
131                         callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader3.html"))
132                         val linkedElement = elementLoader.loadElement(textKey)
133                         assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
134                                 hasEntry("type", "html"), hasEntry("size", 204), hasEntry("sizeHuman", "204 B"),
135                                 hasEntry("title", "Some Nice Page Title"),
136                                 hasEntry("description", "This is the first paragraph of the very nice freesite.")
137                         )))
138                 }
139         }
140
141         @Test
142         fun `element loader can not extract title if it is missing`() {
143                 runWithCallback(textKey) { elementLoader, _, callback, _ ->
144                         callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader4.html"))
145                         val linkedElement = elementLoader.loadElement(textKey)
146                         assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
147                                 hasEntry("type", "html"), hasEntry("size", 229), hasEntry("sizeHuman", "229 B"), hasEntry("title", null),
148                                 hasEntry("description", "This is an example of a very nice freesite.")
149                         )))
150                 }
151         }
152
153         @Test
154         fun `image is not loaded again after it failed`() {
155                 runWithCallback(IMAGE_ID) { elementLoader, _, callback, _ ->
156                         elementLoader.loadElement(IMAGE_ID)
157                         callback.failed(freenetURI)
158                         assertThat(elementLoader.loadElement(IMAGE_ID).failed, equalTo(true))
159                 }
160         }
161
162         @Test
163         fun `image is loaded again after failure cache is expired`() {
164                 runWithCallback(IMAGE_ID, createTicker(1, MINUTES.toNanos(31))) { elementLoader, _, callback, _ ->
165                         elementLoader.loadElement(IMAGE_ID)
166                         callback.failed(freenetURI)
167                         val linkedElement = elementLoader.loadElement(IMAGE_ID)
168                         assertThat(linkedElement.failed, equalTo(false))
169                         assertThat(linkedElement.loading, equalTo(true))
170                 }
171         }
172
173         private fun read(resource: String): ByteArray =
174                 javaClass.getResourceAsStream(resource)?.use { input ->
175                         ByteArrayOutputStream().use {
176                                 input.copyTo(it)
177                                 it
178                         }.toByteArray()
179                 } ?: ByteArray(0)
180
181         @get:Rule
182         val silencedLoggin = silencedLogging()
183
184 }
185
186 private fun runWithCallback(requestUri: String, ticker: Ticker = createTicker(), callbackAction: (elementLoader: ElementLoader, linkedElement: LinkedElement, callback: BackgroundFetchCallback, fetchedUris: List<FreenetURI>) -> Unit) {
187         val fetchedUris = mutableListOf<FreenetURI>()
188         val callback = AtomicReference<BackgroundFetchCallback>()
189         val freenetInterface = overrideStartFetch { uri, backgroundFetchCallback ->
190                 fetchedUris += uri
191                 callback.set(backgroundFetchCallback)
192         }
193         val elementLoader = DefaultElementLoader(freenetInterface, ticker)
194         val linkedElement = elementLoader.loadElement(requestUri)
195         callbackAction(elementLoader, linkedElement, callback.get(), fetchedUris)
196 }
197
198 private fun overrideStartFetch(action: (FreenetURI, BackgroundFetchCallback) -> Unit) = object : FreenetInterface(null, null, null, null, null, dummyHighLevelSimpleClientCreator) {
199         override fun startFetch(uri: FreenetURI, backgroundFetchCallback: BackgroundFetchCallback) {
200                 action(uri, backgroundFetchCallback)
201         }
202 }
203
204 private fun createTicker(vararg times: Long = LongArray(1) { 1 }) = object : Ticker() {
205         private var counter = 0
206         override fun read() =
207                 times[min(times.size - 1, counter)]
208                         .also { counter++ }
209 }
210
211 private fun isLinkedElement(link: Matcher<String> = everything(), properties: Matcher<Map<String, Any?>> = everything(), failed: Matcher<Boolean> = everything(), loading: Matcher<Boolean> = everything()) = object : TypeSafeDiagnosingMatcher<LinkedElement>() {
212         override fun matchesSafely(item: LinkedElement, mismatchDescription: Description) =
213                 handleMatcher(link, item.link, mismatchDescription) &&
214                                 handleMatcher(properties, item.properties, mismatchDescription) &&
215                                 handleMatcher(failed, item.failed, mismatchDescription) &&
216                                 handleMatcher(loading, item.loading, mismatchDescription)
217
218         override fun describeTo(description: Description) {
219                 description.appendText("is linked element for key matching ").appendValue(link)
220                         .appendText(", properties matching ").appendValue(properties)
221                         .appendText(", failed matching ").appendValue(failed)
222                         .appendText(", loading matching ").appendValue(loading)
223         }
224 }
225
226 private const val IMAGE_ID = "KSK@gpl.png"
227 private val freenetURI = FreenetURI(IMAGE_ID)
228 private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg"
229 private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg"
230 private const val textKey = "KSK@gpl.html"
231 private const val sizeOkay = 2097152L
232 private const val sizeNotOkay = sizeOkay + 1