diff --git a/api/src/main/java/com/readrops/api/services/fever/FeverDataSource.kt b/api/src/main/java/com/readrops/api/services/fever/FeverDataSource.kt index 068c2d52..8ebe0678 100644 --- a/api/src/main/java/com/readrops/api/services/fever/FeverDataSource.kt +++ b/api/src/main/java/com/readrops/api/services/fever/FeverDataSource.kt @@ -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) } } diff --git a/api/src/main/java/com/readrops/api/services/fever/FeverSyncData.kt b/api/src/main/java/com/readrops/api/services/fever/FeverSyncData.kt deleted file mode 100644 index bac64011..00000000 --- a/api/src/main/java/com/readrops/api/services/fever/FeverSyncData.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.readrops.api.services.fever - -data class FeverSyncData( - val sinceId: String, -) \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/MockServerExtensions.kt b/api/src/test/java/com/readrops/api/MockServerExtensions.kt index 057c0978..24d7cd45 100644 --- a/api/src/test/java/com/readrops/api/MockServerExtensions.kt +++ b/api/src/test/java/com/readrops/api/MockServerExtensions.kt @@ -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)) } \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/services/fever/FeverDataSourceTest.kt b/api/src/test/java/com/readrops/api/services/fever/FeverDataSourceTest.kt index 61ec5874..919239ad 100644 --- a/api/src/test/java/com/readrops/api/services/fever/FeverDataSourceTest.kt +++ b/api/src/test/java/com/readrops/api/services/fever/FeverDataSourceTest.kt @@ -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() + } } \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/services/fever/adapters/FeverItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/services/fever/adapters/FeverItemsAdapterTest.kt index 05d82da0..774f1950 100644 --- a/api/src/test/java/com/readrops/api/services/fever/adapters/FeverItemsAdapterTest.kt +++ b/api/src/test/java/com/readrops/api/services/fever/adapters/FeverItemsAdapterTest.kt @@ -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))!! diff --git a/api/src/test/resources/services/fever/empty_items.json b/api/src/test/resources/services/fever/empty_items.json new file mode 100644 index 00000000..d0b6c34b --- /dev/null +++ b/api/src/test/resources/services/fever/empty_items.json @@ -0,0 +1,7 @@ +{ + "api_version": 3, + "auth": 1, + "last_refreshed_on_time": 1635849601, + "total_items": 10814, + "items": [] +} \ No newline at end of file diff --git a/api/src/test/resources/services/fever/items.json b/api/src/test/resources/services/fever/items.json deleted file mode 100644 index 9c41bdc2..00000000 --- a/api/src/test/resources/services/fever/items.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "api_version": 3, - "auth": 1, - "last_refreshed_on_time": 1635849601, - "total_items": 10814, - "items": [ - { - "id": "1546007484154894", - "feed_id": 2, - "title": "FreshRSS 1.9.0", - "author": "Alkarex", - "html": "\n
Changelog<\/a>:<\/p>\n Changelog<\/a>:<\/p>\n Changelog<\/a>:<\/p>\n\n
\n
\n
\n
\n
CURLOPT_FOLLOWLOCATION<\/code>
open_basedir<\/code> bug in favicons and PubSubHubbub #1655<\/a>\n<\/li>\n
\n
.\/cli\/db-optimize.php<\/code> for database optimisation #1583<\/a>\n<\/li>\n
actualize_script.php<\/code> (cron for refreshing feeds) #1711<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
VACUUM<\/code> on SQLite and PostgreSQL databases when optimisation is requested #918<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
link<\/code> to articles without HTML-encoding #1683<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
.\/Controllers\/<\/code> directory #1729<\/a>\n<\/li>\n
$entry->_hash($hex)<\/code> for extensions that change the content of entries #1707<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
\n
constants.local.php<\/code> #1725<\/a>\n<\/li>\n
.editorconfig<\/code> file #1732<\/a>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n",
- "url": "https:\/\/github.com\/FreshRSS\/FreshRSS\/releases\/tag\/1.9.0",
- "is_saved": 1,
- "is_read": 1,
- "created_on_time": 1515697500
- }
- ]
-}
\ No newline at end of file
diff --git a/api/src/test/resources/services/fever/items_page1.json b/api/src/test/resources/services/fever/items_page1.json
new file mode 100644
index 00000000..5c53acea
--- /dev/null
+++ b/api/src/test/resources/services/fever/items_page1.json
@@ -0,0 +1,63 @@
+{
+ "api_version": 3,
+ "auth": 1,
+ "last_refreshed_on_time": 1635849601,
+ "total_items": 10814,
+ "items": [
+ {
+ "id": "5",
+ "feed_id": 2,
+ "title": "FreshRSS 1.9.0",
+ "author": "Alkarex",
+ "html": "\n
\n
\n
\n
\n
\n
\n
CURLOPT_FOLLOWLOCATION<\/code>
open_basedir<\/code> bug in favicons and PubSubHubbub #1655<\/a>\n<\/li>\n
\n
.\/cli\/db-optimize.php<\/code> for database optimisation #1583<\/a>\n<\/li>\n
actualize_script.php<\/code> (cron for refreshing feeds) #1711<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
VACUUM<\/code> on SQLite and PostgreSQL databases when optimisation is requested #918<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
link<\/code> to articles without HTML-encoding #1683<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
.\/Controllers\/<\/code> directory #1729<\/a>\n<\/li>\n
$entry->_hash($hex)<\/code> for extensions that change the content of entries #1707<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
\n
constants.local.php<\/code> #1725<\/a>\n<\/li>\n
.editorconfig<\/code> file #1732<\/a>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n",
+ "url": "https:\/\/github.com\/FreshRSS\/FreshRSS\/releases\/tag\/1.9.0",
+ "is_saved": 1,
+ "is_read": 1,
+ "created_on_time": 1515697500
+ },
+ {
+ "id": "4",
+ "feed_id": 2,
+ "title": "FreshRSS 1.9.0",
+ "author": "Alkarex",
+ "html": "\n
\n
\n
\n
\n
\n
\n
CURLOPT_FOLLOWLOCATION<\/code>
open_basedir<\/code> bug in favicons and PubSubHubbub #1655<\/a>\n<\/li>\n
\n
.\/cli\/db-optimize.php<\/code> for database optimisation #1583<\/a>\n<\/li>\n
actualize_script.php<\/code> (cron for refreshing feeds) #1711<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
VACUUM<\/code> on SQLite and PostgreSQL databases when optimisation is requested #918<\/a>\n<\/li>\n<\/ul>\n<\/li>\n
\n
link<\/code> to articles without HTML-encoding #1683<\/a>\n<\/li>\n<\/ul>\n<\/li>\n