Skip to content

Commit d9a67e2

Browse files
test(conversations): cover the periodic conversation sync worker
What is worth pinning down about a background worker is when it stands down and how many accounts it touches when it does not: a guard that quietly stops working is invisible until it turns up as battery drain or request volume, long after the change that broke it. Covers battery saver standing the run down without reading a single account, every configured account being synced rather than only the current one, a failed account asking for another attempt while its neighbours still sync, the attempt cap ending the retries, and an account lookup that throws being survivable. Unit tests gain the WorkManager testing artifact, which was available to instrumented tests only. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent c99c116 commit d9a67e2

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

‎app/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ dependencies {
372372
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
373373
androidTestImplementation("androidx.test:core-ktx:1.7.0")
374374
androidTestImplementation("org.mockito:mockito-android:5.22.0")
375+
testImplementation("androidx.work:work-testing:$workVersion")
375376
androidTestImplementation("androidx.work:work-testing:$workVersion")
376377
androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion") {
377378
exclude(group = "com.android.support", module = "support-annotations")
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Andy Scherzinger <info@andy-scherzinger.de>
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.talk.jobs
9+
10+
import android.app.Application
11+
import android.content.Context
12+
import android.os.PowerManager
13+
import androidx.test.core.app.ApplicationProvider
14+
import androidx.work.ListenableWorker
15+
import androidx.work.testing.TestListenableWorkerBuilder
16+
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
17+
import com.nextcloud.talk.data.user.model.User
18+
import com.nextcloud.talk.users.UserManager
19+
import kotlinx.coroutines.runBlocking
20+
import org.junit.Assert.assertEquals
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.mockito.kotlin.any
24+
import org.mockito.kotlin.mock
25+
import org.mockito.kotlin.never
26+
import org.mockito.kotlin.verifyBlocking
27+
import org.mockito.kotlin.wheneverBlocking
28+
import org.robolectric.RobolectricTestRunner
29+
import org.robolectric.Shadows.shadowOf
30+
import org.robolectric.annotation.Config
31+
32+
/**
33+
* Tests for [ConversationsSyncWorker]: which accounts a run syncs, when a run stands down without
34+
* syncing, and how a failed run reports itself.
35+
*/
36+
@RunWith(RobolectricTestRunner::class)
37+
@Config(application = Application::class, sdk = [33])
38+
class ConversationsSyncWorkerTest {
39+
40+
private val userManager: UserManager = mock()
41+
private val repository: OfflineConversationsRepository = mock()
42+
43+
@Test
44+
fun `every account is synced, not just the current one`() {
45+
val worker = worker()
46+
wheneverBlocking { userManager.getUsers() }.thenReturn(listOf(user(1), user(2), user(3)))
47+
wheneverBlocking { repository.syncRooms(any(), any()) }.thenReturn(true)
48+
49+
val result = runBlocking { worker.sync() }
50+
51+
assertEquals(ListenableWorker.Result.success(), result)
52+
verifyBlocking(repository) { syncRooms(user(1), false) }
53+
verifyBlocking(repository) { syncRooms(user(2), false) }
54+
verifyBlocking(repository) { syncRooms(user(3), false) }
55+
}
56+
57+
@Test
58+
fun `battery saver stands the sync down entirely`() {
59+
val worker = worker()
60+
shadowOf(applicationContext().getSystemService(Context.POWER_SERVICE) as PowerManager)
61+
.setIsPowerSaveMode(true)
62+
63+
val result = runBlocking { worker.sync() }
64+
65+
assertEquals(ListenableWorker.Result.success(), result)
66+
verifyBlocking(userManager, never()) { getUsers() }
67+
verifyBlocking(repository, never()) { syncRooms(any(), any()) }
68+
}
69+
70+
@Test
71+
fun `an account with nothing to sync is not an error`() {
72+
val worker = worker()
73+
wheneverBlocking { userManager.getUsers() }.thenReturn(emptyList())
74+
75+
val result = runBlocking { worker.sync() }
76+
77+
assertEquals(ListenableWorker.Result.success(), result)
78+
verifyBlocking(repository, never()) { syncRooms(any(), any()) }
79+
}
80+
81+
@Test
82+
fun `a failed account asks for another attempt`() {
83+
val worker = worker(runAttempt = 0)
84+
wheneverBlocking { userManager.getUsers() }.thenReturn(listOf(user(1), user(2)))
85+
wheneverBlocking { repository.syncRooms(user(1), false) }.thenReturn(true)
86+
wheneverBlocking { repository.syncRooms(user(2), false) }.thenReturn(false)
87+
88+
val result = runBlocking { worker.sync() }
89+
90+
assertEquals(ListenableWorker.Result.retry(), result)
91+
verifyBlocking(repository) { syncRooms(user(1), false) }
92+
}
93+
94+
@Test
95+
fun `a sync that keeps failing gives up instead of retrying for ever`() {
96+
val worker = worker(runAttempt = 2)
97+
wheneverBlocking { userManager.getUsers() }.thenReturn(listOf(user(1)))
98+
wheneverBlocking { repository.syncRooms(any(), any()) }.thenReturn(false)
99+
100+
val result = runBlocking { worker.sync() }
101+
102+
assertEquals(ListenableWorker.Result.failure(), result)
103+
}
104+
105+
@Test
106+
fun `an account whose lookup throws does not take the other accounts down with it`() {
107+
val worker = worker()
108+
wheneverBlocking { userManager.getUsers() }.thenThrow(IllegalStateException("database is gone"))
109+
110+
val result = runBlocking { worker.sync() }
111+
112+
assertEquals(ListenableWorker.Result.success(), result)
113+
verifyBlocking(repository, never()) { syncRooms(any(), any()) }
114+
}
115+
116+
private fun worker(runAttempt: Int = 0): ConversationsSyncWorker =
117+
TestListenableWorkerBuilder<ConversationsSyncWorker>(applicationContext())
118+
.setRunAttemptCount(runAttempt)
119+
.build()
120+
.also {
121+
it.userManager = userManager
122+
it.conversationsRepository = repository
123+
}
124+
125+
private fun applicationContext(): Context = ApplicationProvider.getApplicationContext()
126+
127+
private fun user(id: Long): User = User(id = id, userId = "user$id", username = "user$id", baseUrl = BASE_URL)
128+
129+
companion object {
130+
private const val BASE_URL = "https://server.example.com"
131+
}
132+
}

0 commit comments

Comments
 (0)