From 093e5b64bbd8c60867125809fea89a8083968b07 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 13:11:01 +0000 Subject: [PATCH 1/7] splitting the unified push reciever logic to a testable delegate --- .../unifiedpush/UnifiedPushMessageDelegate.kt | 69 +++++++++++++++++++ .../unifiedpush/UnifiedPushMessageReceiver.kt | 56 ++------------- tools/coverage.gradle | 1 + 3 files changed, 74 insertions(+), 52 deletions(-) create mode 100644 domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt new file mode 100644 index 0000000..8556864 --- /dev/null +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt @@ -0,0 +1,69 @@ +package app.dapk.st.push.unifiedpush + +import android.content.Context +import app.dapk.st.core.AppLogTag +import app.dapk.st.core.log +import app.dapk.st.core.module +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.push.PushModule +import app.dapk.st.push.PushTokenPayload +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.net.URL + +private const val FALLBACK_UNIFIED_PUSH_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" +private val json = Json { ignoreUnknownKeys = true } + +class UnifiedPushMessageDelegate( + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()), + private val pushModuleProvider: (Context) -> PushModule = { it.module() } +) { + + fun onMessage(context: Context, message: ByteArray) { + log(AppLogTag.PUSH, "UnifiedPush onMessage, $message") + val module = pushModuleProvider(context) + val handler = module.pushHandler() + scope.launch { + withContext(module.dispatcher().io) { + val payload = json.decodeFromString(UnifiedPushMessagePayload.serializer(), String(message)) + handler.onMessageReceived(payload.notification.eventId?.let { EventId(it) }, payload.notification.roomId?.let { RoomId(it) }) + } + } + } + + fun onNewEndpoint(context: Context, endpoint: String) { + log(AppLogTag.PUSH, "UnifiedPush onNewEndpoint $endpoint") + val module = pushModuleProvider(context) + val handler = module.pushHandler() + scope.launch { + withContext(module.dispatcher().io) { + val matrixEndpoint = URL(endpoint).let { URL("${it.protocol}://${it.host}/_matrix/push/v1/notify") } + val content = runCatching { matrixEndpoint.openStream().use { String(it.readBytes()) } }.getOrNull() ?: "" + val gatewayUrl = when { + content.contains("\"gateway\":\"matrix\"") -> matrixEndpoint.toString() + else -> FALLBACK_UNIFIED_PUSH_GATEWAY + } + handler.onNewToken(PushTokenPayload(token = endpoint, gatewayUrl = gatewayUrl)) + } + } + } + + @Serializable + private data class UnifiedPushMessagePayload( + @SerialName("notification") val notification: Notification, + ) { + + @Serializable + data class Notification( + @SerialName("event_id") val eventId: String? = null, + @SerialName("room_id") val roomId: String? = null, + ) + } + +} \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt index 5a9176a..da8c8bf 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageReceiver.kt @@ -3,56 +3,19 @@ package app.dapk.st.push.unifiedpush import android.content.Context import app.dapk.st.core.AppLogTag import app.dapk.st.core.log -import app.dapk.st.core.module -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.push.PushModule -import app.dapk.st.push.PushTokenPayload -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.unifiedpush.android.connector.MessagingReceiver -import java.net.URL - -private val json = Json { ignoreUnknownKeys = true } - -private const val FALLBACK_UNIFIED_PUSH_GATEWAY = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" class UnifiedPushMessageReceiver : MessagingReceiver() { - private val scope = CoroutineScope(SupervisorJob()) + private val delegate = UnifiedPushMessageDelegate() override fun onMessage(context: Context, message: ByteArray, instance: String) { - log(AppLogTag.PUSH, "UnifiedPush onMessage, $message") - val module = context.module() - val handler = module.pushHandler() - scope.launch { - withContext(module.dispatcher().io) { - val payload = json.decodeFromString(UnifiedPushMessagePayload.serializer(), String(message)) - handler.onMessageReceived(payload.notification.eventId?.let { EventId(it) }, payload.notification.roomId?.let { RoomId(it) }) - } - } + delegate.onMessage(context, message) } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - log(AppLogTag.PUSH, "UnifiedPush onNewEndpoint $endpoint") - val module = context.module() - val handler = module.pushHandler() - scope.launch { - withContext(module.dispatcher().io) { - val matrixEndpoint = URL(endpoint).let { URL("${it.protocol}://${it.host}/_matrix/push/v1/notify") } - val content = runCatching { matrixEndpoint.openStream().use { String(it.readBytes()) } }.getOrNull() ?: "" - val gatewayUrl = when { - content.contains("\"gateway\":\"matrix\"") -> matrixEndpoint.toString() - else -> FALLBACK_UNIFIED_PUSH_GATEWAY - } - handler.onNewToken(PushTokenPayload(token = endpoint, gatewayUrl = gatewayUrl)) - } - } + delegate.onNewEndpoint(context, endpoint) } override fun onRegistrationFailed(context: Context, instance: String) { @@ -63,15 +26,4 @@ class UnifiedPushMessageReceiver : MessagingReceiver() { log(AppLogTag.PUSH, "UnifiedPush onUnregistered") } - @Serializable - private data class UnifiedPushMessagePayload( - @SerialName("notification") val notification: Notification, - ) { - - @Serializable - data class Notification( - @SerialName("event_id") val eventId: String? = null, - @SerialName("room_id") val roomId: String? = null, - ) - } -} \ No newline at end of file +} diff --git a/tools/coverage.gradle b/tools/coverage.gradle index 30770d4..660bb6c 100644 --- a/tools/coverage.gradle +++ b/tools/coverage.gradle @@ -12,6 +12,7 @@ def excludes = [ '**/*Activity*', '**/*AndroidService*', '**/*Application*', + '**/*Receiver*', // Generated '**/*serializer*', From 1e8d868348110fc5f268b2ef721d87d18b7f9b8a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 13:44:01 +0000 Subject: [PATCH 2/7] adding unified push delegate tests --- .../kotlin/test/ExpectTestScope.kt | 12 ++- domains/android/push/build.gradle | 4 + .../unifiedpush/UnifiedPushMessageDelegate.kt | 7 +- .../UnifiedPushMessageDelegateTest.kt | 98 +++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt diff --git a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt index f9f3db7..24dc40f 100644 --- a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt +++ b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt @@ -6,7 +6,11 @@ import kotlinx.coroutines.test.runTest import kotlin.coroutines.CoroutineContext fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) { - runTest { testBody(ExpectTest(coroutineContext)) } + runTest { + val expectTest = ExpectTest(coroutineContext) + testBody(expectTest) + } + } class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope { @@ -24,6 +28,11 @@ class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestSc expects.add(times to { block(this@expectUnit) }) } + override fun T.expect(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) { + coJustRun { block(this@expect) }.ignore() + expects.add(times to { block(this@expect) }) + } + override fun T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) { groups.add { block(this@captureExpects) } } @@ -34,5 +43,6 @@ private fun Any.ignore() = Unit interface ExpectTestScope : CoroutineScope { fun verifyExpects() fun T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit) + fun T.expect(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit) fun T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) } \ No newline at end of file diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle index 0e2d176..23398ef 100644 --- a/domains/android/push/build.gradle +++ b/domains/android/push/build.gradle @@ -10,4 +10,8 @@ dependencies { implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.jitPack.unifiedPush + + kotlinTest(it) + androidImportFixturesWorkaround(project, project(":core")) + androidImportFixturesWorkaround(project, project(":domains:android:stub")) } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt index 8556864..fc9a395 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegate.kt @@ -22,7 +22,10 @@ private val json = Json { ignoreUnknownKeys = true } class UnifiedPushMessageDelegate( private val scope: CoroutineScope = CoroutineScope(SupervisorJob()), - private val pushModuleProvider: (Context) -> PushModule = { it.module() } + private val pushModuleProvider: (Context) -> PushModule = { it.module() }, + private val endpointReader: suspend (URL) -> String = { + runCatching { it.openStream().use { String(it.readBytes()) } }.getOrNull() ?: "" + } ) { fun onMessage(context: Context, message: ByteArray) { @@ -44,7 +47,7 @@ class UnifiedPushMessageDelegate( scope.launch { withContext(module.dispatcher().io) { val matrixEndpoint = URL(endpoint).let { URL("${it.protocol}://${it.host}/_matrix/push/v1/notify") } - val content = runCatching { matrixEndpoint.openStream().use { String(it.readBytes()) } }.getOrNull() ?: "" + val content = endpointReader(matrixEndpoint) val gatewayUrl = when { content.contains("\"gateway\":\"matrix\"") -> matrixEndpoint.toString() else -> FALLBACK_UNIFIED_PUSH_GATEWAY diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt new file mode 100644 index 0000000..0ed357c --- /dev/null +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt @@ -0,0 +1,98 @@ +package app.dapk.st.push.unifiedpush + +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.push.PushHandler +import app.dapk.st.push.PushModule +import app.dapk.st.push.PushTokenPayload +import fake.FakeContext +import fixture.CoroutineDispatchersFixture.aCoroutineDispatchers +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.Test +import test.delegateReturn +import test.runExpectTest +import java.net.URL + +private val A_CONTEXT = FakeContext() +private const val A_ROOM_ID = "a room id" +private const val AN_EVENT_ID = "an event id" +private const val AN_ENDPOINT_HOST = "https://aendpointurl.com" +private const val AN_ENDPOINT = "$AN_ENDPOINT_HOST/with/path" +private const val A_GATEWAY_URL = "$AN_ENDPOINT_HOST/_matrix/push/v1/notify" +private const val FALLBACK_GATEWAY_URL = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" + +class UnifiedPushMessageDelegateTest { + + private val fakePushHandler = FakePushHandler() + private val fakeEndpointReader = FakeEndpointReader() + private val fakePushModule = FakePushModule().also { + it.givenPushHandler().returns(fakePushHandler) + } + + private val unifiedPushReceiver = UnifiedPushMessageDelegate( + CoroutineScope(UnconfinedTestDispatcher()), + pushModuleProvider = { _ -> fakePushModule.instance }, + endpointReader = fakeEndpointReader, + ) + + @Test + fun `parses incoming message payloads`() = runExpectTest { + fakePushHandler.expect { it.onMessageReceived(EventId(AN_EVENT_ID), RoomId(A_ROOM_ID)) } + val messageBytes = createMessage(A_ROOM_ID, AN_EVENT_ID) + + unifiedPushReceiver.onMessage(A_CONTEXT.instance, messageBytes) + + verifyExpects() + } + + @Test + fun `given endpoint is a gateway, then uses original endpoint url`() = runExpectTest { + fakeEndpointReader.given(A_GATEWAY_URL).returns("""{"unifiedpush":{"gateway":"matrix"}}""") + fakePushHandler.expect { it.onNewToken(PushTokenPayload(token = AN_ENDPOINT, gatewayUrl = A_GATEWAY_URL)) } + + unifiedPushReceiver.onNewEndpoint(A_CONTEXT.instance, AN_ENDPOINT) + + verifyExpects() + } + + @Test + fun `given endpoint is not a gateway, then uses fallback endpoint url`() = runExpectTest { + fakeEndpointReader.given(A_GATEWAY_URL).returns("") + fakePushHandler.expect { it.onNewToken(PushTokenPayload(token = AN_ENDPOINT, gatewayUrl = FALLBACK_GATEWAY_URL)) } + + unifiedPushReceiver.onNewEndpoint(A_CONTEXT.instance, AN_ENDPOINT) + + verifyExpects() + } + + private fun createMessage(roomId: String, eventId: String) = """ + { + "notification": { + "room_id": "$roomId", + "event_id": "$eventId" + } + } + """.trimIndent().toByteArray() +} + +class FakePushModule { + val instance = mockk() + + init { + every { instance.dispatcher() }.returns(aCoroutineDispatchers()) + } + + fun givenPushHandler() = every { instance.pushHandler() }.delegateReturn() +} + +class FakePushHandler : PushHandler by mockk() + +class FakeEndpointReader : suspend (URL) -> String by mockk() { + + fun given(url: String) = coEvery { this@FakeEndpointReader.invoke(URL(url)) }.delegateReturn() + +} \ No newline at end of file From 8545d0dab435944a3c10ddcdfc4da06af531c713 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 14:32:42 +0000 Subject: [PATCH 3/7] adding unified push registrar tests --- .../kotlin/app/dapk/st/push/PushModule.kt | 3 +- .../dapk/st/push/unifiedpush/UnifiedPush.kt | 11 +++ .../push/unifiedpush/UnifiedPushRegistrar.kt | 15 ++-- .../unifiedpush/UnifiedPushRegistrarTest.kt | 88 +++++++++++++++++++ .../testFixtures/kotlin/fake/FakeContext.kt | 8 ++ 5 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt create mode 100644 domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt index 67d12ae..221342e 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt @@ -9,6 +9,7 @@ import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.firebase.messaging.Messaging import app.dapk.st.push.messaging.MessagingPushTokenRegistrar +import app.dapk.st.push.unifiedpush.UnifiedPush import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar class PushModule( @@ -28,7 +29,7 @@ class PushModule( pushHandler, messaging, ), - UnifiedPushRegistrar(context), + UnifiedPushRegistrar(context, object : UnifiedPush {}), PushTokenRegistrarPreferences(preferences) ) } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt new file mode 100644 index 0000000..8ebf2d5 --- /dev/null +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt @@ -0,0 +1,11 @@ +package app.dapk.st.push.unifiedpush + +import android.content.Context +import org.unifiedpush.android.connector.UnifiedPush + +interface UnifiedPush { + fun saveDistributor(context: Context, distributor: String) = UnifiedPush.saveDistributor(context, distributor) + fun getDistributor(context: Context): String = UnifiedPush.getDistributor(context) + fun registerApp(context: Context) = UnifiedPush.registerApp(context) + fun unregisterApp(context: Context) = UnifiedPush.unregisterApp(context) +} \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt index cbb5ad5..ac1dc0f 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt @@ -7,38 +7,39 @@ import app.dapk.st.core.AppLogTag import app.dapk.st.core.log import app.dapk.st.push.PushTokenRegistrar import app.dapk.st.push.Registrar -import org.unifiedpush.android.connector.UnifiedPush class UnifiedPushRegistrar( private val context: Context, + private val unifiedPush: UnifiedPush, + private val componentFactory: (Context) -> ComponentName = { ComponentName(it, UnifiedPushMessageReceiver::class.java) } ) : PushTokenRegistrar { fun registerSelection(registrar: Registrar) { log(AppLogTag.PUSH, "UnifiedPush - register: $registrar") - UnifiedPush.saveDistributor(context, registrar.id) + unifiedPush.saveDistributor(context, registrar.id) registerApp() } override suspend fun registerCurrentToken() { log(AppLogTag.PUSH, "UnifiedPush - register current token") - if (UnifiedPush.getDistributor(context).isNotEmpty()) { + if (unifiedPush.getDistributor(context).isNotEmpty()) { registerApp() } } private fun registerApp() { context.packageManager.setComponentEnabledSetting( - ComponentName(context, UnifiedPushMessageReceiver::class.java), + componentFactory(context), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP, ) - UnifiedPush.registerApp(context) + unifiedPush.registerApp(context) } override fun unregister() { - UnifiedPush.unregisterApp(context) + unifiedPush.unregisterApp(context) context.packageManager.setComponentEnabledSetting( - ComponentName(context, UnifiedPushMessageReceiver::class.java), + componentFactory(context), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP, ) diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt new file mode 100644 index 0000000..993be26 --- /dev/null +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt @@ -0,0 +1,88 @@ +package app.dapk.st.push.unifiedpush + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import app.dapk.st.push.Registrar +import fake.FakeContext +import fake.FakePackageManager +import io.mockk.Called +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import test.delegateReturn +import test.runExpectTest + +private val A_COMPONENT_NAME = FakeComponentName() +private val A_REGISTRAR_SELECTION = Registrar("a-registrar") +private const val A_SAVED_DISTRIBUTOR = "a distributor" + +class UnifiedPushRegistrarTest { + + private val fakePackageManager = FakePackageManager() + private val fakeContext = FakeContext().also { + it.givenPackageManager().returns(fakePackageManager.instance) + } + private val fakeUnifiedPush = FakeUnifiedPush() + private val fakeComponentFactory = { _: Context -> A_COMPONENT_NAME.instance } + + private val registrar = UnifiedPushRegistrar(fakeContext.instance, fakeUnifiedPush, fakeComponentFactory) + + @Test + fun `when unregistering, then updates unified push and disables component`() = runExpectTest { + fakeUnifiedPush.expect { it.unregisterApp(fakeContext.instance) } + fakePackageManager.instance.expect { + it.setComponentEnabledSetting(A_COMPONENT_NAME.instance, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) + } + + registrar.unregister() + + verifyExpects() + } + + @Test + fun `when registering selection, then updates unified push and enables component`() = runExpectTest { + fakeUnifiedPush.expect { it.registerApp(fakeContext.instance) } + fakeUnifiedPush.expect { it.saveDistributor(fakeContext.instance, A_REGISTRAR_SELECTION.id) } + fakePackageManager.instance.expect { + it.setComponentEnabledSetting(A_COMPONENT_NAME.instance, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) + } + + registrar.registerSelection(A_REGISTRAR_SELECTION) + + verifyExpects() + } + + @Test + fun `given saved distributor, when registering current token, then updates unified push and enables component`() = runExpectTest { + fakeUnifiedPush.givenDistributor(fakeContext.instance).returns(A_SAVED_DISTRIBUTOR) + fakeUnifiedPush.expect { it.registerApp(fakeContext.instance) } + fakePackageManager.instance.expect { + it.setComponentEnabledSetting(A_COMPONENT_NAME.instance, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) + } + + registrar.registerCurrentToken() + + verifyExpects() + } + + @Test + fun `given no distributor, when registering current token, then does nothing`() = runExpectTest { + fakeUnifiedPush.givenDistributor(fakeContext.instance).returns("") + + registrar.registerCurrentToken() + + verify(exactly = 0) { fakeUnifiedPush.registerApp(any()) } + verify { fakePackageManager.instance wasNot Called } + } +} + + +class FakeUnifiedPush : UnifiedPush by mockk() { + fun givenDistributor(context: Context) = every { getDistributor(context) }.delegateReturn() +} + +class FakeComponentName { + val instance = mockk() +} \ No newline at end of file diff --git a/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt b/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt index 623c98c..122191e 100644 --- a/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt +++ b/domains/android/stub/src/testFixtures/kotlin/fake/FakeContext.kt @@ -1,8 +1,16 @@ package fake import android.content.Context +import android.content.pm.PackageManager +import io.mockk.every import io.mockk.mockk +import test.delegateReturn class FakeContext { val instance = mockk() + fun givenPackageManager() = every { instance.packageManager }.delegateReturn() +} + +class FakePackageManager { + val instance = mockk() } \ No newline at end of file From 1991468a010040f0df16effa971cd74cc7d2933e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 14:37:06 +0000 Subject: [PATCH 4/7] using unified push wrapper to allow for testing --- .../push/src/main/kotlin/app/dapk/st/push/PushModule.kt | 6 ++++-- .../src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt | 5 +++-- .../main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt index 221342e..92767c8 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt @@ -22,6 +22,7 @@ class PushModule( ) : ProvidableModule { private val registrars by unsafeLazy { + val unifiedPush = object : UnifiedPush {} PushTokenRegistrars( context, MessagingPushTokenRegistrar( @@ -29,8 +30,9 @@ class PushModule( pushHandler, messaging, ), - UnifiedPushRegistrar(context, object : UnifiedPush {}), - PushTokenRegistrarPreferences(preferences) + UnifiedPushRegistrar(context, unifiedPush), + PushTokenRegistrarPreferences(preferences), + unifiedPush, ) } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt index eb29104..5b4ccc9 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt @@ -3,8 +3,8 @@ package app.dapk.st.push import android.content.Context import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.push.messaging.MessagingPushTokenRegistrar +import app.dapk.st.push.unifiedpush.UnifiedPush import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar -import org.unifiedpush.android.connector.UnifiedPush private val FIREBASE_OPTION = Registrar("Google - Firebase (FCM)") private val NONE = Registrar("None") @@ -14,6 +14,7 @@ class PushTokenRegistrars( private val messagingPushTokenRegistrar: MessagingPushTokenRegistrar, private val unifiedPushRegistrar: UnifiedPushRegistrar, private val pushTokenStore: PushTokenRegistrarPreferences, + private val unifiedPush: UnifiedPush, ) : PushTokenRegistrar { private var selection: Registrar? = null @@ -23,7 +24,7 @@ class PushTokenRegistrars( true -> FIREBASE_OPTION else -> null } - return listOfNotNull(NONE, messagingOption) + UnifiedPush.getDistributors(context).map { Registrar(it) } + return listOfNotNull(NONE, messagingOption) + unifiedPush.getDistributors(context).map { Registrar(it) } } suspend fun currentSelection() = selection ?: (pushTokenStore.currentSelection()?.let { Registrar(it) } ?: defaultSelection()).also { selection = it } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt index 8ebf2d5..61429c4 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt @@ -6,6 +6,7 @@ import org.unifiedpush.android.connector.UnifiedPush interface UnifiedPush { fun saveDistributor(context: Context, distributor: String) = UnifiedPush.saveDistributor(context, distributor) fun getDistributor(context: Context): String = UnifiedPush.getDistributor(context) + fun getDistributors(context: Context): List = UnifiedPush.getDistributors(context) fun registerApp(context: Context) = UnifiedPush.registerApp(context) fun unregisterApp(context: Context) = UnifiedPush.unregisterApp(context) } \ No newline at end of file From b417b83fdfcdfb3a407d27cefc0fb36b9ce2cca8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 15:40:03 +0000 Subject: [PATCH 5/7] adding test around top level push registrar switching --- .../kotlin/app/dapk/st/push/PushModule.kt | 6 +- .../app/dapk/st/push/PushTokenRegistrars.kt | 21 +- .../dapk/st/push/unifiedpush/UnifiedPush.kt | 18 +- .../push/unifiedpush/UnifiedPushRegistrar.kt | 10 +- .../dapk/st/push/PushTokenRegistrarsTest.kt | 205 ++++++++++++++++++ .../unifiedpush/UnifiedPushRegistrarTest.kt | 27 ++- 6 files changed, 255 insertions(+), 32 deletions(-) create mode 100644 domains/android/push/src/test/kotlin/app/dapk/st/push/PushTokenRegistrarsTest.kt diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt index 92767c8..20963e9 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt @@ -9,7 +9,7 @@ import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.firebase.messaging.Messaging import app.dapk.st.push.messaging.MessagingPushTokenRegistrar -import app.dapk.st.push.unifiedpush.UnifiedPush +import app.dapk.st.push.unifiedpush.UnifiedPushImpl import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar class PushModule( @@ -22,9 +22,8 @@ class PushModule( ) : ProvidableModule { private val registrars by unsafeLazy { - val unifiedPush = object : UnifiedPush {} + val unifiedPush = UnifiedPushImpl(context) PushTokenRegistrars( - context, MessagingPushTokenRegistrar( errorTracker, pushHandler, @@ -32,7 +31,6 @@ class PushModule( ), UnifiedPushRegistrar(context, unifiedPush), PushTokenRegistrarPreferences(preferences), - unifiedPush, ) } diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt index 5b4ccc9..b87ef12 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushTokenRegistrars.kt @@ -1,33 +1,30 @@ package app.dapk.st.push -import android.content.Context import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.push.messaging.MessagingPushTokenRegistrar -import app.dapk.st.push.unifiedpush.UnifiedPush import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar private val FIREBASE_OPTION = Registrar("Google - Firebase (FCM)") private val NONE = Registrar("None") class PushTokenRegistrars( - private val context: Context, private val messagingPushTokenRegistrar: MessagingPushTokenRegistrar, private val unifiedPushRegistrar: UnifiedPushRegistrar, private val pushTokenStore: PushTokenRegistrarPreferences, - private val unifiedPush: UnifiedPush, + private val state: SelectionState = SelectionState(selection = null), ) : PushTokenRegistrar { - private var selection: Registrar? = null - fun options(): List { val messagingOption = when (messagingPushTokenRegistrar.isAvailable()) { true -> FIREBASE_OPTION else -> null } - return listOfNotNull(NONE, messagingOption) + unifiedPush.getDistributors(context).map { Registrar(it) } + return listOfNotNull(NONE, messagingOption) + unifiedPushRegistrar.getDistributors() } - suspend fun currentSelection() = selection ?: (pushTokenStore.currentSelection()?.let { Registrar(it) } ?: defaultSelection()).also { selection = it } + suspend fun currentSelection() = state.selection ?: (readStoredSelection() ?: defaultSelection()).also { state.selection = it } + + private suspend fun readStoredSelection() = pushTokenStore.currentSelection()?.let { Registrar(it) }?.takeIf { options().contains(it) } private fun defaultSelection() = when (messagingPushTokenRegistrar.isAvailable()) { true -> FIREBASE_OPTION @@ -35,7 +32,7 @@ class PushTokenRegistrars( } suspend fun makeSelection(option: Registrar) { - selection = option + state.selection = option pushTokenStore.store(option.id) when (option) { NONE -> { @@ -67,7 +64,7 @@ class PushTokenRegistrars( } override fun unregister() { - when (selection) { + when (state.selection) { FIREBASE_OPTION -> messagingPushTokenRegistrar.unregister() NONE -> { runCatching { @@ -87,4 +84,6 @@ class PushTokenRegistrars( } @JvmInline -value class Registrar(val id: String) \ No newline at end of file +value class Registrar(val id: String) + +data class SelectionState(var selection: Registrar?) diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt index 61429c4..55b706d 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPush.kt @@ -4,9 +4,17 @@ import android.content.Context import org.unifiedpush.android.connector.UnifiedPush interface UnifiedPush { - fun saveDistributor(context: Context, distributor: String) = UnifiedPush.saveDistributor(context, distributor) - fun getDistributor(context: Context): String = UnifiedPush.getDistributor(context) - fun getDistributors(context: Context): List = UnifiedPush.getDistributors(context) - fun registerApp(context: Context) = UnifiedPush.registerApp(context) - fun unregisterApp(context: Context) = UnifiedPush.unregisterApp(context) + fun saveDistributor(distributor: String) + fun getDistributor(): String + fun getDistributors(): List + fun registerApp() + fun unregisterApp() +} + +internal class UnifiedPushImpl(private val context: Context) : app.dapk.st.push.unifiedpush.UnifiedPush { + override fun saveDistributor(distributor: String) = UnifiedPush.saveDistributor(context, distributor) + override fun getDistributor(): String = UnifiedPush.getDistributor(context) + override fun getDistributors(): List = UnifiedPush.getDistributors(context) + override fun registerApp() = UnifiedPush.registerApp(context) + override fun unregisterApp() = UnifiedPush.unregisterApp(context) } \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt index ac1dc0f..713b9ad 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrar.kt @@ -14,15 +14,17 @@ class UnifiedPushRegistrar( private val componentFactory: (Context) -> ComponentName = { ComponentName(it, UnifiedPushMessageReceiver::class.java) } ) : PushTokenRegistrar { + fun getDistributors() = unifiedPush.getDistributors().map { Registrar(it) } + fun registerSelection(registrar: Registrar) { log(AppLogTag.PUSH, "UnifiedPush - register: $registrar") - unifiedPush.saveDistributor(context, registrar.id) + unifiedPush.saveDistributor(registrar.id) registerApp() } override suspend fun registerCurrentToken() { log(AppLogTag.PUSH, "UnifiedPush - register current token") - if (unifiedPush.getDistributor(context).isNotEmpty()) { + if (unifiedPush.getDistributor().isNotEmpty()) { registerApp() } } @@ -33,11 +35,11 @@ class UnifiedPushRegistrar( PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP, ) - unifiedPush.registerApp(context) + unifiedPush.registerApp() } override fun unregister() { - unifiedPush.unregisterApp(context) + unifiedPush.unregisterApp() context.packageManager.setComponentEnabledSetting( componentFactory(context), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/PushTokenRegistrarsTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/PushTokenRegistrarsTest.kt new file mode 100644 index 0000000..3fc1cac --- /dev/null +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/PushTokenRegistrarsTest.kt @@ -0,0 +1,205 @@ +package app.dapk.st.push + +import app.dapk.st.domain.push.PushTokenRegistrarPreferences +import app.dapk.st.push.messaging.MessagingPushTokenRegistrar +import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import test.delegateReturn +import test.runExpectTest + +private val UNIFIED_PUSH = Registrar("unified-push option") +private val NONE = Registrar("None") +private val FIREBASE = Registrar("Google - Firebase (FCM)") +private val UNIFIED_PUSH_DISTRIBUTORS = listOf(UNIFIED_PUSH) + +class PushTokenRegistrarsTest { + + private val fakeMessagingPushRegistrar = FakeMessagingPushRegistrar() + private val fakeUnifiedPushRegistrar = FakeUnifiedPushRegistrar() + private val fakePushTokenRegistrarPreferences = FakePushTokenRegistrarPreferences() + private val selectionState = SelectionState(selection = null) + + private val registrars = PushTokenRegistrars( + fakeMessagingPushRegistrar.instance, + fakeUnifiedPushRegistrar.instance, + fakePushTokenRegistrarPreferences.instance, + selectionState, + ) + + @Test + fun `given messaging is available, when reading options, then returns firebase and unified push`() { + fakeMessagingPushRegistrar.givenIsAvailable().returns(true) + fakeUnifiedPushRegistrar.givenDistributors().returns(UNIFIED_PUSH_DISTRIBUTORS) + + val result = registrars.options() + + result shouldBeEqualTo listOf(Registrar("None"), FIREBASE) + UNIFIED_PUSH_DISTRIBUTORS + } + + @Test + fun `given messaging is not available, when reading options, then returns unified push`() { + fakeMessagingPushRegistrar.givenIsAvailable().returns(false) + fakeUnifiedPushRegistrar.givenDistributors().returns(UNIFIED_PUSH_DISTRIBUTORS) + + val result = registrars.options() + + result shouldBeEqualTo listOf(Registrar("None")) + UNIFIED_PUSH_DISTRIBUTORS + } + + @Test + fun `given no saved selection and messaging is not available, when reading default selection, then returns none`() = runTest { + fakePushTokenRegistrarPreferences.givenCurrentSelection().returns(null) + fakeMessagingPushRegistrar.givenIsAvailable().returns(false) + + val result = registrars.currentSelection() + + result shouldBeEqualTo NONE + } + + @Test + fun `given no saved selection and messaging is available, when reading default selection, then returns firebase`() = runTest { + fakePushTokenRegistrarPreferences.givenCurrentSelection().returns(null) + fakeMessagingPushRegistrar.givenIsAvailable().returns(true) + + val result = registrars.currentSelection() + + result shouldBeEqualTo FIREBASE + } + + @Test + fun `given saved selection and is a option, when reading default selection, then returns selection`() = runTest { + fakeUnifiedPushRegistrar.givenDistributors().returns(UNIFIED_PUSH_DISTRIBUTORS) + fakePushTokenRegistrarPreferences.givenCurrentSelection().returns(FIREBASE.id) + fakeMessagingPushRegistrar.givenIsAvailable().returns(true) + + val result = registrars.currentSelection() + + result shouldBeEqualTo FIREBASE + } + + @Test + fun `given saved selection and is not an option, when reading default selection, then returns next default`() = runTest { + fakeUnifiedPushRegistrar.givenDistributors().returns(UNIFIED_PUSH_DISTRIBUTORS) + fakePushTokenRegistrarPreferences.givenCurrentSelection().returns(FIREBASE.id) + fakeMessagingPushRegistrar.givenIsAvailable().returns(false) + + val result = registrars.currentSelection() + + result shouldBeEqualTo NONE + } + + @Test + fun `when selecting none, then stores and unregisters`() = runExpectTest { + fakePushTokenRegistrarPreferences.instance.expect { it.store(NONE.id) } + fakeMessagingPushRegistrar.instance.expect { it.unregister() } + fakeUnifiedPushRegistrar.instance.expect { it.unregister() } + + registrars.makeSelection(NONE) + + verifyExpects() + } + + @Test + fun `when selecting firebase, then stores and unregisters unifiedpush`() = runExpectTest { + fakePushTokenRegistrarPreferences.instance.expect { it.store(FIREBASE.id) } + fakeMessagingPushRegistrar.instance.expect { it.registerCurrentToken() } + fakeUnifiedPushRegistrar.instance.expect { it.unregister() } + + registrars.makeSelection(FIREBASE) + + verifyExpects() + } + + @Test + fun `when selecting unified push, then stores and unregisters firebase`() = runExpectTest { + fakePushTokenRegistrarPreferences.instance.expect { it.store(UNIFIED_PUSH.id) } + fakeMessagingPushRegistrar.instance.expect { it.unregister() } + fakeUnifiedPushRegistrar.instance.expect { it.registerSelection(UNIFIED_PUSH) } + + registrars.makeSelection(UNIFIED_PUSH) + + verifyExpects() + } + + @Test + fun `given unified push selected, when registering current token, then delegates`() = runExpectTest { + selectionState.selection = UNIFIED_PUSH + fakeUnifiedPushRegistrar.instance.expect { it.registerCurrentToken() } + + registrars.registerCurrentToken() + + verifyExpects() + } + + @Test + fun `given firebase selected, when registering current token, then delegates`() = runExpectTest { + selectionState.selection = FIREBASE + fakeMessagingPushRegistrar.instance.expect { it.registerCurrentToken() } + + registrars.registerCurrentToken() + + verifyExpects() + } + + @Test + fun `given none selected, when registering current token, then does nothing`() = runExpectTest { + selectionState.selection = NONE + + registrars.registerCurrentToken() + + verify { fakeMessagingPushRegistrar.instance wasNot Called } + verify { fakeUnifiedPushRegistrar.instance wasNot Called } + } + + @Test + fun `given unified push selected, when unregistering, then delegates`() = runExpectTest { + selectionState.selection = UNIFIED_PUSH + fakeUnifiedPushRegistrar.instance.expect { it.unregister() } + + registrars.unregister() + + verifyExpects() + } + + @Test + fun `given firebase selected, when unregistering, then delegates`() = runExpectTest { + selectionState.selection = FIREBASE + fakeMessagingPushRegistrar.instance.expect { it.unregister() } + + registrars.unregister() + + verifyExpects() + } + + @Test + fun `given none selected, when unregistering, then unregisters all`() = runExpectTest { + selectionState.selection = NONE + fakeUnifiedPushRegistrar.instance.expect { it.unregister() } + fakeMessagingPushRegistrar.instance.expect { it.unregister() } + + registrars.unregister() + + verifyExpects() + } +} + +class FakeMessagingPushRegistrar { + val instance = mockk() + + fun givenIsAvailable() = every { instance.isAvailable() }.delegateReturn() +} + +class FakeUnifiedPushRegistrar { + val instance = mockk() + + fun givenDistributors() = every { instance.getDistributors() }.delegateReturn() +} + +class FakePushTokenRegistrarPreferences { + val instance = mockk() + + fun givenCurrentSelection() = coEvery { instance.currentSelection() }.delegateReturn() +} \ No newline at end of file diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt index 993be26..3fb1ec8 100644 --- a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushRegistrarTest.kt @@ -10,6 +10,7 @@ import io.mockk.Called import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import test.delegateReturn import test.runExpectTest @@ -31,7 +32,7 @@ class UnifiedPushRegistrarTest { @Test fun `when unregistering, then updates unified push and disables component`() = runExpectTest { - fakeUnifiedPush.expect { it.unregisterApp(fakeContext.instance) } + fakeUnifiedPush.expect { it.unregisterApp() } fakePackageManager.instance.expect { it.setComponentEnabledSetting(A_COMPONENT_NAME.instance, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) } @@ -43,8 +44,8 @@ class UnifiedPushRegistrarTest { @Test fun `when registering selection, then updates unified push and enables component`() = runExpectTest { - fakeUnifiedPush.expect { it.registerApp(fakeContext.instance) } - fakeUnifiedPush.expect { it.saveDistributor(fakeContext.instance, A_REGISTRAR_SELECTION.id) } + fakeUnifiedPush.expect { it.registerApp() } + fakeUnifiedPush.expect { it.saveDistributor(A_REGISTRAR_SELECTION.id) } fakePackageManager.instance.expect { it.setComponentEnabledSetting(A_COMPONENT_NAME.instance, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) } @@ -56,8 +57,8 @@ class UnifiedPushRegistrarTest { @Test fun `given saved distributor, when registering current token, then updates unified push and enables component`() = runExpectTest { - fakeUnifiedPush.givenDistributor(fakeContext.instance).returns(A_SAVED_DISTRIBUTOR) - fakeUnifiedPush.expect { it.registerApp(fakeContext.instance) } + fakeUnifiedPush.givenDistributor().returns(A_SAVED_DISTRIBUTOR) + fakeUnifiedPush.expect { it.registerApp() } fakePackageManager.instance.expect { it.setComponentEnabledSetting(A_COMPONENT_NAME.instance, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) } @@ -69,18 +70,28 @@ class UnifiedPushRegistrarTest { @Test fun `given no distributor, when registering current token, then does nothing`() = runExpectTest { - fakeUnifiedPush.givenDistributor(fakeContext.instance).returns("") + fakeUnifiedPush.givenDistributor().returns("") registrar.registerCurrentToken() - verify(exactly = 0) { fakeUnifiedPush.registerApp(any()) } + verify(exactly = 0) { fakeUnifiedPush.registerApp() } verify { fakePackageManager.instance wasNot Called } } + + @Test + fun `given distributors, then returns them as Registrars`() { + fakeUnifiedPush.givenDistributors().returns(listOf("a", "b")) + + val result = registrar.getDistributors() + + result shouldBeEqualTo listOf(Registrar("a"), Registrar("b")) + } } class FakeUnifiedPush : UnifiedPush by mockk() { - fun givenDistributor(context: Context) = every { getDistributor(context) }.delegateReturn() + fun givenDistributor() = every { getDistributor() }.delegateReturn() + fun givenDistributors() = every { getDistributors() }.delegateReturn() } class FakeComponentName { From d28ea9c67fafe7d90a71f401142346f80ea8ed81 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 15:57:55 +0000 Subject: [PATCH 6/7] adding tests around the firebase registrar --- domains/android/push/build.gradle | 1 + .../MessagingPushTokenRegistrarTest.kt | 79 +++++++++++++++++++ .../messaging/MessagingServiceAdapterTest.kt | 41 ++++++++++ .../st/push/unifiedpush/FakePushHandler.kt | 6 ++ .../UnifiedPushMessageDelegateTest.kt | 3 - 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrarTest.kt create mode 100644 domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapterTest.kt create mode 100644 domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/FakePushHandler.kt diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle index 23398ef..e057b0f 100644 --- a/domains/android/push/build.gradle +++ b/domains/android/push/build.gradle @@ -13,5 +13,6 @@ dependencies { kotlinTest(it) androidImportFixturesWorkaround(project, project(":core")) + androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) } diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrarTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrarTest.kt new file mode 100644 index 0000000..6f2b4f9 --- /dev/null +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingPushTokenRegistrarTest.kt @@ -0,0 +1,79 @@ +package app.dapk.st.push.messaging + +import app.dapk.st.firebase.messaging.Messaging +import app.dapk.st.push.PushTokenPayload +import app.dapk.st.push.unifiedpush.FakePushHandler +import fake.FakeErrorTracker +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import test.delegateReturn +import test.runExpectTest + +private const val A_TOKEN = "a-token" +private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify" +private val AN_ERROR = RuntimeException() + +class MessagingPushTokenRegistrarTest { + + private val fakePushHandler = FakePushHandler() + private val fakeErrorTracker = FakeErrorTracker() + private val fakeMessaging = FakeMessaging() + + private val registrar = MessagingPushTokenRegistrar( + fakeErrorTracker, + fakePushHandler, + fakeMessaging.instance, + ) + + @Test + fun `when checking isAvailable, then delegates`() = runExpectTest { + fakeMessaging.givenIsAvailable().returns(true) + + val result = registrar.isAvailable() + + result shouldBeEqualTo true + } + + @Test + fun `when registering current token, then enables and forwards current token to handler`() = runExpectTest { + fakeMessaging.instance.expect { it.enable() } + fakePushHandler.expect { it.onNewToken(PushTokenPayload(A_TOKEN, SYGNAL_GATEWAY)) } + fakeMessaging.givenToken().returns(A_TOKEN) + + registrar.registerCurrentToken() + + verifyExpects() + } + + @Test + fun `given fails to register, when registering current token, then tracks error`() = runExpectTest { + fakeMessaging.instance.expect { it.enable() } + fakeMessaging.givenToken().throws(AN_ERROR) + fakeErrorTracker.expect { it.track(AN_ERROR) } + + registrar.registerCurrentToken() + + verifyExpects() + } + + @Test + fun `when unregistering, then deletes token and disables`() = runExpectTest { + fakeMessaging.instance.expect { it.deleteToken() } + fakeMessaging.instance.expect { it.disable() } + + registrar.unregister() + + verifyExpects() + } + +} + +class FakeMessaging { + val instance = mockk() + + fun givenIsAvailable() = every { instance.isAvailable() }.delegateReturn() + fun givenToken() = coEvery { instance.token() }.delegateReturn() +} \ No newline at end of file diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapterTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapterTest.kt new file mode 100644 index 0000000..26ca2cf --- /dev/null +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/messaging/MessagingServiceAdapterTest.kt @@ -0,0 +1,41 @@ +package app.dapk.st.push.messaging + +import app.dapk.st.push.PushTokenPayload +import app.dapk.st.push.unifiedpush.FakePushHandler +import fixture.aRoomId +import fixture.anEventId +import org.junit.Test +import test.runExpectTest + +private const val A_TOKEN = "a-push-token" +private const val SYGNAL_GATEWAY = "https://sygnal.dapk.app/_matrix/push/v1/notify" +private val A_ROOM_ID = aRoomId() +private val AN_EVENT_ID = anEventId() + +class MessagingServiceAdapterTest { + + private val fakePushHandler = FakePushHandler() + + private val messagingServiceAdapter = MessagingServiceAdapter(fakePushHandler) + + @Test + fun `onNewToken, then delegates to push handler`() = runExpectTest { + fakePushHandler.expect { + it.onNewToken(PushTokenPayload(token = A_TOKEN, gatewayUrl = SYGNAL_GATEWAY)) + } + messagingServiceAdapter.onNewToken(A_TOKEN) + + verifyExpects() + } + + + @Test + fun `onMessageReceived, then delegates to push handler`() = runExpectTest { + fakePushHandler.expect { + it.onMessageReceived(AN_EVENT_ID, A_ROOM_ID) + } + messagingServiceAdapter.onMessageReceived(AN_EVENT_ID, A_ROOM_ID) + + verifyExpects() + } +} diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/FakePushHandler.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/FakePushHandler.kt new file mode 100644 index 0000000..64d0243 --- /dev/null +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/FakePushHandler.kt @@ -0,0 +1,6 @@ +package app.dapk.st.push.unifiedpush + +import app.dapk.st.push.PushHandler +import io.mockk.mockk + +class FakePushHandler : PushHandler by mockk() \ No newline at end of file diff --git a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt index 0ed357c..c6fd8b1 100644 --- a/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt +++ b/domains/android/push/src/test/kotlin/app/dapk/st/push/unifiedpush/UnifiedPushMessageDelegateTest.kt @@ -2,7 +2,6 @@ package app.dapk.st.push.unifiedpush import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId -import app.dapk.st.push.PushHandler import app.dapk.st.push.PushModule import app.dapk.st.push.PushTokenPayload import fake.FakeContext @@ -89,8 +88,6 @@ class FakePushModule { fun givenPushHandler() = every { instance.pushHandler() }.delegateReturn() } -class FakePushHandler : PushHandler by mockk() - class FakeEndpointReader : suspend (URL) -> String by mockk() { fun given(url: String) = coEvery { this@FakeEndpointReader.invoke(URL(url)) }.delegateReturn() From 67be48a48e9ca4075cddb735acc81c51064cecfc Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 3 Nov 2022 16:18:11 +0000 Subject: [PATCH 7/7] fixing previous false positive --- core/src/testFixtures/kotlin/test/ExpectTestScope.kt | 2 +- .../st/notifications/NotificationRendererTest.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt index 24dc40f..bff2b8f 100644 --- a/core/src/testFixtures/kotlin/test/ExpectTestScope.kt +++ b/core/src/testFixtures/kotlin/test/ExpectTestScope.kt @@ -29,7 +29,7 @@ class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestSc } override fun T.expect(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) { - coJustRun { block(this@expect) }.ignore() + coJustRun { block(this@expect) } expects.add(times to { block(this@expect) }) } diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt index 724aa50..6875205 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt @@ -15,7 +15,6 @@ import io.mockk.every import io.mockk.mockk import org.junit.Test import test.delegateReturn -import test.expect import test.runExpectTest private const val SUMMARY_ID = 101 @@ -45,12 +44,13 @@ class NotificationRendererTest { @Test fun `given removed rooms when rendering then cancels notifications with cancelled room ids`() = runExpectTest { val removedRooms = setOf(aRoomId("id-1"), aRoomId("id-2")) - fakeNotificationFactory.instance.expect { it.mapToNotifications(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet())) } - fakeNotificationManager.instance.expectUnit { - removedRooms.forEach { removedRoom -> it.cancel(removedRoom.value, ROOM_MESSAGE_ID) } - } + val state = aNotificationState(removedRooms = removedRooms) + fakeNotificationFactory.givenNotifications(state).returns(aNotifications()) + fakeNotificationManager.instance.expectUnit { removedRooms.forEach { removedRoom -> it.cancel(removedRoom.value, ROOM_MESSAGE_ID) } } + fakeNotificationManager.instance.expectUnit { it.cancel(SUMMARY_ID) } + + notificationRenderer.render(state) - notificationRenderer.render(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet())) verifyExpects() }