🚸 Improve text extraction even further
[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 `element loader can extract first paragraph from real-world example`() {
155                 runWithCallback(textKey) { elementLoader, _, callback, _ ->
156                         callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader5.html"))
157                         val linkedElement = elementLoader.loadElement(textKey)
158                         assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
159                                 hasEntry("type", "html"), hasEntry("title", "Some Nice Page Title"),
160                                 hasEntry("description", "This is the first paragraph of the very nice freesite.")
161                         )))
162                 }
163         }
164
165         @Test
166         fun `image is not loaded again after it failed`() {
167                 runWithCallback(IMAGE_ID) { elementLoader, _, callback, _ ->
168                         elementLoader.loadElement(IMAGE_ID)
169                         callback.failed(freenetURI)
170                         assertThat(elementLoader.loadElement(IMAGE_ID).failed, equalTo(true))
171                 }
172         }
173
174         @Test
175         fun `image is loaded again after failure cache is expired`() {
176                 runWithCallback(IMAGE_ID, createTicker(1, MINUTES.toNanos(31))) { elementLoader, _, callback, _ ->
177                         elementLoader.loadElement(IMAGE_ID)
178                         callback.failed(freenetURI)
179                         val linkedElement = elementLoader.loadElement(IMAGE_ID)
180                         assertThat(linkedElement.failed, equalTo(false))
181                         assertThat(linkedElement.loading, equalTo(true))
182                 }
183         }
184
185         private fun read(resource: String): ByteArray =
186                 javaClass.getResourceAsStream(resource)?.use { input ->
187                         ByteArrayOutputStream().use {
188                                 input.copyTo(it)
189                                 it
190                         }.toByteArray()
191                 } ?: ByteArray(0)
192
193         @get:Rule
194         val silencedLoggin = silencedLogging()
195
196 }
197
198 private fun runWithCallback(requestUri: String, ticker: Ticker = createTicker(), callbackAction: (elementLoader: ElementLoader, linkedElement: LinkedElement, callback: BackgroundFetchCallback, fetchedUris: List<FreenetURI>) -> Unit) {
199         val fetchedUris = mutableListOf<FreenetURI>()
200         val callback = AtomicReference<BackgroundFetchCallback>()
201         val freenetInterface = overrideStartFetch { uri, backgroundFetchCallback ->
202                 fetchedUris += uri
203                 callback.set(backgroundFetchCallback)
204         }
205         val elementLoader = DefaultElementLoader(freenetInterface, ticker)
206         val linkedElement = elementLoader.loadElement(requestUri)
207         callbackAction(elementLoader, linkedElement, callback.get(), fetchedUris)
208 }
209
210 private fun overrideStartFetch(action: (FreenetURI, BackgroundFetchCallback) -> Unit) = object : FreenetInterface(null, null, null, null, null, dummyHighLevelSimpleClientCreator) {
211         override fun startFetch(uri: FreenetURI, backgroundFetchCallback: BackgroundFetchCallback) {
212                 action(uri, backgroundFetchCallback)
213         }
214 }
215
216 private fun createTicker(vararg times: Long = LongArray(1) { 1 }) = object : Ticker() {
217         private var counter = 0
218         override fun read() =
219                 times[min(times.size - 1, counter)]
220                         .also { counter++ }
221 }
222
223 private fun isLinkedElement(link: Matcher<String> = everything(), properties: Matcher<Map<String, Any?>> = everything(), failed: Matcher<Boolean> = everything(), loading: Matcher<Boolean> = everything()) = object : TypeSafeDiagnosingMatcher<LinkedElement>() {
224         override fun matchesSafely(item: LinkedElement, mismatchDescription: Description) =
225                 handleMatcher(link, item.link, mismatchDescription) &&
226                                 handleMatcher(properties, item.properties, mismatchDescription) &&
227                                 handleMatcher(failed, item.failed, mismatchDescription) &&
228                                 handleMatcher(loading, item.loading, mismatchDescription)
229
230         override fun describeTo(description: Description) {
231                 description.appendText("is linked element for key matching ").appendValue(link)
232                         .appendText(", properties matching ").appendValue(properties)
233                         .appendText(", failed matching ").appendValue(failed)
234                         .appendText(", loading matching ").appendValue(loading)
235         }
236 }
237
238 private const val IMAGE_ID = "KSK@gpl.png"
239 private val freenetURI = FreenetURI(IMAGE_ID)
240 private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg"
241 private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg"
242 private const val textKey = "KSK@gpl.html"
243 private const val sizeOkay = 2097152L
244 private const val sizeNotOkay = sizeOkay + 1