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.equalTo
13 import org.hamcrest.Matchers.hasEntry
14 import org.hamcrest.TypeSafeDiagnosingMatcher
17 import java.io.ByteArrayOutputStream
18 import java.util.concurrent.TimeUnit.MINUTES
19 import java.util.concurrent.atomic.AtomicReference
20 import kotlin.math.min
23 * Unit test for [DefaultElementLoaderTest].
25 class DefaultElementLoaderTest {
28 fun `image loader starts request for link that is not known`() {
29 runWithCallback(IMAGE_ID) { _, _, _, fetchedUris ->
30 assertThat(fetchedUris, contains(freenetURI))
35 fun `element loader only starts request once`() {
36 runWithCallback(IMAGE_ID) { elementLoader, _, _, fetchedUris ->
37 elementLoader.loadElement(IMAGE_ID)
38 assertThat(fetchedUris, contains(freenetURI))
43 fun `element loader returns loading element on first call`() {
44 runWithCallback(IMAGE_ID) { _, linkedElement, _, _ ->
45 assertThat(linkedElement.loading, equalTo(true))
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))
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))
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))
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))
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))
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))
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"),
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.")
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.")
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.")
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.")
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.")
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))
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))
185 private fun read(resource: String): ByteArray =
186 javaClass.getResourceAsStream(resource)?.use { input ->
187 ByteArrayOutputStream().use {
194 val silencedLoggin = silencedLogging()
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 ->
203 callback.set(backgroundFetchCallback)
205 val elementLoader = DefaultElementLoader(freenetInterface, ticker)
206 val linkedElement = elementLoader.loadElement(requestUri)
207 callbackAction(elementLoader, linkedElement, callback.get(), fetchedUris)
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)
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)]
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)
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)
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