Add some tests to FeverDataSource

This commit is contained in:
Shinokuni 2024-08-10 21:45:37 +02:00
parent 0265b88ff3
commit 7a8f255b72
10 changed files with 325 additions and 63 deletions

View File

@ -27,7 +27,7 @@ class FeverDataSource(private val service: FeverService) {
login: String,
password: String,
syncType: SyncType,
syncData: FeverSyncData
lastSinceId: String,
): FeverSyncResult = with(CoroutineScope(Dispatchers.IO)) {
val body = getFeverRequestBody(login, password)
@ -39,14 +39,16 @@ class FeverDataSource(private val service: FeverService) {
async {
unreadIds = service.getUnreadItemsIds(body)
.reversed()
.subList(0, MAX_ITEMS_IDS)
.take(MAX_ITEMS_IDS)
var lastId = unreadIds.first()
var maxId = unreadIds.first()
items = buildList {
repeat(INITIAL_SYNC_ITEMS_REQUESTS_COUNT) {
val newItems = service.getItems(body, lastId, null)
for(index in 0 until INITIAL_SYNC_ITEMS_REQUESTS_COUNT) {
val newItems = service.getItems(body, maxId, null)
lastId = newItems.last().remoteId!!
if (newItems.isEmpty()) break
// always take the lowest id
maxId = newItems.last().remoteId!!
addAll(newItems)
}
}
@ -58,8 +60,6 @@ class FeverDataSource(private val service: FeverService) {
)
.awaitAll()
}
} else {
return FeverSyncResult().apply {
listOf(
@ -70,18 +70,23 @@ class FeverDataSource(private val service: FeverService) {
async { favicons = listOf() },
async {
items = buildList {
var sinceId = syncData.sinceId
var localSinceId = lastSinceId
while (true) {
val newItems = service.getItems(body, null, sinceId)
val newItems = service.getItems(body, null, localSinceId)
if (newItems.isEmpty()) break
sinceId = newItems.first().remoteId!!
// always take the highest id
localSinceId = newItems.first().remoteId!!
addAll(newItems)
}
}
if (items.isNotEmpty()) items.first().remoteId!!.toLong() else sinceId.toLong()
sinceId = if (items.isNotEmpty()) {
items.first().remoteId!!.toLong()
} else {
localSinceId.toLong()
}
}
}
)
.awaitAll()
@ -105,8 +110,8 @@ class FeverDataSource(private val service: FeverService) {
}
companion object {
private const val MAX_ITEMS_IDS = 5000
private const val INITIAL_SYNC_ITEMS_REQUESTS_COUNT = 10
private const val MAX_ITEMS_IDS = 1000
private const val INITIAL_SYNC_ITEMS_REQUESTS_COUNT = 20 // (1000 items max)
}
}

View File

@ -1,5 +0,0 @@
package com.readrops.api.services.fever
data class FeverSyncData(
val sinceId: String,
)

View File

@ -16,4 +16,10 @@ fun MockWebServer.enqueueStream(stream: InputStream) {
enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream)))
}
fun MockResponse.Companion.okResponseWithBody(stream: InputStream): MockResponse {
return MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream))
}

View File

@ -2,13 +2,17 @@ package com.readrops.api.services.fever
import com.readrops.api.TestUtils
import com.readrops.api.apiModule
import com.readrops.api.utils.ApiUtils
import com.readrops.api.enqueueOK
import com.readrops.api.enqueueStream
import com.readrops.api.okResponseWithBody
import com.readrops.api.services.SyncType
import com.readrops.api.utils.AuthInterceptor
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.Buffer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.After
import org.junit.Before
import org.junit.Rule
@ -18,8 +22,8 @@ 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.assertFalse
import kotlin.test.assertTrue
@ -59,12 +63,7 @@ class FeverDataSourceTest : KoinTest {
@Test
fun loginSuccessfulTest() = runTest {
val stream = TestUtils.loadResource("services/fever/successful_auth.json")
mockServer.enqueue(
MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/json")
.setBody(Buffer().readFrom(stream))
)
mockServer.enqueueStream(stream)
assertTrue { dataSource.login("", "") }
}
@ -72,13 +71,161 @@ class FeverDataSourceTest : KoinTest {
@Test
fun loginFailedTest() = runTest {
val stream = TestUtils.loadResource("services/fever/failed_auth.json")
mockServer.enqueue(
MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/json")
.setBody(Buffer().readFrom(stream))
)
mockServer.enqueueStream(stream)
assertFalse { dataSource.login("", "") }
}
@Test
fun setItemStateTest() = runTest {
mockServer.enqueueOK()
dataSource.setItemState("login", "password", "saved", "itemId")
val request = mockServer.takeRequest()
val requestBody = request.body.readUtf8()
assertEquals("saved", request.requestUrl?.queryParameter("as"))
assertEquals("itemId", request.requestUrl?.queryParameter("id"))
assertTrue { requestBody.contains("api_key") }
assertTrue { requestBody.contains("fb2f5a9b0eccc1ee95c1d559a2dd797a") }
}
@Test
fun initialSyncTest() = runTest {
var pageNumber = 0
var firstMaxId = ""
var secondMaxId = ""
var thirdMaxId = ""
mockServer.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
with(request.path!!) {
return when {
this == "/?feeds" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/feeds.json"))
}
this == "/?groups" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/folders.json"))
}
this == "/?unread_item_ids" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
}
this == "/?saved_item_ids" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
}
contains("/?items") -> {
when (pageNumber++) {
0 -> {
firstMaxId = request.requestUrl?.queryParameter("max_id").orEmpty()
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page2.json"))
}
1 -> {
secondMaxId = request.requestUrl?.queryParameter("max_id").orEmpty()
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page1.json"))
}
2 -> {
thirdMaxId = request.requestUrl?.queryParameter("max_id").orEmpty()
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/empty_items.json"))
}
else -> MockResponse().setResponseCode(404)
}
}
else -> MockResponse().setResponseCode(404)
}
}
}
}
val result = dataSource.synchronize("login", "password", SyncType.INITIAL_SYNC, "")
assertEquals(1, result.folders.size)
assertEquals(1, result.feverFeeds.feeds.size)
assertEquals(6, result.unreadIds.size)
assertEquals(6, result.starredIds.size)
assertEquals(10, result.items.size)
assertEquals(10, result.items.size)
assertEquals("1564058340320135", firstMaxId)
assertEquals("6", secondMaxId)
assertEquals("1", thirdMaxId)
}
@Test
fun classicSyncTest() = runTest {
var pageNumber = 0
var firstLastSinceId = ""
var secondLastSinceId = ""
var thirdLastSinceId = ""
mockServer.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
with(request.path!!) {
return when {
this == "/?feeds" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/feeds.json"))
}
this == "/?groups" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/folders.json"))
}
this == "/?unread_item_ids" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
}
this == "/?saved_item_ids" -> {
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/itemsIds.json"))
}
contains("/?items") -> {
when (pageNumber++) {
0 -> {
firstLastSinceId = request.requestUrl?.queryParameter("since_id").orEmpty()
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page1.json"))
}
1 -> {
secondLastSinceId = request.requestUrl?.queryParameter("since_id").orEmpty()
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/items_page2.json"))
}
2 -> {
thirdLastSinceId = request.requestUrl?.queryParameter("since_id").orEmpty()
MockResponse.okResponseWithBody(TestUtils.loadResource("services/fever/empty_items.json"))
}
else -> MockResponse().setResponseCode(404)
}
}
else -> MockResponse().setResponseCode(404)
}
}
}
}
val result = dataSource.synchronize("login", "password", SyncType.CLASSIC_SYNC, "1")
assertEquals(1, result.folders.size)
assertEquals(1, result.feverFeeds.feeds.size)
assertEquals(6, result.unreadIds.size)
assertEquals(6, result.starredIds.size)
assertEquals(10, result.items.size)
assertEquals("5", result.items.first().remoteId)
assertEquals("6", result.items.last().remoteId)
assertEquals("1", firstLastSinceId)
assertEquals("5", secondLastSinceId)
assertEquals("10", thirdLastSinceId)
mockServer.dispatcher.shutdown()
}
}

View File

@ -19,7 +19,7 @@ class FeverItemsAdapterTest {
@Test
fun validItemsTest() {
val stream = TestUtils.loadResource("services/fever/items.json")
val stream = TestUtils.loadResource("services/fever/items_page2.json")
val items = adapter.fromJson(Buffer().readFrom(stream))!!

View File

@ -0,0 +1,7 @@
{
"api_version": 3,
"auth": 1,
"last_refreshed_on_time": 1635849601,
"total_items": 10814,
"items": []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,6 @@ package com.readrops.app.repositories
import android.util.Log
import com.readrops.api.services.SyncType
import com.readrops.api.services.fever.FeverDataSource
import com.readrops.api.services.fever.FeverSyncData
import com.readrops.api.services.fever.ItemAction
import com.readrops.api.services.fever.adapters.FeverFeeds
import com.readrops.api.utils.exceptions.LoginFailedException
@ -43,7 +42,7 @@ class FeverRepository(
account.login!!,
account.password!!,
syncType,
FeverSyncData(account.lastModified.toString())
account.lastModified.toString()
).run {
insertFolders(folders)
val newFeeds = insertFeeds(feverFeeds)
@ -51,7 +50,7 @@ class FeverRepository(
val newItems = insertItems(items)
insertItemsIds(unreadIds, starredIds.toMutableList())
// We store the id to use for the next synchronisation even if it's not a timestamp
// We use the most recent item id as lastModified instead of a timestamp
database.accountDao().updateLastModified(sinceId, account.id)
SyncResult(
@ -66,11 +65,10 @@ class FeverRepository(
onUpdate: suspend (Feed) -> Unit
): Pair<SyncResult, ErrorResult> = throw NotImplementedError("This method can't be called here")
// Not supported by Fever API
override suspend fun insertNewFeeds(
newFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): ErrorResult = throw CloneNotSupportedException()
): ErrorResult = throw NotImplementedError("Add feed action not supported by Fever API")
override suspend fun updateFeed(feed: Feed) =
throw NotImplementedError("Update feed action not supported by Fever API")
@ -78,15 +76,12 @@ class FeverRepository(
override suspend fun deleteFeed(feed: Feed) =
throw NotImplementedError("Delete feed action not supported by Fever API")
// Not supported by Fever API
override suspend fun addFolder(folder: Folder) =
throw NotImplementedError("Add folder action not supported by Fever API")
// Not supported by Fever API
override suspend fun updateFolder(folder: Folder) =
throw NotImplementedError("Update folder action not supported by Fever API")
// Not supported by Fever API
override suspend fun deleteFolder(folder: Folder) =
throw NotImplementedError("Delete folder action not supported by Fever API")