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 `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))
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))
173 private fun read(resource: String): ByteArray =
174 javaClass.getResourceAsStream(resource)?.use { input ->
175 ByteArrayOutputStream().use {
182 val silencedLoggin = silencedLogging()
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 ->
191 callback.set(backgroundFetchCallback)
193 val elementLoader = DefaultElementLoader(freenetInterface, ticker)
194 val linkedElement = elementLoader.loadElement(requestUri)
195 callbackAction(elementLoader, linkedElement, callback.get(), fetchedUris)
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)
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)]
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)
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)
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