Add new LocalRSSRepository with some tests

This commit is contained in:
Shinokuni 2023-07-21 22:29:52 +02:00
parent 37f8bab393
commit f7d1eea3ec
7 changed files with 334 additions and 1 deletions

View File

@ -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'

View File

@ -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'
}

View File

@ -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) }
}
}

View File

@ -0,0 +1,9 @@
package com.readrops.app.compose
import java.io.InputStream
object TestUtils {
fun loadResource(path: String): InputStream =
javaClass.classLoader?.getResourceAsStream(path)!!
}

View 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>

View File

@ -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) {}
}

View File

@ -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()
}
}
}