Merge pull request #245 from ouchadam/tech/push-tests

Tech/Push registration tests
This commit is contained in:
Adam Brown 2022-11-03 16:59:02 +00:00 committed by GitHub
commit ab330c94d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 676 additions and 79 deletions

View File

@ -6,7 +6,11 @@ import kotlinx.coroutines.test.runTest
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) { fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
runTest { testBody(ExpectTest(coroutineContext)) } runTest {
val expectTest = ExpectTest(coroutineContext)
testBody(expectTest)
}
} }
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope { 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) }) expects.add(times to { block(this@expectUnit) })
} }
override fun <T> T.expect(times: Int, block: suspend MockKMatcherScope.(T) -> Unit) {
coJustRun { block(this@expect) }
expects.add(times to { block(this@expect) })
}
override fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) { override fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) {
groups.add { block(this@captureExpects) } groups.add { block(this@captureExpects) }
} }
@ -34,5 +43,6 @@ private fun Any.ignore() = Unit
interface ExpectTestScope : CoroutineScope { interface ExpectTestScope : CoroutineScope {
fun verifyExpects() fun verifyExpects()
fun <T> T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit) fun <T> T.expectUnit(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
fun <T> T.expect(times: Int = 1, block: suspend MockKMatcherScope.(T) -> Unit)
fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit) fun <T> T.captureExpects(block: suspend MockKMatcherScope.(T) -> Unit)
} }

View File

@ -10,4 +10,9 @@ dependencies {
implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.mavenCentral.kotlinSerializationJson
implementation Dependencies.jitPack.unifiedPush implementation Dependencies.jitPack.unifiedPush
kotlinTest(it)
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
} }

View File

@ -9,6 +9,7 @@ import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.domain.push.PushTokenRegistrarPreferences
import app.dapk.st.firebase.messaging.Messaging import app.dapk.st.firebase.messaging.Messaging
import app.dapk.st.push.messaging.MessagingPushTokenRegistrar import app.dapk.st.push.messaging.MessagingPushTokenRegistrar
import app.dapk.st.push.unifiedpush.UnifiedPushImpl
import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
class PushModule( class PushModule(
@ -21,15 +22,15 @@ class PushModule(
) : ProvidableModule { ) : ProvidableModule {
private val registrars by unsafeLazy { private val registrars by unsafeLazy {
val unifiedPush = UnifiedPushImpl(context)
PushTokenRegistrars( PushTokenRegistrars(
context,
MessagingPushTokenRegistrar( MessagingPushTokenRegistrar(
errorTracker, errorTracker,
pushHandler, pushHandler,
messaging, messaging,
), ),
UnifiedPushRegistrar(context), UnifiedPushRegistrar(context, unifiedPush),
PushTokenRegistrarPreferences(preferences) PushTokenRegistrarPreferences(preferences),
) )
} }

View File

@ -1,32 +1,30 @@
package app.dapk.st.push package app.dapk.st.push
import android.content.Context
import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.domain.push.PushTokenRegistrarPreferences
import app.dapk.st.push.messaging.MessagingPushTokenRegistrar import app.dapk.st.push.messaging.MessagingPushTokenRegistrar
import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar import app.dapk.st.push.unifiedpush.UnifiedPushRegistrar
import org.unifiedpush.android.connector.UnifiedPush
private val FIREBASE_OPTION = Registrar("Google - Firebase (FCM)") private val FIREBASE_OPTION = Registrar("Google - Firebase (FCM)")
private val NONE = Registrar("None") private val NONE = Registrar("None")
class PushTokenRegistrars( class PushTokenRegistrars(
private val context: Context,
private val messagingPushTokenRegistrar: MessagingPushTokenRegistrar, private val messagingPushTokenRegistrar: MessagingPushTokenRegistrar,
private val unifiedPushRegistrar: UnifiedPushRegistrar, private val unifiedPushRegistrar: UnifiedPushRegistrar,
private val pushTokenStore: PushTokenRegistrarPreferences, private val pushTokenStore: PushTokenRegistrarPreferences,
private val state: SelectionState = SelectionState(selection = null),
) : PushTokenRegistrar { ) : PushTokenRegistrar {
private var selection: Registrar? = null
fun options(): List<Registrar> { fun options(): List<Registrar> {
val messagingOption = when (messagingPushTokenRegistrar.isAvailable()) { val messagingOption = when (messagingPushTokenRegistrar.isAvailable()) {
true -> FIREBASE_OPTION true -> FIREBASE_OPTION
else -> null 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()) { private fun defaultSelection() = when (messagingPushTokenRegistrar.isAvailable()) {
true -> FIREBASE_OPTION true -> FIREBASE_OPTION
@ -34,7 +32,7 @@ class PushTokenRegistrars(
} }
suspend fun makeSelection(option: Registrar) { suspend fun makeSelection(option: Registrar) {
selection = option state.selection = option
pushTokenStore.store(option.id) pushTokenStore.store(option.id)
when (option) { when (option) {
NONE -> { NONE -> {
@ -66,7 +64,7 @@ class PushTokenRegistrars(
} }
override fun unregister() { override fun unregister() {
when (selection) { when (state.selection) {
FIREBASE_OPTION -> messagingPushTokenRegistrar.unregister() FIREBASE_OPTION -> messagingPushTokenRegistrar.unregister()
NONE -> { NONE -> {
runCatching { runCatching {
@ -86,4 +84,6 @@ class PushTokenRegistrars(
} }
@JvmInline @JvmInline
value class Registrar(val id: String) value class Registrar(val id: String)
data class SelectionState(var selection: Registrar?)

View File

@ -0,0 +1,20 @@
package app.dapk.st.push.unifiedpush
import android.content.Context
import org.unifiedpush.android.connector.UnifiedPush
interface UnifiedPush {
fun saveDistributor(distributor: String)
fun getDistributor(): String
fun getDistributors(): List<String>
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<String> = UnifiedPush.getDistributors(context)
override fun registerApp() = UnifiedPush.registerApp(context)
override fun unregisterApp() = UnifiedPush.unregisterApp(context)
}

View File

@ -0,0 +1,72 @@
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() },
private val endpointReader: suspend (URL) -> String = {
runCatching { it.openStream().use { String(it.readBytes()) } }.getOrNull() ?: ""
}
) {
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 = endpointReader(matrixEndpoint)
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,
)
}
}

View File

@ -3,56 +3,19 @@ package app.dapk.st.push.unifiedpush
import android.content.Context import android.content.Context
import app.dapk.st.core.AppLogTag import app.dapk.st.core.AppLogTag
import app.dapk.st.core.log 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 kotlinx.serialization.json.Json
import org.unifiedpush.android.connector.MessagingReceiver 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() { class UnifiedPushMessageReceiver : MessagingReceiver() {
private val scope = CoroutineScope(SupervisorJob()) private val delegate = UnifiedPushMessageDelegate()
override fun onMessage(context: Context, message: ByteArray, instance: String) { override fun onMessage(context: Context, message: ByteArray, instance: String) {
log(AppLogTag.PUSH, "UnifiedPush onMessage, $message") delegate.onMessage(context, message)
val module = context.module<PushModule>()
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) })
}
}
} }
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
log(AppLogTag.PUSH, "UnifiedPush onNewEndpoint $endpoint") delegate.onNewEndpoint(context, endpoint)
val module = context.module<PushModule>()
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))
}
}
} }
override fun onRegistrationFailed(context: Context, instance: String) { override fun onRegistrationFailed(context: Context, instance: String) {
@ -63,15 +26,4 @@ class UnifiedPushMessageReceiver : MessagingReceiver() {
log(AppLogTag.PUSH, "UnifiedPush onUnregistered") 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,
)
}
}

View File

@ -7,38 +7,41 @@ import app.dapk.st.core.AppLogTag
import app.dapk.st.core.log import app.dapk.st.core.log
import app.dapk.st.push.PushTokenRegistrar import app.dapk.st.push.PushTokenRegistrar
import app.dapk.st.push.Registrar import app.dapk.st.push.Registrar
import org.unifiedpush.android.connector.UnifiedPush
class UnifiedPushRegistrar( class UnifiedPushRegistrar(
private val context: Context, private val context: Context,
private val unifiedPush: UnifiedPush,
private val componentFactory: (Context) -> ComponentName = { ComponentName(it, UnifiedPushMessageReceiver::class.java) }
) : PushTokenRegistrar { ) : PushTokenRegistrar {
fun getDistributors() = unifiedPush.getDistributors().map { Registrar(it) }
fun registerSelection(registrar: Registrar) { fun registerSelection(registrar: Registrar) {
log(AppLogTag.PUSH, "UnifiedPush - register: $registrar") log(AppLogTag.PUSH, "UnifiedPush - register: $registrar")
UnifiedPush.saveDistributor(context, registrar.id) unifiedPush.saveDistributor(registrar.id)
registerApp() registerApp()
} }
override suspend fun registerCurrentToken() { override suspend fun registerCurrentToken() {
log(AppLogTag.PUSH, "UnifiedPush - register current token") log(AppLogTag.PUSH, "UnifiedPush - register current token")
if (UnifiedPush.getDistributor(context).isNotEmpty()) { if (unifiedPush.getDistributor().isNotEmpty()) {
registerApp() registerApp()
} }
} }
private fun registerApp() { private fun registerApp() {
context.packageManager.setComponentEnabledSetting( context.packageManager.setComponentEnabledSetting(
ComponentName(context, UnifiedPushMessageReceiver::class.java), componentFactory(context),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP, PackageManager.DONT_KILL_APP,
) )
UnifiedPush.registerApp(context) unifiedPush.registerApp()
} }
override fun unregister() { override fun unregister() {
UnifiedPush.unregisterApp(context) unifiedPush.unregisterApp()
context.packageManager.setComponentEnabledSetting( context.packageManager.setComponentEnabledSetting(
ComponentName(context, UnifiedPushMessageReceiver::class.java), componentFactory(context),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP, PackageManager.DONT_KILL_APP,
) )

View File

@ -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<MessagingPushTokenRegistrar>()
fun givenIsAvailable() = every { instance.isAvailable() }.delegateReturn()
}
class FakeUnifiedPushRegistrar {
val instance = mockk<UnifiedPushRegistrar>()
fun givenDistributors() = every { instance.getDistributors() }.delegateReturn()
}
class FakePushTokenRegistrarPreferences {
val instance = mockk<PushTokenRegistrarPreferences>()
fun givenCurrentSelection() = coEvery { instance.currentSelection() }.delegateReturn()
}

View File

@ -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<Messaging>()
fun givenIsAvailable() = every { instance.isAvailable() }.delegateReturn()
fun givenToken() = coEvery { instance.token() }.delegateReturn()
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -0,0 +1,95 @@
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.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<PushModule>()
init {
every { instance.dispatcher() }.returns(aCoroutineDispatchers())
}
fun givenPushHandler() = every { instance.pushHandler() }.delegateReturn()
}
class FakeEndpointReader : suspend (URL) -> String by mockk() {
fun given(url: String) = coEvery { this@FakeEndpointReader.invoke(URL(url)) }.delegateReturn()
}

View File

@ -0,0 +1,99 @@
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.amshove.kluent.shouldBeEqualTo
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() }
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() }
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)
}
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().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)
}
registrar.registerCurrentToken()
verifyExpects()
}
@Test
fun `given no distributor, when registering current token, then does nothing`() = runExpectTest {
fakeUnifiedPush.givenDistributor().returns("")
registrar.registerCurrentToken()
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() = every { getDistributor() }.delegateReturn()
fun givenDistributors() = every { getDistributors() }.delegateReturn()
}
class FakeComponentName {
val instance = mockk<ComponentName>()
}

View File

@ -1,8 +1,16 @@
package fake package fake
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import test.delegateReturn
class FakeContext { class FakeContext {
val instance = mockk<Context>() val instance = mockk<Context>()
fun givenPackageManager() = every { instance.packageManager }.delegateReturn()
}
class FakePackageManager {
val instance = mockk<PackageManager>()
} }

View File

@ -15,7 +15,6 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.junit.Test import org.junit.Test
import test.delegateReturn import test.delegateReturn
import test.expect
import test.runExpectTest import test.runExpectTest
private const val SUMMARY_ID = 101 private const val SUMMARY_ID = 101
@ -45,12 +44,13 @@ class NotificationRendererTest {
@Test @Test
fun `given removed rooms when rendering then cancels notifications with cancelled room ids`() = runExpectTest { fun `given removed rooms when rendering then cancels notifications with cancelled room ids`() = runExpectTest {
val removedRooms = setOf(aRoomId("id-1"), aRoomId("id-2")) val removedRooms = setOf(aRoomId("id-1"), aRoomId("id-2"))
fakeNotificationFactory.instance.expect { it.mapToNotifications(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet())) } val state = aNotificationState(removedRooms = removedRooms)
fakeNotificationManager.instance.expectUnit { fakeNotificationFactory.givenNotifications(state).returns(aNotifications())
removedRooms.forEach { removedRoom -> it.cancel(removedRoom.value, ROOM_MESSAGE_ID) } 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() verifyExpects()
} }

View File

@ -12,6 +12,7 @@ def excludes = [
'**/*Activity*', '**/*Activity*',
'**/*AndroidService*', '**/*AndroidService*',
'**/*Application*', '**/*Application*',
'**/*Receiver*',
// Generated // Generated
'**/*serializer*', '**/*serializer*',