Add FreshRSSDataSource kotlin migration with tests

This new data source is intended to replace the old java one
This commit is contained in:
Shinokuni 2023-08-01 21:40:47 +02:00
parent d673616bb4
commit 35ad5dfbc4
5 changed files with 458 additions and 0 deletions

View File

@ -0,0 +1,121 @@
package com.readrops.api.services.freshrss
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
import com.readrops.db.entities.Item
import okhttp3.MultipartBody
import java.io.StringReader
import java.util.Properties
class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
suspend fun login(login: String, password: String): String {
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("Email", login)
.addFormDataPart("Passwd", password)
.build()
val response = service.login(requestBody)
val properties = Properties()
properties.load(StringReader(response.string()))
response.close()
return properties.getProperty("Auth")
}
suspend fun getWriteToken(): String = service.getWriteToken().string()
suspend fun getUserInfo(): FreshRSSUserInfo = service.userInfo()
suspend fun sync() {
}
suspend fun getFolders() = service.getFolders()
suspend fun getFeeds() = service.getFeeds()
suspend fun getItems(excludeTargets: List<String>, max: Int, lastModified: Long): List<Item> {
return service.getItems(excludeTargets, max, lastModified)
}
suspend fun getStarredItems(max: Int) = service.getStarredItems(max)
suspend fun getItemsIds(excludeTarget: String, includeTarget: String, max: Int): List<String> {
return service.getItemsIds(excludeTarget, includeTarget, max)
}
private suspend fun setItemsReadState(read: Boolean, itemIds: List<String>, token: String) {
return if (read) {
service.setItemsState(token, GOOGLE_READ, null, itemIds)
} else {
service.setItemsState(token, null, GOOGLE_READ, itemIds)
}
}
private suspend fun setItemStarState(starred: Boolean, itemIds: List<String>, token: String) {
return if (starred) {
service.setItemsState(token, GOOGLE_STARRED, null, itemIds)
} else {
service.setItemsState(token, null, GOOGLE_STARRED, itemIds)
}
}
suspend fun createFeed(token: String, feedUrl: String) {
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe");
}
suspend fun deleteFeed(token: String, feedUrl: String) {
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe")
}
suspend fun updateFeed(token: String, feedUrl: String, title: String, folderId: String) {
service.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit")
}
suspend fun createFolder(token: String, tagName: String) {
service.createFolder(token, "$FOLDER_PREFIX$tagName")
}
suspend fun updateFolder(token: String, folderId: String, name: String) {
service.updateFolder(token, folderId, "$FOLDER_PREFIX$name")
}
suspend fun deleteFolder(token: String, folderId: String) {
service.deleteFolder(token, folderId)
}
suspend fun setItemsReadState(syncData: FreshRSSSyncData, token: String) {
if (syncData.readItemsIds.isNotEmpty()) {
setItemsReadState(true, syncData.readItemsIds, token)
}
if (syncData.unreadItemsIds.isNotEmpty()) {
setItemsReadState(false, syncData.unreadItemsIds, token)
}
}
suspend fun setItemsStarState(syncData: FreshRSSSyncData, token: String) {
if (syncData.starredItemsIds.isNotEmpty()) {
setItemStarState(true, syncData.starredItemsIds, token)
}
if (syncData.unstarredItemsIds.isNotEmpty()) {
setItemStarState(false, syncData.unstarredItemsIds, token)
}
}
companion object {
private const val MAX_ITEMS = 2500
private const val MAX_STARRED_ITEMS = 1000
const val GOOGLE_READ = "user/-/state/com.google/read"
const val GOOGLE_UNREAD = "user/-/state/com.google/unread"
const val GOOGLE_STARRED = "user/-/state/com.google/starred"
const val GOOGLE_READING_LIST = "user/-/state/com.google/reading-list"
const val FEED_PREFIX = "feed/"
const val FOLDER_PREFIX = "user/-/label/"
}
}

View File

@ -0,0 +1,73 @@
package com.readrops.api.services.freshrss
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
interface NewFreshRSSService {
@POST("accounts/ClientLogin")
suspend fun login(@Body body: RequestBody?): ResponseBody
@GET("reader/api/0/token")
suspend fun getWriteToken(): ResponseBody
@GET("reader/api/0/user-info")
suspend fun userInfo(): FreshRSSUserInfo
@GET("reader/api/0/subscription/list?output=json")
suspend fun getFeeds(): List<Feed>
@GET("reader/api/0/tag/list?output=json")
suspend fun getFolders(): List<Folder>
@GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
suspend fun getItems(@Query("xt") excludeTarget: List<String>?, @Query("n") max: Int,
@Query("ot") lastModified: Long?): List<Item>
@GET("reader/api/0/stream/contents/user/-/state/com.google/starred")
suspend fun getStarredItems(@Query("n") max: Int): List<Item>
@GET("reader/api/0/stream/items/ids")
suspend fun getItemsIds(@Query("xt") excludeTarget: String?, @Query("s") includeTarget: String?,
@Query("n") max: Int): List<String>
@FormUrlEncoded
@POST("reader/api/0/edit-tag")
suspend fun setItemsState(@Field("T") token: String, @Field("a") addAction: String?,
@Field("r") removeAction: String?, @Field("i") itemIds: List<String>)
@FormUrlEncoded
@POST("reader/api/0/subscription/edit")
suspend fun createOrDeleteFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("ac") action: String)
@FormUrlEncoded
@POST("reader/api/0/subscription/edit")
suspend fun updateFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("t") title: String,
@Field("a") folderId: String, @Field("ac") action: String)
@FormUrlEncoded
@POST("reader/api/0/edit-tag")
suspend fun createFolder(@Field("T") token: String, @Field("a") tagName: String)
@FormUrlEncoded
@POST("reader/api/0/rename-tag")
suspend fun updateFolder(@Field("T") token: String, @Field("s") folderId: String, @Field("dest") newFolderId: String)
@FormUrlEncoded
@POST("reader/api/0/disable-tag")
suspend fun deleteFolder(@Field("T") token: String, @Field("s") folderId: String)
companion object {
const val END_POINT = "/api/greader.php/"
}
}

View File

@ -0,0 +1,260 @@
package com.readrops.api.services.freshrss
import com.readrops.api.TestUtils
import com.readrops.api.apiModule
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.Buffer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.koin.test.KoinTest
import org.koin.test.KoinTestRule
import org.koin.test.get
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.net.HttpURLConnection
import java.net.URLEncoder
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class FreshRSSDataSourceTest : KoinTest {
private lateinit var freshRSSDataSource: NewFreshRSSDataSource
private val mockServer = MockWebServer()
@get:Rule
val koinTestRule = KoinTestRule.create {
modules(apiModule, module {
single {
Retrofit.Builder()
.baseUrl("http://localhost:8080/")
.client(get())
.addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi"))))
.build()
.create(NewFreshRSSService::class.java)
}
})
}
@Before
fun before() {
mockServer.start(8080)
freshRSSDataSource = NewFreshRSSDataSource(get())
}
@After
fun tearDown() {
mockServer.shutdown()
}
@Test
fun loginTest() {
runBlocking {
val responseBody = TestUtils.loadResource("services/freshrss/login_response_body")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(responseBody)))
val authString = freshRSSDataSource.login("Login", "Password")
assertEquals("login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a", authString)
val request = mockServer.takeRequest()
val requestBody = request.body.readUtf8()
assertTrue {
requestBody.contains("name=\"Email\"") && requestBody.contains("Login")
}
assertTrue {
requestBody.contains("name=\"Passwd\"") && requestBody.contains("Password")
}
}
}
@Test
fun writeTokenTest() = runBlocking {
val responseBody = TestUtils.loadResource("services/freshrss/writetoken_response_body")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(responseBody)))
val writeToken = freshRSSDataSource.getWriteToken()
assertEquals("PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg", writeToken)
}
@Test
fun userInfoTest() = runBlocking {
}
@Test
fun foldersTest() = runBlocking {
val stream = TestUtils.loadResource("services/freshrss/adapters/folders.json")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream)))
val folders = freshRSSDataSource.getFolders()
assertTrue { folders.size == 1 }
}
@Test
fun feedsTest() = runBlocking {
val stream = TestUtils.loadResource("services/freshrss/adapters/feeds.json")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream)))
val feeds = freshRSSDataSource.getFeeds()
assertTrue { feeds.size == 1 }
}
@Test
fun itemsTest() = runBlocking {
val stream = TestUtils.loadResource("services/freshrss/adapters/items.json")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream)))
val items = freshRSSDataSource.getItems(listOf(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_STARRED), 100, 21343321321321)
assertTrue { items.size == 2 }
val request = mockServer.takeRequest()
with(request.requestUrl!!) {
assertEquals(listOf(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_STARRED), queryParameterValues("xt"))
assertEquals("100", queryParameter("n"))
assertEquals("21343321321321", queryParameter("ot"))
}
}
@Test
fun starredItemsTest() = runBlocking {
val stream = TestUtils.loadResource("services/freshrss/adapters/items.json")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream)))
val items = freshRSSDataSource.getStarredItems(100)
assertTrue { items.size == 2 }
val request = mockServer.takeRequest()
assertEquals("100", request.requestUrl!!.queryParameter("n"))
}
@Test
fun getItemsIdsTest() = runBlocking {
val stream = TestUtils.loadResource("services/freshrss/adapters/items_starred_ids.json")
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody(Buffer().readFrom(stream)))
val ids = freshRSSDataSource.getItemsIds(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_READING_LIST, 100)
assertTrue { ids.size == 5 }
val request = mockServer.takeRequest()
with(request.requestUrl!!) {
assertEquals(NewFreshRSSDataSource.GOOGLE_READ, queryParameter("xt"))
assertEquals(NewFreshRSSDataSource.GOOGLE_READING_LIST, queryParameter("s"))
assertEquals("100", queryParameter("n"))
}
}
@Test
fun createFeedTest() = runBlocking {
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK))
freshRSSDataSource.createFeed("token", "https://feed.url")
val request = mockServer.takeRequest()
with(request.body.readUtf8()) {
assertTrue { contains("T=token") }
assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") }
assertTrue { contains("ac=subscribe") }
}
}
@Test
fun deleteFeedTest() = runBlocking {
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK))
freshRSSDataSource.deleteFeed("token", "https://feed.url")
val request = mockServer.takeRequest()
with(request.body.readUtf8()) {
assertTrue { contains("T=token") }
assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") }
assertTrue { contains("ac=unsubscribe") }
}
}
@Test
fun updateFeedTest() = runBlocking {
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK))
freshRSSDataSource.updateFeed("token", "https://feed.url", "title", "folderId")
val request = mockServer.takeRequest()
with(request.body.readUtf8()) {
assertTrue { contains("T=token") }
assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") }
assertTrue { contains("t=title") }
assertTrue { contains("a=folderId") }
assertTrue { contains("ac=edit") }
}
}
@Test
fun createFolderTest() = runBlocking {
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK))
freshRSSDataSource.createFolder("token", "folder")
val request = mockServer.takeRequest()
with(request.body.readUtf8()) {
assertTrue { contains("T=token") }
assertTrue { contains("a=${URLEncoder.encode("${NewFreshRSSDataSource.FOLDER_PREFIX}folder", "UTF-8")}") }
}
}
@Test
fun updateFolderTest() = runBlocking {
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK))
freshRSSDataSource.updateFolder("token", "folderId", "folder")
val request = mockServer.takeRequest()
with(request.body.readUtf8()) {
assertTrue { contains("T=token") }
assertTrue { contains("s=folderId") }
assertTrue { contains("dest=${URLEncoder.encode("${NewFreshRSSDataSource.FOLDER_PREFIX}folder", "UTF-8")}") }
}
}
@Test
fun deleteFolderTest() = runBlocking {
mockServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK))
freshRSSDataSource.deleteFolder("token", "folderId")
val request = mockServer.takeRequest()
with(request.body.readUtf8()) {
assertTrue { contains("T=token") }
assertTrue { contains("s=folderId") }
}
}
}

View File

@ -0,0 +1,3 @@
SID=login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a
LSID=null
Auth=login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a

View File

@ -0,0 +1 @@
PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg