diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt index f253030a74..aee24c8a1b 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.test.FakePushProvider +import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory @@ -57,10 +58,7 @@ class DefaultPushServiceTest { @Test fun `test push ok`() = runTest { - val aConfig = CurrentUserPushConfig( - url = "aUrl", - pushKey = "aPushKey", - ) + val aConfig = aCurrentUserPushConfig() val testPushResult = lambdaRecorder { } val aPushProvider = FakePushProvider( currentUserPushConfig = aConfig diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt index 0ccd08df82..012c9c043e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.test import io.element.android.appconfig.PushConfig import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest -import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.runTest @@ -33,10 +33,7 @@ class DefaultTestPushTest { executeResult = executeResult, ) ) - val aConfig = CurrentUserPushConfig( - url = "aUrl", - pushKey = "aPushKey", - ) + val aConfig = aCurrentUserPushConfig() defaultTestPush.execute(aConfig) executeResult.assertions() .isCalledOnce() diff --git a/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/Fixtures.kt b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/Fixtures.kt new file mode 100644 index 0000000000..a5fd319dcb --- /dev/null +++ b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/Fixtures.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.test + +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig + +fun aCurrentUserPushConfig( + url: String = "aUrl", + pushKey: String = "aPushKey", +) = CurrentUserPushConfig( + url = url, + pushKey = pushKey, +) diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index d1c8cf5ee1..3651230721 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) + testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushstore.test) testImplementation(projects.tests.testutils) testImplementation(projects.services.toolbox.test) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt index b1d321f42f..82db2d6021 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt @@ -17,11 +17,13 @@ package io.element.android.libraries.pushproviders.unifiedpush object UnifiedPushConfig { + const val PUSH_GATEWAY_PATH: String = "_matrix/push/v1/notify" + /** * It is the push gateway for UnifiedPush. * Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify' */ - const val DEFAULT_PUSH_GATEWAY_HTTP_URL: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" + const val DEFAULT_PUSH_GATEWAY_HTTP_URL: String = "https://matrix.gateway.unifiedpush.org/$PUSH_GATEWAY_PATH" const val UNIFIED_PUSH_DISTRIBUTORS_URL = "https://unifiedpush.org/users/distributors/" diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt index c39c7ec066..9da92e7315 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.pushproviders.unifiedpush import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.AppScope import kotlinx.coroutines.withContext import timber.log.Timber @@ -34,12 +35,18 @@ class DefaultUnifiedPushGatewayResolver @Inject constructor( private val coroutineDispatchers: CoroutineDispatchers, ) : UnifiedPushGatewayResolver { override suspend fun getGateway(endpoint: String): String { - val gateway = UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL - try { - val url = URL(endpoint) + val url = tryOrNull( + onError = { Timber.d(it, "Cannot parse endpoint as an URL") } + ) { + URL(endpoint) + } + return if (url == null) { + Timber.d("Using default gateway") + UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL + } else { val port = if (url.port != -1) ":${url.port}" else "" val customBase = "${url.protocol}://${url.host}$port" - val customUrl = "$customBase/_matrix/push/v1/notify" + val customUrl = "$customBase/${UnifiedPushConfig.PUSH_GATEWAY_PATH}" Timber.i("Testing $customUrl") return withContext(coroutineDispatchers.io) { val api = unifiedPushApiFactory.create(customBase) @@ -47,16 +54,13 @@ class DefaultUnifiedPushGatewayResolver @Inject constructor( val discoveryResponse = api.discover() if (discoveryResponse.unifiedpush.gateway == "matrix") { Timber.d("Using custom gateway") - return@withContext customUrl } } catch (throwable: Throwable) { Timber.tag("UnifiedPushHelper").e(throwable) } - return@withContext gateway + // Always return the custom url. + customUrl } - } catch (e: Throwable) { - Timber.d(e, "Cannot try custom gateway") } - return gateway } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/network/UnifiedPushApi.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/network/UnifiedPushApi.kt index cd6cd7440e..6f9ff5679d 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/network/UnifiedPushApi.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/network/UnifiedPushApi.kt @@ -16,9 +16,10 @@ package io.element.android.libraries.pushproviders.unifiedpush.network +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import retrofit2.http.GET interface UnifiedPushApi { - @GET("_matrix/push/v1/notify") + @GET(UnifiedPushConfig.PUSH_GATEWAY_PATH) suspend fun discover(): DiscoveryResponse } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt new file mode 100644 index 0000000000..f11ae99965 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushApiFactory +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class UnifiedPushMatrixGatewayTest @Inject constructor( + private val unifiedPushApiFactory: UnifiedPushApiFactory, + private val coroutineDispatchers: CoroutineDispatchers, + private val pushProvider: PushProvider, +) : NotificationTroubleshootTest { + override val order = 450 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = "Test push gateway", + defaultDescription = "Ensure that the push gateway is valid.", + visibleWhenIdle = false, + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + override val state: StateFlow = delegate.state + + override fun isRelevant(data: TestFilterData): Boolean { + return data.currentPushProviderName == UnifiedPushConfig.NAME + } + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val config = pushProvider.getCurrentUserPushConfig() + if (config == null) { + delegate.updateState( + description = "No current push provider", + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } else { + val gatewayBaseUrl = config.url.removeSuffix("/${UnifiedPushConfig.PUSH_GATEWAY_PATH}") + // Checking if the gateway is a Matrix gateway + coroutineScope.launch(coroutineDispatchers.io) { + val api = unifiedPushApiFactory.create(gatewayBaseUrl) + try { + val discoveryResponse = api.discover() + if (discoveryResponse.unifiedpush.gateway == "matrix") { + delegate.updateState( + description = "${config.url} is a Matrix gateway.", + status = NotificationTroubleshootTestState.Status.Success + ) + } else { + delegate.updateState( + description = "${config.url} is not a Matrix gateway.", + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + } catch (throwable: Throwable) { + delegate.updateState( + description = "Fail to check the gateway ${config.url}: ${throwable.localizedMessage}", + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + } + } + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt index e180e36c38..cee74bfa31 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt @@ -25,23 +25,23 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test -class DefaultUnifiedPushGatewayResolverTest { - private val matrixDiscoveryResponse = { - DiscoveryResponse( - unifiedpush = DiscoveryUnifiedPush( - gateway = "matrix" - ) +internal val matrixDiscoveryResponse = { + DiscoveryResponse( + unifiedpush = DiscoveryUnifiedPush( + gateway = "matrix" ) - } + ) +} - private val invalidDiscoveryResponse = { - DiscoveryResponse( - unifiedpush = DiscoveryUnifiedPush( - gateway = "" - ) +internal val invalidDiscoveryResponse = { + DiscoveryResponse( + unifiedpush = DiscoveryUnifiedPush( + gateway = "" ) - } + ) +} +class DefaultUnifiedPushGatewayResolverTest { @Test fun `when a custom url provide a correct matrix gateway, the custom url is returned`() = runTest { val unifiedPushApiFactory = FakeUnifiedPushApiFactory( diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt new file mode 100644 index 0000000000..7b4d48070c --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushMatrixGatewayTestTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.libraries.pushproviders.test.FakePushProvider +import io.element.android.libraries.pushproviders.test.aCurrentUserPushConfig +import io.element.android.libraries.pushproviders.unifiedpush.FakeUnifiedPushApiFactory +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig +import io.element.android.libraries.pushproviders.unifiedpush.invalidDiscoveryResponse +import io.element.android.libraries.pushproviders.unifiedpush.matrixDiscoveryResponse +import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class UnifiedPushMatrixGatewayTestTest { + @Test + fun `test UnifiedPushMatrixGatewayTest success`() = runTest { + val sut = createUnifiedPushMatrixGatewayTest( + currentUserPushConfig = aCurrentUserPushConfig(), + discoveryResponse = matrixDiscoveryResponse, + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + @Test + fun `test UnifiedPushMatrixGatewayTest no config found`() = runTest { + val sut = createUnifiedPushMatrixGatewayTest( + currentUserPushConfig = null, + discoveryResponse = matrixDiscoveryResponse, + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } + + @Test + fun `test UnifiedPushMatrixGatewayTest not valid gateway`() = runTest { + val sut = createUnifiedPushMatrixGatewayTest( + currentUserPushConfig = aCurrentUserPushConfig(), + discoveryResponse = invalidDiscoveryResponse, + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + // Reset the error + sut.reset() + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + } + } + + @Test + fun `test UnifiedPushMatrixGatewayTest network error`() = runTest { + val sut = createUnifiedPushMatrixGatewayTest( + currentUserPushConfig = aCurrentUserPushConfig(), + discoveryResponse = { throw RuntimeException("Network error") }, + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } + + @Test + fun `test isRelevant`() = runTest { + val sut = createUnifiedPushMatrixGatewayTest() + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = UnifiedPushConfig.NAME))).isTrue() + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "other"))).isFalse() + } + + private fun TestScope.createUnifiedPushMatrixGatewayTest( + currentUserPushConfig: CurrentUserPushConfig? = null, + discoveryResponse: () -> DiscoveryResponse = matrixDiscoveryResponse, + ): UnifiedPushMatrixGatewayTest { + return UnifiedPushMatrixGatewayTest( + unifiedPushApiFactory = FakeUnifiedPushApiFactory(discoveryResponse), + coroutineDispatchers = testCoroutineDispatchers(), + pushProvider = FakePushProvider(currentUserPushConfig = currentUserPushConfig), + ) + } +}