1+ package com.readrops.app.sync
2+
3+ import android.app.Notification
4+ import android.content.Context
5+ import android.util.Log
6+ import androidx.core.app.NotificationManagerCompat
7+ import androidx.test.core.app.ApplicationProvider
8+ import androidx.work.Configuration
9+ import androidx.work.ListenableWorker
10+ import androidx.work.OneTimeWorkRequestBuilder
11+ import androidx.work.WorkInfo
12+ import androidx.work.WorkManager
13+ import androidx.work.testing.SynchronousExecutor
14+ import androidx.work.testing.TestListenableWorkerBuilder
15+ import androidx.work.testing.WorkManagerTestInitHelper
16+ import androidx.work.workDataOf
17+ import com.readrops.api.utils.ApiUtils
18+ import com.readrops.app.testutil.ReadropsTestRule
19+ import com.readrops.app.testutil.TestUtils
20+ import com.readrops.app.util.extensions.getSerializable
21+ import com.readrops.db.Database
22+ import com.readrops.db.entities.Feed
23+ import com.readrops.db.entities.account.Account
24+ import com.readrops.db.entities.account.AccountType
25+ import junit.framework.TestCase.assertNotNull
26+ import kotlinx.coroutines.delay
27+ import kotlinx.coroutines.runBlocking
28+ import kotlinx.coroutines.test.runTest
29+ import okhttp3.mockwebserver.Dispatcher
30+ import okhttp3.mockwebserver.MockResponse
31+ import okhttp3.mockwebserver.MockWebServer
32+ import okhttp3.mockwebserver.RecordedRequest
33+ import okio.Buffer
34+ import org.junit.After
35+ import org.junit.Before
36+ import org.junit.Rule
37+ import org.junit.Test
38+ import org.koin.test.KoinTest
39+ import org.koin.test.inject
40+ import java.net.HttpURLConnection
41+ import java.util.concurrent.TimeUnit
42+ import kotlin.test.assertEquals
43+ import kotlin.test.assertFalse
44+ import kotlin.test.assertTrue
45+
46+ /* *
47+ *
48+ * This test suite runs over [SyncWorker] setup and implementation:
49+ * - WorkManagerTestInitHelper is used to test worker setup
50+ * - TestListenableWorkerBuilder is used to test worker implementation
51+ *
52+ * Notifications are also tested:
53+ * - Show notification
54+ * - Trigger read and star actions
55+ *
56+ * Remaining to test:
57+ * - [SyncWorker.startNow] which is currently untestable
58+ * - Simultaneous execution between a manual and auto worker
59+ * - Notification click (show the right screen)
60+ */
61+ class SyncWorkerTest : KoinTest {
62+
63+ private val database: Database by inject()
64+ private val notificationManager: NotificationManagerCompat by inject()
65+ private val mockServer = MockWebServer ()
66+ private val context = ApplicationProvider .getApplicationContext<Context >()
67+
68+ @get:Rule
69+ val rule = ReadropsTestRule ()
70+
71+ private val localAccount = Account (
72+ name = " Local account" ,
73+ type = AccountType .LOCAL ,
74+ isNotificationsEnabled = true
75+ )
76+
77+ private val feverAccount = Account (
78+ name = " Fever account" ,
79+ type = AccountType .FEVER ,
80+ )
81+
82+ private val localFeed = Feed (
83+ name = " Hacker news" ,
84+ url = mockServer.url(" /local" ).toString(),
85+ isNotificationEnabled = true
86+ )
87+
88+ @Before
89+ fun before () = runTest {
90+ // mockServer.start()
91+
92+ val config = Configuration .Builder ()
93+ .setMinimumLoggingLevel(Log .DEBUG )
94+ .setExecutor(SynchronousExecutor ())
95+ .build()
96+ WorkManagerTestInitHelper .initializeTestWorkManager(context, config)
97+
98+ mockServer.dispatcher = object : Dispatcher () {
99+
100+ override fun dispatch (request : RecordedRequest ): MockResponse {
101+ return MockResponse ()
102+ .setResponseCode(HttpURLConnection .HTTP_OK )
103+ .setHeader(ApiUtils .CONTENT_TYPE_HEADER , " application/rss+xml" )
104+ .setBody(Buffer ().readFrom(TestUtils .loadResource(" rss_feed_1_item.xml" )))
105+ }
106+
107+ }
108+
109+ localAccount.id = database.accountDao().insert(localAccount).toInt()
110+ feverAccount.id = database.accountDao().insert(feverAccount).toInt()
111+
112+ localFeed.apply {
113+ accountId = localAccount.id
114+ id = database.feedDao().insert(localFeed).toInt()
115+ }
116+ }
117+
118+ @After
119+ fun after () {
120+ mockServer.shutdown()
121+ database.clearAllTables()
122+ notificationManager.cancelAll()
123+ }
124+
125+ @Test
126+ fun manualWorkerTest () = runTest {
127+ val worker = TestListenableWorkerBuilder .from<SyncWorker >(context, SyncWorker ::class .java)
128+ .setTags(listOf (SyncWorker .WORK_MANUAL ))
129+ .setInputData(
130+ workDataOf(
131+ SyncWorker .ACCOUNT_ID_KEY to localAccount.id,
132+ SyncWorker .FEED_ID_KEY to localFeed.id
133+ )
134+ )
135+ .build()
136+
137+ val result = worker.doWork()
138+
139+ assertTrue { result is ListenableWorker .Result .Success }
140+ assertTrue { result.outputData.getBoolean(SyncWorker .END_SYNC_KEY , false ) }
141+
142+ assertEquals(0 , notificationManager.activeNotifications.size)
143+ }
144+
145+ @Test
146+ fun autoWorkerWithNotificationsTest () = runBlocking {
147+ val worker = TestListenableWorkerBuilder .from<SyncWorker >(context, SyncWorker ::class .java)
148+ .setTags(listOf (SyncWorker .WORK_AUTO ))
149+ .setInputData(
150+ workDataOf(
151+ SyncWorker .ACCOUNT_ID_KEY to localAccount.id,
152+ SyncWorker .FEED_ID_KEY to localFeed.id
153+ )
154+ )
155+ .build()
156+
157+ val result = worker.doWork()
158+
159+ assertTrue { result is ListenableWorker .Result .Success }
160+ assertTrue { result.outputData.getBoolean(SyncWorker .END_SYNC_KEY , false ) }
161+
162+ with (notificationManager.activeNotifications.first()) {
163+ assertEquals(SyncWorker .SYNC_RESULT_NOTIFICATION_ID , id)
164+ assertEquals(
165+ " Hacker news" ,
166+ this .notification.extras.getString(Notification .EXTRA_TITLE )
167+ )
168+
169+ notification.actions.forEach { it.actionIntent.send() }
170+
171+ // wait for global scope to execute in SyncBroadcastReceiver
172+ delay(1000L )
173+
174+ val items = database.itemDao().selectItems(localFeed.id)
175+
176+ assertTrue { items.first().isRead }
177+ assertTrue { items.first().isStarred }
178+ }
179+ }
180+
181+ @Test
182+ fun workerConflictTest () = runTest {
183+ val workManager = WorkManager .getInstance(context)
184+ val driver = WorkManagerTestInitHelper .getTestDriver(context)!!
185+
186+ val request1 = OneTimeWorkRequestBuilder <SyncWorker >()
187+ .addTag(SyncWorker .TAG )
188+ .addTag(SyncWorker .WORK_MANUAL )
189+ .setInputData(
190+ workDataOf(
191+ SyncWorker .ACCOUNT_ID_KEY to localAccount.id,
192+ SyncWorker .FEED_ID_KEY to localFeed.id
193+ )
194+ )
195+ .build()
196+
197+ val request2 = OneTimeWorkRequestBuilder <SyncWorker >()
198+ .addTag(SyncWorker .TAG )
199+ .addTag(SyncWorker .WORK_MANUAL )
200+ .setInputData(
201+ workDataOf(
202+ SyncWorker .ACCOUNT_ID_KEY to localAccount.id,
203+ SyncWorker .FEED_ID_KEY to localFeed.id
204+ )
205+ )
206+ .build()
207+
208+ workManager.enqueue(request1)
209+ workManager.enqueue(request2)
210+
211+ driver.setAllConstraintsMet(request1.id)
212+ driver.setAllConstraintsMet(request2.id)
213+
214+ val workInfos = listOf (
215+ workManager.getWorkInfoById(request1.id).get(),
216+ workManager.getWorkInfoById(request2.id).get()
217+ )
218+
219+ assertTrue { workInfos.any { it?.state == WorkInfo .State .FAILED } }
220+ val failedWorkInfo = workInfos.find { it?.state == WorkInfo .State .FAILED }!!
221+ assertEquals(true , failedWorkInfo.outputData.getBoolean(SyncWorker .SYNC_FAILURE_KEY , false ))
222+ assertNotNull { failedWorkInfo.outputData.getSerializable(SyncWorker .SYNC_FAILURE_EXCEPTION_KEY ) }
223+ }
224+
225+ @Test
226+ fun periodicLaunchTest () {
227+ val workManager = WorkManager .getInstance(context)
228+
229+ SyncWorker .startPeriodically(context, " 1" )
230+ var workInfo = workManager.getWorkInfosByTag(SyncWorker .WORK_AUTO ).get()
231+ .first()
232+ assertTrue { workInfo.state == WorkInfo .State .ENQUEUED }
233+ assertEquals(TimeUnit .HOURS .toMillis(1L ), workInfo.periodicityInfo?.repeatIntervalMillis)
234+
235+ SyncWorker .startPeriodically(context, " manual" )
236+ workInfo = workManager.getWorkInfoById(workInfo.id).get()!!
237+ assertTrue { workInfo.state == WorkInfo .State .CANCELLED }
238+ }
239+
240+ @Test
241+ fun exceptionTest () = runTest {
242+ val manualWorker =
243+ TestListenableWorkerBuilder .from<SyncWorker >(context, SyncWorker ::class .java)
244+ .setTags(listOf (SyncWorker .WORK_MANUAL ))
245+ .setInputData(
246+ workDataOf(
247+ SyncWorker .ACCOUNT_ID_KEY to feverAccount.id,
248+ )
249+ )
250+ .build()
251+
252+ val result = manualWorker.doWork()
253+
254+ assertTrue { result is ListenableWorker .Result .Failure }
255+ assertTrue { result.outputData.getBoolean(SyncWorker .SYNC_FAILURE_KEY , false ) }
256+ assertNotNull { result.outputData.getSerializable(SyncWorker .SYNC_FAILURE_EXCEPTION_KEY ) }
257+
258+ val autoWorker =
259+ TestListenableWorkerBuilder .from<SyncWorker >(context, SyncWorker ::class .java)
260+ .setTags(listOf (SyncWorker .WORK_AUTO ))
261+ .setInputData(
262+ workDataOf(
263+ SyncWorker .ACCOUNT_ID_KEY to feverAccount.id,
264+ )
265+ )
266+ .build()
267+
268+ val autoResult = autoWorker.doWork()
269+
270+ assertTrue { autoResult is ListenableWorker .Result .Failure }
271+ assertFalse { autoResult.outputData.getBoolean(SyncWorker .SYNC_FAILURE_KEY , false ) }
272+ }
273+
274+ @Test
275+ fun localAccountErrorTest () = runTest {
276+ mockServer.dispatcher = object : Dispatcher () {
277+
278+ override fun dispatch (request : RecordedRequest ): MockResponse {
279+ return MockResponse ()
280+ .setResponseCode(HttpURLConnection .HTTP_NOT_FOUND )
281+ }
282+ }
283+
284+ val worker = TestListenableWorkerBuilder .from<SyncWorker >(context, SyncWorker ::class .java)
285+ .setTags(listOf (SyncWorker .WORK_MANUAL ))
286+ .setInputData(
287+ workDataOf(
288+ SyncWorker .ACCOUNT_ID_KEY to localAccount.id,
289+ SyncWorker .FEED_ID_KEY to localFeed.id
290+ )
291+ )
292+ .build()
293+
294+ val result = worker.doWork()
295+
296+ assertTrue { result is ListenableWorker .Result .Success }
297+ assertNotNull { result.outputData.getSerializable(SyncWorker .LOCAL_SYNC_ERRORS_KEY ) }
298+ }
299+ }
0 commit comments