1 package net.pterodactylus.sone.core
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.containsString
13 import org.hamcrest.Matchers.equalTo
14 import org.hamcrest.Matchers.hasEntry
15 import org.hamcrest.Matchers.not
16 import org.hamcrest.TypeSafeDiagnosingMatcher
19 import java.io.ByteArrayOutputStream
20 import java.util.concurrent.TimeUnit.MINUTES
21 import java.util.concurrent.atomic.AtomicReference
22 import java.util.logging.Handler
23 import java.util.logging.Level.ALL
24 import java.util.logging.LogRecord
25 import java.util.logging.Logger
26 import kotlin.math.min
29 * Unit test for [DefaultElementLoaderTest].
31 class DefaultElementLoaderTest {
34 fun `image loader starts request for link that is not known`() {
35 runWithCallback(IMAGE_ID) { _, _, _, fetchedUris ->
36 assertThat(fetchedUris, contains(freenetURI))
41 fun `element loader only starts request once`() {
42 runWithCallback(IMAGE_ID) { elementLoader, _, _, fetchedUris ->
43 elementLoader.loadElement(IMAGE_ID)
44 assertThat(fetchedUris, contains(freenetURI))
49 fun `element loader returns loading element on first call`() {
50 runWithCallback(IMAGE_ID) { _, linkedElement, _, _ ->
51 assertThat(linkedElement.loading, equalTo(true))
56 fun `element loader does not cancel on image mime type with 2 mib size`() {
57 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
58 assertThat(callback.shouldCancel(freenetURI, "image/png", sizeOkay), equalTo(false))
59 assertThat(loggedRecords.map(LogRecord::getMessage), not(contains(containsString("Canceling download"))))
64 fun `element loader does cancel on image mime type with more than 2 mib size`() {
65 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
66 assertThat(callback.shouldCancel(freenetURI, "image/png", sizeNotOkay), equalTo(true))
67 assertThat(loggedRecords.map(LogRecord::getMessage), contains(containsString("Canceling download")))
72 fun `element loader does cancel on audio mime type`() {
73 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
74 assertThat(callback.shouldCancel(freenetURI, "audio/mpeg", sizeOkay), equalTo(true))
75 assertThat(loggedRecords.map(LogRecord::getMessage), contains(containsString("Canceling download")))
80 fun `element loader does cancel on video mime type`() {
81 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
82 assertThat(callback.shouldCancel(freenetURI, "video/mkv", sizeOkay), equalTo(true))
83 assertThat(loggedRecords.map(LogRecord::getMessage), contains(containsString("Canceling download")))
88 fun `element loader does cancel on text mime type`() {
89 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
90 assertThat(callback.shouldCancel(freenetURI, "text/plain", sizeOkay), equalTo(true))
91 assertThat(loggedRecords.map(LogRecord::getMessage), contains(containsString("Canceling download")))
96 fun `element loader does not cancel on text html mime type`() {
97 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
98 assertThat(callback.shouldCancel(freenetURI, "text/html", sizeOkay), equalTo(false))
99 assertThat(loggedRecords.map(LogRecord::getMessage), not(contains(containsString("Canceling download"))))
104 fun `image loader can load image`() {
105 runWithCallback(decomposedKey) { elementLoader, _, callback, _ ->
106 callback.loaded(FreenetURI(normalizedKey), "image/png", read("/static/images/unknown-image-0.png"))
107 val linkedElement = elementLoader.loadElement(decomposedKey)
108 assertThat(linkedElement, isLinkedElement(equalTo(normalizedKey), allOf(
109 hasEntry("type", "image"), hasEntry("size", 2451), hasEntry("sizeHuman", "2 KiB"),
115 fun `element loader logs information about downloaded image`() {
116 runWithCallback(decomposedKey) { _, _, callback, _ ->
117 callback.loaded(FreenetURI(normalizedKey), "image/png", read("/static/images/unknown-image-0.png"))
118 assertThat(loggedRecords.map(LogRecord::getMessage), contains(allOf(
119 containsString(normalizedKey), containsString("2451")
125 fun `element loader can extract description from description header`() {
126 runWithCallback(textKey) { elementLoader, _, callback, _ ->
127 callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader.html"))
128 val linkedElement = elementLoader.loadElement(textKey)
129 assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
130 hasEntry("type", "html"), hasEntry("size", 266), hasEntry("sizeHuman", "266 B"),
131 hasEntry("title", "Some Nice Page Title"),
132 hasEntry("description", "This is an example of a very nice freesite.")
138 fun `element loader logs information from downloaded freesite`() {
139 runWithCallback(textKey) { _, _, callback, _ ->
140 callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader.html"))
141 assertThat(loggedRecords.map(LogRecord::getMessage), contains(allOf(
142 containsString(textKey), containsString("Some Nice Page Title"),
143 containsString("This is an example of a very nice freesite.")
149 fun `element loader can extract description from first non-heading paragraph`() {
150 runWithCallback(textKey) { elementLoader, _, callback, _ ->
151 callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader2.html"))
152 val linkedElement = elementLoader.loadElement(textKey)
153 assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
154 hasEntry("type", "html"), hasEntry("size", 185), hasEntry("sizeHuman", "185 B"),
155 hasEntry("title", "Some Nice Page Title"),
156 hasEntry("description", "This is the first paragraph of the very nice freesite.")
162 fun `element loader can extract description if html is more complicated`() {
163 runWithCallback(textKey) { elementLoader, _, callback, _ ->
164 callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader3.html"))
165 val linkedElement = elementLoader.loadElement(textKey)
166 assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
167 hasEntry("type", "html"), hasEntry("size", 204), hasEntry("sizeHuman", "204 B"),
168 hasEntry("title", "Some Nice Page Title"),
169 hasEntry("description", "This is the first paragraph of the very nice freesite.")
175 fun `element loader can not extract title if it is missing`() {
176 runWithCallback(textKey) { elementLoader, _, callback, _ ->
177 callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader4.html"))
178 val linkedElement = elementLoader.loadElement(textKey)
179 assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
180 hasEntry("type", "html"), hasEntry("size", 229), hasEntry("sizeHuman", "229 B"), hasEntry("title", null),
181 hasEntry("description", "This is an example of a very nice freesite.")
187 fun `element loader can extract first paragraph from real-world example`() {
188 runWithCallback(textKey) { elementLoader, _, callback, _ ->
189 callback.loaded(FreenetURI(textKey), "text/html; charset=UTF-8", read("element-loader5.html"))
190 val linkedElement = elementLoader.loadElement(textKey)
191 assertThat(linkedElement, isLinkedElement(equalTo(textKey), allOf(
192 hasEntry("type", "html"), hasEntry("title", "Some Nice Page Title"),
193 hasEntry("description", "This is the first paragraph of the very nice freesite.")
199 fun `image is not loaded again after it failed`() {
200 runWithCallback(IMAGE_ID) { elementLoader, _, callback, _ ->
201 elementLoader.loadElement(IMAGE_ID)
202 callback.failed(freenetURI)
203 assertThat(elementLoader.loadElement(IMAGE_ID).failed, equalTo(true))
208 fun `element loading failure is logged`() {
209 runWithCallback(IMAGE_ID) { _, _, callback, _ ->
210 callback.failed(freenetURI)
211 assertThat(loggedRecords.map(LogRecord::getMessage), contains(containsString("Download failed")))
216 fun `image is loaded again after failure cache is expired`() {
217 runWithCallback(IMAGE_ID, createTicker(1, MINUTES.toNanos(31))) { elementLoader, _, callback, _ ->
218 elementLoader.loadElement(IMAGE_ID)
219 callback.failed(freenetURI)
220 val linkedElement = elementLoader.loadElement(IMAGE_ID)
221 assertThat(linkedElement.failed, equalTo(false))
222 assertThat(linkedElement.loading, equalTo(true))
226 private fun read(resource: String): ByteArray =
227 javaClass.getResourceAsStream(resource)?.use { input ->
228 ByteArrayOutputStream().use {
235 val silencedLoggin = silencedLogging()
237 private val loggedRecords = mutableListOf<LogRecord>()
240 Logger.getLogger(DefaultElementLoader::class.qualifiedName)
241 .apply { level = ALL }
243 addHandler(object : Handler() {
244 override fun publish(record: LogRecord) {
245 loggedRecords += record
248 override fun flush() = Unit
249 override fun close() = Unit
256 private fun runWithCallback(requestUri: String, ticker: Ticker = createTicker(), callbackAction: (elementLoader: ElementLoader, linkedElement: LinkedElement, callback: BackgroundFetchCallback, fetchedUris: List<FreenetURI>) -> Unit) {
257 val fetchedUris = mutableListOf<FreenetURI>()
258 val callback = AtomicReference<BackgroundFetchCallback>()
259 val freenetInterface = overrideStartFetch { uri, backgroundFetchCallback ->
261 callback.set(backgroundFetchCallback)
263 val elementLoader = DefaultElementLoader(freenetInterface, ticker)
264 val linkedElement = elementLoader.loadElement(requestUri)
265 callbackAction(elementLoader, linkedElement, callback.get(), fetchedUris)
268 private fun overrideStartFetch(action: (FreenetURI, BackgroundFetchCallback) -> Unit) = object : FreenetInterface(null, null, null, null, null, dummyHighLevelSimpleClientCreator) {
269 override fun startFetch(uri: FreenetURI, backgroundFetchCallback: BackgroundFetchCallback) {
270 action(uri, backgroundFetchCallback)
274 private fun createTicker(vararg times: Long = LongArray(1) { 1 }) = object : Ticker() {
275 private var counter = 0
276 override fun read() =
277 times[min(times.size - 1, counter)]
281 private fun isLinkedElement(link: Matcher<String> = everything(), properties: Matcher<Map<String, Any?>> = everything(), failed: Matcher<Boolean> = everything(), loading: Matcher<Boolean> = everything()) = object : TypeSafeDiagnosingMatcher<LinkedElement>() {
282 override fun matchesSafely(item: LinkedElement, mismatchDescription: Description) =
283 handleMatcher(link, item.link, mismatchDescription) &&
284 handleMatcher(properties, item.properties, mismatchDescription) &&
285 handleMatcher(failed, item.failed, mismatchDescription) &&
286 handleMatcher(loading, item.loading, mismatchDescription)
288 override fun describeTo(description: Description) {
289 description.appendText("is linked element for key matching ").appendValue(link)
290 .appendText(", properties matching ").appendValue(properties)
291 .appendText(", failed matching ").appendValue(failed)
292 .appendText(", loading matching ").appendValue(loading)
296 private const val IMAGE_ID = "KSK@gpl.png"
297 private val freenetURI = FreenetURI(IMAGE_ID)
298 private const val decomposedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/fru%CC%88hstu%CC%88ck.jpg"
299 private const val normalizedKey = "CHK@DCiVgTWW9nnWHJc9EVwtFJ6jAfBSVyy~rgiPvhUKbS4,mNY85V0x7dYcv7SnEYo1PCC6y2wNWMDNt-y9UWQx9fI,AAMC--8/frühstück.jpg"
300 private const val textKey = "KSK@gpl.html"
301 private const val sizeOkay = 2097152L
302 private const val sizeNotOkay = sizeOkay + 1