Add new LocalRSSRepository with some tests
This commit is contained in:
parent
37f8bab393
commit
f7d1eea3ec
@ -59,7 +59,7 @@ dependencies {
|
||||
implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0'
|
||||
implementation 'org.redundent:kotlin-xml-builder:1.7.3'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
api 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
|
||||
implementation('com.squareup.retrofit2:retrofit:2.9.0') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp3'
|
||||
|
@ -94,4 +94,5 @@ dependencies {
|
||||
androidTestImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version"
|
||||
androidTestImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version"
|
||||
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package com.readrops.app.compose
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.readrops.api.apiModule
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.app.compose.repositories.LocalRSSRepository
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.Buffer
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.koin.dsl.module
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.KoinTestRule
|
||||
import org.koin.test.get
|
||||
import java.net.HttpURLConnection
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
|
||||
class LocalRSSRepositoryTest : KoinTest {
|
||||
|
||||
private val mockServer: MockWebServer = MockWebServer()
|
||||
private val account = Account(accountType = AccountType.LOCAL)
|
||||
private lateinit var database: Database
|
||||
private lateinit var repository: LocalRSSRepository
|
||||
private lateinit var feeds: List<Feed>
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
|
||||
|
||||
KoinTestRule.create {
|
||||
modules(apiModule, module {
|
||||
single { database }
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
.callTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.HOURS)
|
||||
.addInterceptor(get<AuthInterceptor>())
|
||||
.build()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mockServer.start()
|
||||
val url = mockServer.url("/rss")
|
||||
|
||||
account.id = database.accountDao().compatInsert(account).toInt()
|
||||
feeds = listOf(
|
||||
Feed(
|
||||
name = "feedTest",
|
||||
url = url.toString(),
|
||||
accountId = account.id,
|
||||
),
|
||||
)
|
||||
|
||||
runBlocking {
|
||||
database.newFeedDao().insert(feeds).run {
|
||||
feeds.first().id = first().toInt()
|
||||
}
|
||||
}
|
||||
|
||||
repository = LocalRSSRepository(get(), database, account)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun synchronizeTest() = runBlocking {
|
||||
val stream = TestUtils.loadResource("rss_feed.xml")
|
||||
|
||||
mockServer.enqueue(
|
||||
MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8")
|
||||
.setBody(Buffer().readFrom(stream))
|
||||
)
|
||||
|
||||
val result = repository.synchronize(null) {
|
||||
assertEquals(it.name, feeds.first().name)
|
||||
}
|
||||
|
||||
assertTrue { result.first.items.isNotEmpty() }
|
||||
assertTrue { database.itemDao().itemExists(result.first.items.first().guid!!, account.id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun synchronizeWithFeedsTest(): Unit = runBlocking {
|
||||
val stream = TestUtils.loadResource("rss_feed.xml")
|
||||
|
||||
mockServer.enqueue(
|
||||
MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8")
|
||||
.setBody(Buffer().readFrom(stream))
|
||||
)
|
||||
|
||||
val result = repository.synchronize(feeds) {
|
||||
assertEquals(it.name, feeds.first().name)
|
||||
}
|
||||
|
||||
assertTrue { result.first.items.isNotEmpty() }
|
||||
assertTrue { database.itemDao().itemExists(result.first.items.first().guid!!, account.id) }
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.readrops.app.compose
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
object TestUtils {
|
||||
|
||||
fun loadResource(path: String): InputStream =
|
||||
javaClass.classLoader?.getResourceAsStream(path)!!
|
||||
}
|
61
appcompose/src/androidTest/resources/rss_feed.xml
Normal file
61
appcompose/src/androidTest/resources/rss_feed.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"
|
||||
xmlns:atom="http://www.w3.org/1999/xhtml">
|
||||
<channel>
|
||||
<title>Hacker News</title>
|
||||
<atom:link href="https://news.ycombinator.com/feed/" rel="self" />
|
||||
<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>
|
||||
<author>Author 1</author>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273602">Comments</a>]]></description>
|
||||
<media:description>media description</media: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,56 @@
|
||||
package com.readrops.app.compose.repositories
|
||||
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import com.readrops.db.entities.account.Account
|
||||
|
||||
data class ErrorResult(
|
||||
val values: Map<Feed, Exception>
|
||||
)
|
||||
|
||||
abstract class ARepository(
|
||||
val database: Database,
|
||||
val account: Account
|
||||
) {
|
||||
|
||||
/**
|
||||
* This method is intended for remote accounts.
|
||||
*/
|
||||
abstract suspend fun login()
|
||||
|
||||
/**
|
||||
* Global synchronization for the local account.
|
||||
* @param selectedFeeds feeds to be updated
|
||||
* @param onUpdate get synchronization status
|
||||
* @return returns the result of the synchronization used by notifications
|
||||
* and errors per feed if occurred to be transmitted to the user
|
||||
*/
|
||||
abstract suspend fun synchronize(
|
||||
selectedFeeds: List<Feed>?,
|
||||
onUpdate: (Feed) -> Unit
|
||||
): Pair<SyncResult, ErrorResult>
|
||||
|
||||
/**
|
||||
* Global synchronization for remote accounts. Unlike the local account, remote accounts
|
||||
* won't benefit from synchronization status and granular synchronization
|
||||
*/
|
||||
abstract suspend fun synchronize(): SyncResult
|
||||
|
||||
abstract suspend fun insertNewFeeds()
|
||||
}
|
||||
|
||||
abstract class BaseRepository(
|
||||
database: Database,
|
||||
account: Account,
|
||||
) : ARepository(database, account) {
|
||||
|
||||
open suspend fun updateFeed(feed: Feed) {}
|
||||
|
||||
open suspend fun deleteFeed(feed: Feed) {}
|
||||
|
||||
open suspend fun addFolder(folder: Folder) {}
|
||||
|
||||
open suspend fun deleteFolder(folder: Folder) {}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package com.readrops.app.compose.repositories
|
||||
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
class LocalRSSRepository(
|
||||
private val dataSource: LocalRSSDataSource,
|
||||
database: Database,
|
||||
account: Account
|
||||
) : BaseRepository(database, account) {
|
||||
|
||||
override suspend fun login() { /* useless here */
|
||||
}
|
||||
|
||||
override suspend fun synchronize(
|
||||
selectedFeeds: List<Feed>?,
|
||||
onUpdate: (Feed) -> Unit
|
||||
): Pair<SyncResult, ErrorResult> {
|
||||
val errors = mutableMapOf<Feed, Exception>()
|
||||
val syncResult = SyncResult()
|
||||
|
||||
val feeds = if (selectedFeeds.isNullOrEmpty()) {
|
||||
database.newFeedDao().selectFeeds(account.id)
|
||||
} else selectedFeeds
|
||||
|
||||
for (feed in feeds) {
|
||||
try {
|
||||
val pair = dataSource.queryRSSResource(feed.url!!, null)
|
||||
|
||||
pair?.let {
|
||||
insertNewItems(it.second, feed)
|
||||
syncResult.items = it.second
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors[feed] = e
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Pair(syncResult, ErrorResult(errors))
|
||||
}
|
||||
|
||||
override suspend fun synchronize(): SyncResult = throw NotImplementedError("This method can't be called here")
|
||||
|
||||
|
||||
override suspend fun insertNewFeeds() {
|
||||
/*TODO("Not yet implemented")*/
|
||||
}
|
||||
|
||||
private suspend fun insertNewItems(items: List<Item>, feed: Feed) {
|
||||
items.sortedWith(Item::compareTo) // TODO Check if ordering is useful in this situation
|
||||
val itemsToInsert = mutableListOf<Item>()
|
||||
|
||||
for (item in items) {
|
||||
if (!database.itemDao().itemExists(item.guid!!, feed.accountId)) {
|
||||
if (item.description != null) {
|
||||
item.cleanDescription = Jsoup.parse(item.description).text()
|
||||
}
|
||||
|
||||
if (item.content != null) {
|
||||
item.readTime = 0.0
|
||||
} else if (item.description != null) {
|
||||
item.readTime = 0.0
|
||||
}
|
||||
|
||||
item.feedId = feed.id
|
||||
itemsToInsert += item
|
||||
}
|
||||
}
|
||||
|
||||
database.newItemDao().insert(itemsToInsert)
|
||||
}
|
||||
|
||||
private suspend fun insertFeed(feed: Feed): Feed {
|
||||
if (database.newFeedDao().feedExists(feed.url!!, account.id)) {
|
||||
throw IllegalStateException("Feed already exists for account ${account.accountName}")
|
||||
}
|
||||
|
||||
return feed.apply {
|
||||
accountId = account.id
|
||||
// we need empty headers to query the feed just after, without any 304 result
|
||||
etag = null
|
||||
lastModified = null
|
||||
|
||||
id = database.newFeedDao().insert(this).toInt()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user