Skip to content

Commit 57c64bf

Browse files
committed
[WIP] Add UnifiedPushService test with workaround for PushService binder
1 parent 78a9680 commit 57c64bf

4 files changed

Lines changed: 143 additions & 4 deletions

File tree

app/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ dependencies {
137137
implementation(libs.kotlinx.coroutines)
138138
coreLibraryDesugaring(libs.android.desugaring)
139139

140+
// reflection
141+
implementation(kotlin("reflect")) // only for UnifiedPushService.resetBinder workaround, remove when not needed anymore
142+
140143
// Hilt
141144
implementation(libs.hilt.android.base)
142145
ksp(libs.androidx.hilt.compiler)
@@ -217,6 +220,7 @@ dependencies {
217220
androidTestImplementation(libs.androidx.work.testing)
218221
androidTestImplementation(libs.hilt.android.testing)
219222
androidTestImplementation(libs.junit)
223+
androidTestImplementation(libs.kotlinx.coroutines.test)
220224
androidTestImplementation(libs.mockk.android)
221225
androidTestImplementation(libs.okhttp.mockwebserver)
222226
androidTestImplementation(libs.room.testing)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*/
4+
5+
package at.bitfire.davdroid.push
6+
7+
import android.content.Context
8+
import android.content.Intent
9+
import android.os.IBinder
10+
import androidx.test.rule.ServiceTestRule
11+
import dagger.hilt.android.qualifiers.ApplicationContext
12+
import dagger.hilt.android.testing.BindValue
13+
import dagger.hilt.android.testing.HiltAndroidRule
14+
import dagger.hilt.android.testing.HiltAndroidTest
15+
import io.mockk.coVerify
16+
import io.mockk.confirmVerified
17+
import io.mockk.every
18+
import io.mockk.impl.annotations.RelaxedMockK
19+
import io.mockk.junit4.MockKRule
20+
import io.mockk.mockk
21+
import org.junit.After
22+
import org.junit.Before
23+
import org.junit.Rule
24+
import org.junit.Test
25+
import org.unifiedpush.android.connector.FailedReason
26+
import org.unifiedpush.android.connector.PushService
27+
import org.unifiedpush.android.connector.data.PushEndpoint
28+
import javax.inject.Inject
29+
30+
@HiltAndroidTest
31+
class UnifiedPushServiceTest {
32+
33+
@get:Rule
34+
val hiltRule = HiltAndroidRule(this)
35+
36+
@get:Rule
37+
val mockKRule = MockKRule(this)
38+
39+
@get:Rule
40+
val serviceTestRule = ServiceTestRule()
41+
42+
@Inject @ApplicationContext
43+
lateinit var context: Context
44+
45+
@RelaxedMockK @BindValue
46+
lateinit var pushRegistrationManager: PushRegistrationManager
47+
48+
lateinit var binder: IBinder
49+
lateinit var unifiedPushService: UnifiedPushService
50+
51+
52+
@Before
53+
fun setUp() {
54+
hiltRule.inject()
55+
56+
binder = serviceTestRule.bindService(Intent(context, UnifiedPushService::class.java))!!
57+
unifiedPushService = (binder as PushService.PushBinder).getService() as UnifiedPushService
58+
}
59+
60+
@After
61+
fun tearDown() {
62+
UnifiedPushService.resetBinder()
63+
}
64+
65+
66+
@Test
67+
fun testOnNewEndpoint_1() {
68+
val endpoint = mockk<PushEndpoint> {
69+
every { url } returns "https://example.com/12"
70+
}
71+
unifiedPushService.onNewEndpoint(endpoint, "12")
72+
73+
coVerify {
74+
pushRegistrationManager.processSubscription(12, endpoint)
75+
}
76+
confirmVerified(pushRegistrationManager)
77+
}
78+
79+
@Test
80+
fun testOnNewEndpoint_2() {
81+
val endpoint = mockk<PushEndpoint> {
82+
every { url } returns "https://example.com/34"
83+
}
84+
unifiedPushService.onNewEndpoint(endpoint, "34")
85+
86+
coVerify {
87+
pushRegistrationManager.processSubscription(34, endpoint)
88+
}
89+
confirmVerified(pushRegistrationManager)
90+
}
91+
92+
@Test
93+
fun testOnRegistrationFailed() {
94+
unifiedPushService.onRegistrationFailed(FailedReason.INTERNAL_ERROR, "34")
95+
96+
coVerify {
97+
pushRegistrationManager.removeSubscription(34)
98+
}
99+
confirmVerified(pushRegistrationManager)
100+
}
101+
102+
}

app/src/main/kotlin/at/bitfire/davdroid/push/UnifiedPushService.kt

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ import dagger.Lazy
1515
import dagger.hilt.android.AndroidEntryPoint
1616
import kotlinx.coroutines.Dispatchers
1717
import kotlinx.coroutines.runBlocking
18+
import org.jetbrains.annotations.TestOnly
1819
import org.unifiedpush.android.connector.FailedReason
1920
import org.unifiedpush.android.connector.PushService
2021
import org.unifiedpush.android.connector.data.PushEndpoint
2122
import org.unifiedpush.android.connector.data.PushMessage
2223
import java.util.logging.Level
2324
import java.util.logging.Logger
2425
import javax.inject.Inject
26+
import kotlin.reflect.KMutableProperty
27+
import kotlin.reflect.jvm.isAccessible
2528

2629
/**
2730
* Entry point for UnifiedPush.
@@ -59,10 +62,10 @@ class UnifiedPushService : PushService() {
5962

6063
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
6164
val serviceId = instance.toLongOrNull() ?: return
62-
logger.log(Level.FINE, "Got UnifiedPush endpoint for service $serviceId", endpoint.url)
65+
logger.warning("Got UnifiedPush endpoint for service $serviceId: ${endpoint.url}")
6366

6467
// register new endpoint at CalDAV/CardDAV servers
65-
runBlocking {
68+
runBlocking(Dispatchers.Default) {
6669
pushRegistrationManager.processSubscription(serviceId, endpoint)
6770
}
6871
}
@@ -72,7 +75,7 @@ class UnifiedPushService : PushService() {
7275
logger.warning("UnifiedPush registration failed for service $serviceId: $reason")
7376

7477
// unregister subscriptions
75-
runBlocking {
78+
runBlocking(Dispatchers.Default) {
7679
pushRegistrationManager.removeSubscription(serviceId)
7780
}
7881
}
@@ -81,7 +84,7 @@ class UnifiedPushService : PushService() {
8184
val serviceId = instance.toLongOrNull() ?: return
8285
logger.warning("UnifiedPush unregistered for service $serviceId")
8386

84-
runBlocking {
87+
runBlocking(Dispatchers.Default) {
8588
pushRegistrationManager.removeSubscription(serviceId)
8689
}
8790
}
@@ -145,4 +148,33 @@ class UnifiedPushService : PushService() {
145148
}
146149
}
147150

151+
152+
companion object {
153+
154+
/**
155+
* We need to reset PushService::Companion.binder to null before creating a new PushService with a new binder. The
156+
* current implementation caches the binder, which will always use the first UnifiedPushService that is created during the first
157+
* test. All following test will fail because the wrong binder is used by PushService.
158+
*
159+
* This method resets the binder using reflection, because it's not accessible directly.
160+
*
161+
* See https://codeberg.org/UnifiedPush/android-connector/issues/8
162+
*/
163+
@TestOnly
164+
fun resetBinder() {
165+
// requires kotlin-reflection
166+
val pushServiceClass = PushService::class
167+
val companionClass = pushServiceClass.nestedClasses.first { it.isCompanion }
168+
169+
val binderProperty = companionClass.members
170+
.filterIsInstance(KMutableProperty::class.java)
171+
.first { it.name.contains("binder") }
172+
binderProperty.isAccessible = true
173+
val binderSetter = binderProperty.setter
174+
175+
binderSetter.call(companionClass, null)
176+
}
177+
178+
}
179+
148180
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", vers
9393
junit = { module = "junit:junit", version = "4.13.2" }
9494
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
9595
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
96+
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
9697
mikepenz-aboutLibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "mikepenz-aboutLibraries" }
9798
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
9899
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }

0 commit comments

Comments
 (0)