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