1+ package com.readrops.app.sync
2+
3+ import android.content.Context
4+ import android.content.SharedPreferences
5+ import androidx.core.app.NotificationCompat.Builder
6+ import androidx.test.core.app.ApplicationProvider
7+ import coil3.imageLoader
8+ import com.readrops.api.utils.ApiUtils
9+ import com.readrops.app.R
10+ import com.readrops.app.ReadropsApp
11+ import com.readrops.app.testutil.ReadropsTestRule
12+ import com.readrops.app.testutil.TestUtils
13+ import com.readrops.app.testutil.okResponseWithBody
14+ import com.readrops.db.Database
15+ import com.readrops.db.entities.Feed
16+ import com.readrops.db.entities.Folder
17+ import com.readrops.db.entities.account.Account
18+ import com.readrops.db.entities.account.AccountType
19+ import junit.framework.TestCase.assertEquals
20+ import junit.framework.TestCase.assertNotNull
21+ import kotlinx.coroutines.test.runTest
22+ import okhttp3.mockwebserver.Dispatcher
23+ import okhttp3.mockwebserver.MockResponse
24+ import okhttp3.mockwebserver.MockWebServer
25+ import okhttp3.mockwebserver.RecordedRequest
26+ import okio.Buffer
27+ import org.junit.After
28+ import org.junit.Before
29+ import org.junit.Rule
30+ import org.junit.Test
31+ import org.koin.test.KoinTest
32+ import org.koin.test.inject
33+ import java.net.HttpURLConnection
34+
35+ class SynchronizerTest : KoinTest {
36+
37+ // TODO database.accountDao().selectAllAccounts().first() test case
38+ // TODO FeedColors.getFeedColor in fetchFeedColors test case, but should wait for FeedColors.getFeedColor coil usage?
39+
40+ private val database: Database by inject()
41+ private val encryptedSharedPreferences: SharedPreferences by inject()
42+ private val synchronizer: Synchronizer by inject()
43+ private val context = ApplicationProvider .getApplicationContext<Context >()
44+ private val mockServer = MockWebServer ()
45+
46+ @get:Rule
47+ val rule = ReadropsTestRule ()
48+
49+ private val remoteAccount = Account (
50+ name = " Remote account" ,
51+ type = AccountType .FRESHRSS ,
52+ url = mockServer.url(" /remote" ).toString(),
53+ writeToken = " writeToken"
54+ )
55+
56+ private val localAccount = Account (
57+ name = " Local account" ,
58+ type = AccountType .LOCAL
59+ )
60+
61+ private val feverAccount = Account (
62+ name = " Fever account" ,
63+ type = AccountType .FEVER ,
64+ url = mockServer.url(" /fever/" ).toString(),
65+ login = " login" ,
66+ password = " password"
67+ )
68+
69+ private val localFeed = Feed (
70+ name = " Hacker News" ,
71+ url = mockServer.url(" /local" ).toString()
72+ )
73+
74+ private val localFolder = Folder (
75+ name = " Local folder"
76+ )
77+
78+ @Before
79+ fun before () = runTest {
80+ // mockServer.start()
81+
82+ mockServer.dispatcher = object : Dispatcher () {
83+ override fun dispatch (request : RecordedRequest ): MockResponse {
84+ return MockResponse ()
85+ .setResponseCode(HttpURLConnection .HTTP_OK )
86+ .setHeader(ApiUtils .CONTENT_TYPE_HEADER , " application/rss+xml" )
87+ .setBody(Buffer ().readFrom(TestUtils .loadResource(" rss_feed.xml" )))
88+ }
89+ }
90+
91+ remoteAccount.id = database.accountDao().insert(remoteAccount).toInt()
92+ localAccount.id = database.accountDao().insert(localAccount).toInt()
93+ feverAccount.id = database.accountDao().insert(feverAccount).toInt()
94+
95+ localFolder.apply {
96+ accountId = localAccount.id
97+ id = database.folderDao().insert(localFolder).toInt()
98+ }
99+
100+ localFeed.apply {
101+ accountId = localAccount.id
102+ folderId = localFolder.id
103+ id = database.feedDao().insert(this ).toInt()
104+ }
105+
106+ encryptedSharedPreferences.edit()
107+ .putString(feverAccount.loginKey, feverAccount.login)
108+ .putString(feverAccount.passwordKey, feverAccount.password)
109+ .commit()
110+ }
111+
112+ @After
113+ fun after () {
114+ mockServer.shutdown()
115+ database.clearAllTables()
116+ // important when working with cached favicons
117+ context.cacheDir.resolve(" image_cache" ).deleteRecursively()
118+ }
119+
120+ @Test
121+ fun localAccountTest () = runTest {
122+ val notificationBuilder = Builder (context, ReadropsApp .SYNC_CHANNEL_ID )
123+ .setSmallIcon(R .drawable.ic_sync)
124+
125+ synchronizer.synchronizeAccounts(
126+ notificationBuilder,
127+ SyncInputData (localAccount.id, - 1 , - 1 )
128+ ) { feed, feedMax, feedCount ->
129+ assertEquals(" Hacker News" , feed.name)
130+ assertEquals(1 , feedMax)
131+ assertEquals(1 , feedCount)
132+ }
133+
134+ val items = database.itemDao().selectItems(localFeed.id)
135+ assertEquals(7 , items.size)
136+ }
137+
138+ @Test
139+ fun localAccountFeedSelectionTest () = runTest {
140+ val notificationBuilder = Builder (context, ReadropsApp .SYNC_CHANNEL_ID )
141+ .setSmallIcon(R .drawable.ic_sync)
142+
143+ synchronizer.synchronizeAccounts(
144+ notificationBuilder,
145+ SyncInputData (localAccount.id, localFeed.id, - 1 )
146+ ) { feed, feedMax, feedCount ->
147+ assertEquals(" Hacker News" , feed.name)
148+ assertEquals(1 , feedMax)
149+ assertEquals(1 , feedCount)
150+ }
151+
152+ val items = database.itemDao().selectItems(localFeed.id)
153+ assertEquals(7 , items.size)
154+ }
155+
156+ @Test
157+ fun localAccountFolderSelectionTest () = runTest {
158+ val notificationBuilder = Builder (context, ReadropsApp .SYNC_CHANNEL_ID )
159+ .setSmallIcon(R .drawable.ic_sync)
160+
161+ synchronizer.synchronizeAccounts(
162+ notificationBuilder,
163+ SyncInputData (localAccount.id, - 1 , localFolder.id)
164+ ) { feed, feedMax, feedCount ->
165+ assertEquals(" Hacker News" , feed.name)
166+ assertEquals(1 , feedMax)
167+ assertEquals(1 , feedCount)
168+ }
169+
170+ val items = database.itemDao().selectItems(localFeed.id)
171+ assertEquals(7 , items.size)
172+ }
173+
174+
175+ @Test
176+ fun remoteAccountTest () = runTest {
177+ mockServer.dispatcher = object : Dispatcher () {
178+
179+ override fun dispatch (request : RecordedRequest ): MockResponse {
180+ with (request.path!! ) {
181+ return when {
182+ contains(" tag/list" ) -> {
183+ MockResponse .okResponseWithBody(TestUtils .loadResource(" greader/folders.json" ))
184+ }
185+
186+ contains(" subscription/list" ) -> {
187+ MockResponse .okResponseWithBody(TestUtils .loadResource(" greader/feeds.json" ))
188+ }
189+
190+ // items
191+ contains(" contents/user/-/state/com.google/reading-list" ) -> {
192+ MockResponse .okResponseWithBody(TestUtils .loadResource(" greader/items.json" ))
193+ }
194+
195+ // starred items
196+ contains(" contents/user/-/state/com.google/starred" ) -> {
197+ MockResponse .okResponseWithBody(TestUtils .loadResource(" greader/items.json" ))
198+ }
199+
200+ // unread ids & starred ids
201+ contains(" stream/items/ids" ) -> {
202+ MockResponse .okResponseWithBody(TestUtils .loadResource(" greader/items_starred_ids.json" ))
203+ }
204+
205+ else -> MockResponse ().setResponseCode(404 )
206+ }
207+ }
208+ }
209+
210+ }
211+
212+ val notificationBuilder = Builder (context, ReadropsApp .SYNC_CHANNEL_ID )
213+ .setSmallIcon(R .drawable.ic_sync)
214+
215+ synchronizer.synchronizeAccounts(
216+ notificationBuilder,
217+ SyncInputData (remoteAccount.id, - 1 , - 1 )
218+ ) { feed, feedMax, feedCount ->
219+
220+ }
221+
222+ val feeds = database.feedDao().selectFeeds(remoteAccount.id)
223+ assertEquals(1 , feeds.size)
224+
225+ // contains both unstarred and starred items
226+ val items = database.itemDao().selectItems(feeds.first().id)
227+ assertEquals(4 , items.size)
228+ }
229+
230+
231+ @Test
232+ fun feverFaviconsTest () = runTest {
233+ mockServer.dispatcher = object : Dispatcher () {
234+
235+ override fun dispatch (request : RecordedRequest ): MockResponse {
236+ with (request.path!! ) {
237+ return when {
238+ contains(" /?feeds" ) -> {
239+ MockResponse .okResponseWithBody(TestUtils .loadResource(" fever/feeds.json" ))
240+ }
241+
242+ contains(" /?groups" ) -> {
243+ MockResponse .okResponseWithBody(TestUtils .loadResource(" fever/folders.json" ))
244+ }
245+
246+ contains(" /?favicons" ) -> {
247+ MockResponse .okResponseWithBody(TestUtils .loadResource(" fever/favicons.json" ))
248+ }
249+
250+ contains(" /?unread_item_ids" ) -> {
251+ MockResponse .okResponseWithBody(TestUtils .loadResource(" fever/itemsIds.json" ))
252+ }
253+
254+ contains(" /?saved_item_ids" ) -> {
255+ MockResponse .okResponseWithBody(TestUtils .loadResource(" fever/itemsIds.json" ))
256+ }
257+
258+ contains(" /?items" ) -> {
259+ MockResponse .okResponseWithBody(TestUtils .loadResource(" fever/items_page1.json" ))
260+ }
261+
262+ else -> MockResponse ().setResponseCode(404 )
263+ }
264+ }
265+ }
266+
267+ }
268+
269+ val notificationBuilder = Builder (context, ReadropsApp .SYNC_CHANNEL_ID )
270+ .setSmallIcon(R .drawable.ic_sync)
271+
272+ synchronizer.synchronizeAccounts(
273+ notificationBuilder,
274+ SyncInputData (feverAccount.id, - 1 , - 1 )
275+ ) { feed, feedMax, feedCount ->
276+
277+ }
278+
279+ val feeds = database.feedDao().selectFeeds(feverAccount.id)
280+ val diskCache = context.imageLoader.diskCache!!
281+
282+ assertNotNull { diskCache.openSnapshot(feeds.first().iconUrl!! ) }
283+ }
284+ }
0 commit comments