mirror of
https://github.com/readrops/Readrops.git
synced 2025-02-02 19:56:50 +01:00
Add data source for local rss, with tests
This commit is contained in:
parent
355b5a4375
commit
51cea7c4c2
@ -56,8 +56,13 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1'
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.8.1'
|
||||
|
||||
implementation('com.squareup.retrofit2:retrofit:2.7.1') {
|
||||
exclude group: 'okhttp3', module: 'okhttp3'
|
||||
}
|
||||
implementation('com.squareup.retrofit2:converter-moshi:2.7.1') {
|
||||
exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2
|
||||
}
|
||||
|
57
api/src/androidTest/assets/localfeed/rss_feed.xml
Normal file
57
api/src/androidTest/assets/localfeed/rss_feed.xml
Normal file
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Hacker News</title>
|
||||
<link>https://news.ycombinator.com/</link>
|
||||
<description>Links for the intellectually curious, ranked by readers.</description>
|
||||
<item>
|
||||
<title>Africa declared free of wild polio</title>
|
||||
<link>https://www.bbc.com/news/world-africa-53887947</link>
|
||||
<pubDate>Tue, 25 Aug 2020 17:15:49 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24273602</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273602">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Palantir S-1</title>
|
||||
<link>https://www.sec.gov/Archives/edgar/data/1321655/000119312520230013/d904406ds1.htm</link>
|
||||
<pubDate>Tue, 25 Aug 2020 21:03:42 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24276086</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24276086">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Openwifi: Linux mac80211 compatible full-stack 802.11/Wi-Fi design based on SDR</title>
|
||||
<link>https://github.com/open-sdr/openwifi</link>
|
||||
<pubDate>Tue, 25 Aug 2020 17:45:19 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24273919</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273919">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Syllabus for Eric's PhD Students</title>
|
||||
<link>https://docs.google.com/document/d/11D3kHElzS2HQxTwPqcaTnU5HCJ8WGE5brTXI4KLf4dM/edit</link>
|
||||
<pubDate>Tue, 25 Aug 2020 18:55:12 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24274699</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24274699">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>WebBundles harmful to content blocking, security tools, and the open web</title>
|
||||
<link>https://brave.com/webbundles-harmful-to-content-blocking-security-tools-and-the-open-web/</link>
|
||||
<pubDate>Tue, 25 Aug 2020 19:18:50 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24274968</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24274968">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Zappos CEO Tony Hsieh is stepping down after 21 years</title>
|
||||
<link>https://footwearnews.com/2020/business/executive-moves/zappos-ceo-tony-hsieh-steps-down-1203045974/</link>
|
||||
<pubDate>Tue, 25 Aug 2020 06:11:42 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24268522</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24268522">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Evgeny Kuznetsov practices with Bauer stick that has hole in the blade</title>
|
||||
<link>https://russianmachineneverbreaks.com/2020/07/17/evgeny-kuznetsov-practices-with-bauer-stick-that-has-hole-in-the-blade/</link>
|
||||
<pubDate>Tue, 25 Aug 2020 19:38:09 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24275159</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24275159">Comments</a>]]></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,116 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import android.accounts.NetworkErrorException
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.HttpManager
|
||||
import com.readrops.api.utils.ParseException
|
||||
import junit.framework.TestCase.*
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LocalRSSDataSourceTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
private lateinit var url: HttpUrl
|
||||
|
||||
private val mockServer: MockWebServer = MockWebServer()
|
||||
private val localRSSDataSource = LocalRSSDataSource(HttpManager.getInstance().okHttpClient)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer.start()
|
||||
url = mockServer.url("/rss")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockServer.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun successfulQueryTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/rss+xml; charset=UTF-8")
|
||||
.setBody(context.resources.assets.open("localfeed/rss_feed.xml").toString()))
|
||||
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null, false)
|
||||
|
||||
assertNotNull(pair?.first)
|
||||
assertNotNull(pair?.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun response304Test() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED))
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null, false)
|
||||
|
||||
assertNull(pair)
|
||||
}
|
||||
|
||||
@Test(expected = NetworkErrorException::class)
|
||||
fun response404Test() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null, false)
|
||||
}
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun noContentTypeTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null, false)
|
||||
}
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun badContentTypeTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", ""))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null, false)
|
||||
}
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun badContentTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/xml")
|
||||
.setBody("<html> </html>"))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isUrlResourceSuccessfulTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/atom+xml"))
|
||||
|
||||
assertTrue(localRSSDataSource.isUrlRSSResource(url.toString()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isUrlRSSResourceFailureTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
|
||||
|
||||
assertFalse(localRSSDataSource.isUrlRSSResource(url.toString()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isUrlRSSResourceBadContentTypeTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/xml")
|
||||
.setBody("<html> </html>"))
|
||||
|
||||
assertFalse(localRSSDataSource.isUrlRSSResource(url.toString()))
|
||||
}
|
||||
}
|
@ -3,7 +3,10 @@
|
||||
|
||||
<!-- for tests only -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application android:requestLegacyExternalStorage="true" />
|
||||
<application
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true" />
|
||||
|
||||
</manifest>
|
@ -0,0 +1,88 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import android.accounts.NetworkErrorException
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.readrops.api.utils.LibUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class LocalRSSDataSource(private val httpClient: OkHttpClient) {
|
||||
|
||||
/**
|
||||
* Query RSS url
|
||||
* @param url url to query
|
||||
* @param headers request headers
|
||||
* @param withItems parse items with their feed
|
||||
* @return a Feed object with its items if specified by [withItems]
|
||||
*/
|
||||
@WorkerThread
|
||||
fun queryRSSResource(url: String, headers: Headers?, withItems: Boolean): Pair<Feed, List<Item>>? {
|
||||
val response = queryUrl(url, headers)
|
||||
|
||||
return when {
|
||||
response.isSuccessful -> {
|
||||
val header = response.header(LibUtils.CONTENT_TYPE_HEADER)
|
||||
?: throw ParseException("Unable to get $url content-type")
|
||||
|
||||
val contentType = LibUtils.parseContentType(header)
|
||||
?: throw ParseException("Unable to get $url content-type")
|
||||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
// if we can't guess type based on content-type header, we use the content
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!)
|
||||
// if we can't guess type even with the content, we are unable to go further
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) throw ParseException("Unable to guess $url RSS type")
|
||||
|
||||
val feed = parseFeed(response, type)
|
||||
val items = if (withItems) parseItems(response.body?.byteStream()!!, type) else listOf()
|
||||
|
||||
return Pair(feed, items)
|
||||
}
|
||||
response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null
|
||||
else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun isUrlRSSResource(url: String): Boolean {
|
||||
val response = queryUrl(url, null)
|
||||
|
||||
return if (response.isSuccessful) {
|
||||
val contentType = response.header(LibUtils.CONTENT_TYPE_HEADER)
|
||||
?: return false
|
||||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN)
|
||||
type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method
|
||||
|
||||
type != LocalRSSHelper.RSSType.UNKNOWN
|
||||
} else false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun queryUrl(url: String, headers: Headers?): Response {
|
||||
val requestBuilder = Request.Builder().url(url)
|
||||
headers?.let { requestBuilder.headers(it) }
|
||||
|
||||
return httpClient.newCall(requestBuilder.build()).execute()
|
||||
}
|
||||
|
||||
private fun parseFeed(response: Response, type: LocalRSSHelper.RSSType): Feed {
|
||||
response.body?.close()
|
||||
return Feed()
|
||||
}
|
||||
|
||||
private fun parseItems(inputStream: InputStream, type: LocalRSSHelper.RSSType): List<Item> {
|
||||
return listOf()
|
||||
}
|
||||
}
|
@ -4,10 +4,13 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class LibUtils {
|
||||
|
||||
@ -28,6 +31,8 @@ public final class LibUtils {
|
||||
public static final int HTTP_NOT_FOUND = 404;
|
||||
public static final int HTTP_CONFLICT = 409;
|
||||
|
||||
private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)";
|
||||
|
||||
|
||||
public static String inputStreamToString(InputStream input) {
|
||||
Scanner scanner = new Scanner(input).useDelimiter("\\A");
|
||||
@ -44,4 +49,16 @@ public final class LibUtils {
|
||||
return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
|
||||
|| type.equals("image/png");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String parseContentType(String header) {
|
||||
Matcher matcher = Pattern.compile(RSS_CONTENT_TYPE_REGEX)
|
||||
.matcher(header);
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user