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