mirror of https://github.com/readrops/Readrops.git
Add FreshRSSDataSource kotlin migration with tests
This new data source is intended to replace the old java one
This commit is contained in:
parent
d673616bb4
commit
35ad5dfbc4
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
SID=login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a
|
||||
LSID=null
|
||||
Auth=login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a
|
|
@ -0,0 +1 @@
|
|||
PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg
|
Loading…
Reference in New Issue