Skip to content

Commit 1f1ef87

Browse files
committed
Add tests for Synchronizer
1 parent 6f303f7 commit 1f1ef87

File tree

13 files changed

+550
-0
lines changed

13 files changed

+550
-0
lines changed

app/src/androidTest/java/com/readrops/app/TestApplication.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import coil3.ImageLoader
88
import coil3.PlatformContext
99
import coil3.SingletonImageLoader
1010
import coil3.annotation.ExperimentalCoilApi
11+
import coil3.disk.DiskCache
12+
import coil3.disk.directory
1113
import coil3.test.FakeImageLoaderEngine
1214
import coil3.util.DebugLogger
1315
import coil3.util.Logger
@@ -42,6 +44,11 @@ class TestApplication : Application(), SingletonImageLoader.Factory {
4244
return ImageLoader.Builder(this)
4345
.logger(DebugLogger(minLevel = Logger.Level.Debug))
4446
.components { add(fakeEngine) }
47+
.diskCache {
48+
DiskCache.Builder()
49+
.directory(this.cacheDir.resolve("image_cache"))
50+
.build()
51+
}
4552
.build()
4653
}
4754
}
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.readrops.app.testutil
2+
3+
import okhttp3.mockwebserver.MockResponse
4+
import okhttp3.mockwebserver.MockWebServer
5+
import okio.Buffer
6+
import java.io.InputStream
7+
import java.net.HttpURLConnection
8+
9+
fun MockWebServer.enqueueOK() {
10+
enqueue(MockResponse()
11+
.setResponseCode(HttpURLConnection.HTTP_OK)
12+
)
13+
}
14+
15+
fun MockWebServer.enqueueOKStream(stream: InputStream) {
16+
enqueue(MockResponse()
17+
.setResponseCode(HttpURLConnection.HTTP_OK)
18+
.setBody(Buffer().readFrom(stream)))
19+
}
20+
21+
fun MockResponse.Companion.okResponseWithBody(stream: InputStream): MockResponse {
22+
return MockResponse()
23+
.setResponseCode(HttpURLConnection.HTTP_OK)
24+
.setBody(Buffer().readFrom(stream))
25+
}

app/src/androidTest/resources/fever/favicons.json

Lines changed: 11 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"api_version": 3,
3+
"auth": 1,
4+
"feeds": [
5+
{
6+
"id": 32,
7+
"favicon_id": 85,
8+
"title": "xda-developers",
9+
"url": "https:\/\/www.xda-developers.com\/feed\/",
10+
"site_url": "https:\/\/www.xda-developers.com\/",
11+
"is_spark": 0,
12+
"last_updated_on_time": 1640364024
13+
}
14+
],
15+
"last_refreshed_on_time": 1640284745,
16+
"feeds_groups": [
17+
{
18+
"group_id": 3,
19+
"feed_ids": "5,4"
20+
}
21+
],
22+
"another_field": "another_value"
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"api_version": 3,
3+
"auth": 1,
4+
"last_refreshed_on_time": 1635849601,
5+
"groups": [
6+
{
7+
"id": 4,
8+
"title": "Libre"
9+
}
10+
],
11+
"feeds_groups": [
12+
{
13+
"group_id": 3,
14+
"feed_ids": "5,4"
15+
}
16+
]
17+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"api_version": 3,
3+
"auth": 1,
4+
"last_refreshed_on_time": 1635849601,
5+
"unread_item_ids": "1564058340320120,1564058340320124,1564058340320127,1564058340320128,1564058340320134,1564058340320135"
6+
}

0 commit comments

Comments
 (0)