From dbe5bb767b64f55212056d36bd874603532b4e73 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 8 Aug 2025 12:21:36 +0200 Subject: [PATCH 01/14] Extract code to retrieve .well-known files to its own modules. --- features/login/impl/build.gradle.kts | 5 +- .../DefaultAccountProviderAccessControl.kt | 7 +- .../ElementWellknownRetriever.kt | 42 -------- .../login/impl/resolver/HomeserverResolver.kt | 11 ++- .../network/DefaultWellknownRequest.kt | 27 ------ .../impl/resolver/network/WellknownRequest.kt | 15 --- .../WebClientUrlForAuthenticationRetriever.kt | 20 +--- ...DefaultAccountProviderAccessControlTest.kt | 7 +- .../FakeElementWellknownRetriever.kt | 19 ---- .../changeserver/ChangeServerPresenterTest.kt | 18 ++-- .../resolver/network/FakeWellknownRequest.kt | 19 ---- .../onboarding/OnBoardingPresenterTest.kt | 8 +- .../qrcode/scan/QrCodeScanPresenterTest.kt | 8 +- .../SearchAccountProviderPresenterTest.kt | 97 ++++++++++++++++--- libraries/wellknown/api/build.gradle.kts | 13 +++ .../wellknown/api/ElementWellKnown.kt | 13 +++ .../libraries/wellknown/api/WellKnown.kt | 17 ++++ .../wellknown/api/WellknownRetriever.kt | 13 +++ libraries/wellknown/impl/build.gradle.kts | 32 ++++++ .../impl/DefaultWellknownRetriever.kt | 69 +++++++++++++ .../impl/InternalElementWellKnown.kt | 6 +- .../wellknown/impl/InternalWellKnown.kt | 15 +-- .../impl/InternalWellKnownBaseConfig.kt | 4 +- .../libraries/wellknown/impl}/WellknownAPI.kt | 6 +- libraries/wellknown/test/build.gradle.kts | 19 ++++ .../wellknown/test/FakeWellknownRetriever.kt | 26 +++++ 26 files changed, 337 insertions(+), 199 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/ElementWellknownRetriever.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt delete mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/FakeElementWellknownRetriever.kt delete mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt create mode 100644 libraries/wellknown/api/build.gradle.kts create mode 100644 libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt create mode 100644 libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt create mode 100644 libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt create mode 100644 libraries/wellknown/impl/build.gradle.kts create mode 100644 libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt => libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt (80%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt => libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt (65%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt => libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt (84%) rename {features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network => libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl}/WellknownAPI.kt (67%) create mode 100644 libraries/wellknown/test/build.gradle.kts create mode 100644 libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index a4f20fb97d..ec01f6012a 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -36,7 +36,6 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api) - implementation(projects.libraries.network) implementation(projects.libraries.designsystem) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) @@ -44,10 +43,9 @@ dependencies { implementation(projects.libraries.qrcode) implementation(projects.libraries.oidc.api) implementation(projects.libraries.uiUtils) + implementation(projects.libraries.wellknown.api) implementation(libs.androidx.browser) - implementation(platform(libs.network.retrofit.bom)) implementation(libs.androidx.webkit) - implementation(libs.network.retrofit) implementation(libs.serialization.json) api(projects.features.login.api) @@ -65,6 +63,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oidc.test) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.libraries.wellknown.test) testImplementation(projects.tests.testutils) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt index d591e97f6f..bda1fa1c4b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControl.kt @@ -13,12 +13,13 @@ import io.element.android.features.login.api.accesscontrol.AccountProviderAccess import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.di.AppScope +import io.element.android.libraries.wellknown.api.WellknownRetriever import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultAccountProviderAccessControl @Inject constructor( private val enterpriseService: EnterpriseService, - private val elementWellknownRetriever: ElementWellknownRetriever, + private val wellknownRetriever: WellknownRetriever, ) : AccountProviderAccessControl { override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try { assertIsAllowedToConnectToAccountProvider( @@ -37,8 +38,8 @@ class DefaultAccountProviderAccessControl @Inject constructor( ) { if (enterpriseService.isEnterpriseBuild.not()) { // Ensure that Element Pro is not required for this account provider - val wellKnown = elementWellknownRetriever.retrieve( - accountProviderUrl = accountProviderUrl.ensureProtocol(), + val wellKnown = wellknownRetriever.getElementWellKnown( + baseUrl = accountProviderUrl.ensureProtocol(), ) if (wellKnown?.enforceElementPro == true) { throw AccountProviderAccessException.NeedElementProException( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/ElementWellknownRetriever.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/ElementWellknownRetriever.kt deleted file mode 100644 index e68809df07..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accesscontrol/ElementWellknownRetriever.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.accesscontrol - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.impl.resolver.network.ElementWellKnown -import io.element.android.features.login.impl.resolver.network.WellknownAPI -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.network.RetrofitFactory -import timber.log.Timber -import javax.inject.Inject - -interface ElementWellknownRetriever { - suspend fun retrieve(accountProviderUrl: String): ElementWellKnown? -} - -@ContributesBinding(AppScope::class) -class DefaultElementWellknownRetriever @Inject constructor( - private val retrofitFactory: RetrofitFactory, -) : ElementWellknownRetriever { - override suspend fun retrieve(accountProviderUrl: String): ElementWellKnown? { - val wellknownApi = try { - retrofitFactory.create(accountProviderUrl) - .create(WellknownAPI::class.java) - } catch (e: Exception) { - // If the base URL is not valid, we cannot retrieve the well-known data - Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl") - return null - } - return try { - wellknownApi.getElementWellKnown() - } catch (e: Exception) { - Timber.e(e, "Failed to retrieve Element well-known data for $accountProviderUrl") - null - } - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index 6190d6ff6b..56b7391102 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -7,13 +7,14 @@ package io.element.android.features.login.impl.resolver -import io.element.android.features.login.impl.resolver.network.WellknownRequest import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.libraries.wellknown.api.WellknownRetriever import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -27,7 +28,7 @@ import javax.inject.Inject */ class HomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, - private val wellknownRequest: WellknownRequest, + private val wellknownRetriever: WellknownRetriever, ) { fun resolve(userInput: String): Flow> = flow { val flowContext = currentCoroutineContext() @@ -41,7 +42,7 @@ class HomeserverResolver @Inject constructor( list.parallelMap { url -> val wellKnown = tryOrNull { withTimeout(5000) { - wellknownRequest.execute(url) + wellknownRetriever.getWellKnown(url) } } val isValid = wellKnown?.isValid().orFalse() @@ -86,3 +87,7 @@ class HomeserverResolver @Inject constructor( } } } + +private fun WellKnown.isValid(): Boolean { + return homeServer?.baseURL?.isNotBlank().orFalse() +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt deleted file mode 100644 index d217b4ca9e..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ -package io.element.android.features.login.impl.resolver.network - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.network.RetrofitFactory -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultWellknownRequest @Inject constructor( - private val retrofitFactory: RetrofitFactory, -) : WellknownRequest { - /** - * Return the WellKnown data, if found. - * @param baseUrl for instance https://matrix.org - */ - override suspend fun execute(baseUrl: String): WellKnown { - val wellknownApi = retrofitFactory.create(baseUrl) - .create(WellknownAPI::class.java) - return wellknownApi.getWellKnown() - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt deleted file mode 100644 index b734850989..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ -package io.element.android.features.login.impl.resolver.network - -interface WellknownRequest { - /** - * Return the WellKnown data, or throw an error if not found. - * @param baseUrl for instance https://matrix.org - */ - suspend fun execute(baseUrl: String): WellKnown -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt index 3793f3a53b..8046576347 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/web/WebClientUrlForAuthenticationRetriever.kt @@ -10,12 +10,10 @@ package io.element.android.features.login.impl.web import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.AuthenticationConfig -import io.element.android.features.login.impl.resolver.network.WellknownAPI import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.di.AppScope -import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.wellknown.api.WellknownRetriever import timber.log.Timber -import java.net.HttpURLConnection import javax.inject.Inject interface WebClientUrlForAuthenticationRetriever { @@ -24,24 +22,16 @@ interface WebClientUrlForAuthenticationRetriever { @ContributesBinding(AppScope::class) class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor( - private val retrofitFactory: RetrofitFactory, + private val wellknownRetriever: WellknownRetriever, ) : WebClientUrlForAuthenticationRetriever { override suspend fun retrieve(homeServerUrl: String): String { if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) { Timber.w("Temporary account creation flow is only supported on matrix.org") throw AccountCreationNotSupported() } - val wellknownApi = retrofitFactory.create(homeServerUrl) - .create(WellknownAPI::class.java) - val result = try { - wellknownApi.getElementWellKnown() - } catch (e: retrofit2.HttpException) { - throw when { - e.code() == HttpURLConnection.HTTP_NOT_FOUND -> AccountCreationNotSupported() - else -> e - } - } - val registrationHelperUrl = result.registrationHelperUrl + val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl) + ?: throw AccountCreationNotSupported() + val registrationHelperUrl = wellknown.registrationHelperUrl return if (registrationHelperUrl != null) { registrationHelperUrl.toUri() .buildUpon() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt index 87ec750b36..b0ee59cea8 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt @@ -10,10 +10,11 @@ package io.element.android.features.login.impl.accesscontrol import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.changeserver.AccountProviderAccessException -import io.element.android.features.login.impl.resolver.network.ElementWellKnown +import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL +import io.element.android.libraries.wellknown.api.ElementWellKnown import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Test @@ -152,8 +153,8 @@ class DefaultAccountProviderAccessControlTest { isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver }, defaultHomeserverListResult = { allowedAccountProviders }, ), - elementWellknownRetriever = FakeElementWellknownRetriever( - retrieveResult = { elementWellKnown } + wellknownRetriever = FakeWellknownRetriever( + getElementWellKnownResult = { elementWellKnown }, ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/FakeElementWellknownRetriever.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/FakeElementWellknownRetriever.kt deleted file mode 100644 index 70854302c8..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/FakeElementWellknownRetriever.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.accesscontrol - -import io.element.android.features.login.impl.resolver.network.ElementWellKnown -import io.element.android.tests.testutils.simulateLongTask - -class FakeElementWellknownRetriever( - private val retrieveResult: (String) -> ElementWellKnown? = { null }, -) : ElementWellknownRetriever { - override suspend fun retrieve(accountProviderUrl: String): ElementWellKnown? = simulateLongTask { - retrieveResult(accountProviderUrl) - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index a096024141..dc2b552ff5 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -11,17 +11,17 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl -import io.element.android.features.login.impl.accesscontrol.ElementWellknownRetriever -import io.element.android.features.login.impl.accesscontrol.FakeElementWellknownRetriever import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.resolver.network.ElementWellKnown +import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -113,14 +113,14 @@ class ChangeServerPresenterTest { @Test fun `present - change server element pro required error`() = runTest { - val retrieveResult = lambdaRecorder { + val getElementWellKnownResult = lambdaRecorder { ElementWellKnown( enforceElementPro = true, ) } createPresenter( - elementWellknownRetriever = FakeElementWellknownRetriever( - retrieveResult = retrieveResult, + wellknownRetriever = FakeWellknownRetriever( + getElementWellKnownResult = getElementWellKnownResult, ), ).test { val initialState = awaitItem() @@ -136,7 +136,7 @@ class ChangeServerPresenterTest { assertThat( (failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId ).isEqualTo("io.element.enterprise") - retrieveResult.assertions() + getElementWellKnownResult.assertions() .isCalledOnce() .with(value(A_HOMESERVER_URL.ensureProtocol())) } @@ -146,13 +146,13 @@ class ChangeServerPresenterTest { authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), enterpriseService: EnterpriseService = FakeEnterpriseService(), - elementWellknownRetriever: ElementWellknownRetriever = FakeElementWellknownRetriever(), + wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), ) = ChangeServerPresenter( authenticationService = authenticationService, accountProviderDataSource = accountProviderDataSource, defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl( enterpriseService = enterpriseService, - elementWellknownRetriever = elementWellknownRetriever, + wellknownRetriever = wellknownRetriever, ), ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt deleted file mode 100644 index e629ebe3a5..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.resolver.network - -class FakeWellknownRequest : WellknownRequest { - private var resultMap: Map = emptyMap() - fun givenResultMap(map: Map) { - resultMap = map - } - - override suspend fun execute(baseUrl: String): WellKnown { - return resultMap[baseUrl] ?: error("No result provided for $baseUrl") - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index ae00099687..035be8f31b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -13,11 +13,10 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl -import io.element.android.features.login.impl.accesscontrol.ElementWellknownRetriever -import io.element.android.features.login.impl.accesscontrol.FakeElementWellknownRetriever import io.element.android.features.login.impl.login.LoginHelper import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever +import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -34,6 +33,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow +import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest @@ -238,7 +238,7 @@ private fun createPresenter( buildMeta: BuildMeta = aBuildMeta(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), enterpriseService: EnterpriseService = FakeEnterpriseService(), - elementWellknownRetriever: ElementWellknownRetriever = FakeElementWellknownRetriever(), + wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), rageshakeFeatureAvailability: () -> Boolean = { true }, loginHelper: LoginHelper = createLoginHelper(), ) = OnBoardingPresenter( @@ -248,7 +248,7 @@ private fun createPresenter( enterpriseService = enterpriseService, defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl( enterpriseService = enterpriseService, - elementWellknownRetriever = elementWellknownRetriever, + wellknownRetriever = wellknownRetriever, ), rageshakeFeatureAvailability = rageshakeFeatureAvailability, loginHelper = loginHelper, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt index 2d6cdf71dc..a4d594399f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt @@ -14,15 +14,15 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl -import io.element.android.features.login.impl.accesscontrol.ElementWellknownRetriever -import io.element.android.features.login.impl.accesscontrol.FakeElementWellknownRetriever import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager +import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory +import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.test import io.element.android.tests.testutils.testCoroutineDispatchers @@ -162,14 +162,14 @@ class QrCodeScanPresenterTest { coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(), enterpriseService: EnterpriseService = FakeEnterpriseService(), - elementWellknownRetriever: ElementWellknownRetriever = FakeElementWellknownRetriever(), + wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), ) = QrCodeScanPresenter( qrCodeLoginDataFactory = qrCodeLoginDataFactory, qrCodeLoginManager = qrCodeLoginManager, coroutineDispatchers = coroutineDispatchers, defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl( enterpriseService = enterpriseService, - elementWellknownRetriever = elementWellknownRetriever, + wellknownRetriever = wellknownRetriever, ), ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index 96e539a2d8..b679c92ee1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -13,12 +13,14 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverResolver -import io.element.android.features.login.impl.resolver.network.FakeWellknownRequest -import io.element.android.features.login.impl.resolver.network.WellKnown -import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig +import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.libraries.wellknown.api.WellKnownBaseConfig import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -30,9 +32,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - initial state`() = runTest { - val fakeWellknownRequest = FakeWellknownRequest() + val fakeWellknownRetriever = FakeWellknownRetriever() val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -46,9 +48,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - enter text no result`() = runTest { - val fakeWellknownRequest = FakeWellknownRequest() + val fakeWellknownRetriever = FakeWellknownRetriever() val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -66,9 +68,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - enter valid url no wellknown`() = runTest { - val fakeWellknownRequest = FakeWellknownRequest() + val fakeWellknownRetriever = FakeWellknownRetriever() val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -92,14 +94,20 @@ class SearchAccountProviderPresenterTest { @Test fun `present - enter text one result with wellknown`() = runTest { - val fakeWellknownRequest = FakeWellknownRequest() - fakeWellknownRequest.givenResultMap( - mapOf( - "https://test.io" to aWellKnown(), - ) + val getWellKnownResult = lambdaRecorder { + when (it) { + "https://test.org" -> error("not found") + "https://test.com" -> error("not found") + "https://test.io" -> aWellKnown() + "https://test" -> error("not found") + else -> error("should not happen") + } + } + val fakeWellknownRetriever = FakeWellknownRetriever( + getWellKnownResult = getWellKnownResult, ) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -118,6 +126,65 @@ class SearchAccountProviderPresenterTest { ) ) ) + getWellKnownResult.assertions().isCalledExactly(4) + .withSequence( + listOf(value("https://test.org")), + listOf(value("https://test.com")), + listOf(value("https://test.io")), + listOf(value("https://test")), + ) + } + } + + @Test + fun `present - enter text two results with wellknown`() = runTest { + val getWellKnownResult = lambdaRecorder { + when (it) { + "https://test.org" -> aWellKnown() + "https://test.com" -> error("not found") + "https://test.io" -> aWellKnown() + "https://test" -> error("not found") + else -> error("should not happen") + } + } + val fakeWellknownRetriever = FakeWellknownRetriever( + getWellKnownResult = getWellKnownResult, + ) + val presenter = SearchAccountProviderPresenter( + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + changeServerPresenter = { aChangeServerState() } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("test") + assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + AsyncData.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org"), + ) + ) + ) + assertThat(awaitItem().userInputResult).isEqualTo( + AsyncData.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org"), + aHomeserverData(homeserverUrl = "https://test.io"), + ) + ) + ) + getWellKnownResult.assertions().isCalledExactly(4) + .withSequence( + listOf(value("https://test.org")), + listOf(value("https://test.com")), + listOf(value("https://test.io")), + listOf(value("https://test")), + ) } } diff --git a/libraries/wellknown/api/build.gradle.kts b/libraries/wellknown/api/build.gradle.kts new file mode 100644 index 0000000000..3a805e38c8 --- /dev/null +++ b/libraries/wellknown/api/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.wellknown.api" +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt new file mode 100644 index 0000000000..5d6563e3c3 --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.api + +data class ElementWellKnown( + val registrationHelperUrl: String? = null, + val enforceElementPro: Boolean? = null, +) diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt new file mode 100644 index 0000000000..9b41b52e2f --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.api + +data class WellKnown( + val homeServer: WellKnownBaseConfig? = null, + val identityServer: WellKnownBaseConfig? = null, +) + +data class WellKnownBaseConfig( + val baseURL: String? = null +) diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt new file mode 100644 index 0000000000..e617bc8e13 --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.api + +interface WellknownRetriever { + suspend fun getWellKnown(baseUrl: String): WellKnown? + suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? +} diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts new file mode 100644 index 0000000000..a9c1a1e9bf --- /dev/null +++ b/libraries/wellknown/impl/build.gradle.kts @@ -0,0 +1,32 @@ +import extension.setupAnvil + +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-library") + id("kotlin-parcelize") + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "io.element.android.libraries.wellknown.impl" +} + +setupAnvil() + +dependencies { + implementation(libs.androidx.annotationjvm) + implementation(libs.coroutines.core) + implementation(platform(libs.network.retrofit.bom)) + implementation(libs.network.retrofit) + implementation(libs.serialization.json) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.network) + implementation(projects.libraries.wellknown.api) +} diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt new file mode 100644 index 0000000000..4149d24df6 --- /dev/null +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.libraries.wellknown.api.WellKnownBaseConfig +import io.element.android.libraries.wellknown.api.WellknownRetriever +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultWellknownRetriever @Inject constructor( + private val retrofitFactory: RetrofitFactory, +) : WellknownRetriever { + override suspend fun getWellKnown(baseUrl: String): WellKnown? { + val wellknownApi = buildWellknownApi(baseUrl) ?: return null + return try { + wellknownApi.getWellKnown().map() + } catch (e: Exception) { + Timber.e(e, "Failed to retrieve well-known data for $baseUrl") + null + } + } + + override suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? { + val wellknownApi = buildWellknownApi(baseUrl) ?: return null + return try { + wellknownApi.getElementWellKnown().map() + } catch (e: Exception) { + Timber.e(e, "Failed to retrieve Element well-known data for $baseUrl") + null + } + } + + private fun buildWellknownApi(accountProviderUrl: String): WellknownAPI? { + return try { + retrofitFactory.create(accountProviderUrl.ensureProtocol()) + .create(WellknownAPI::class.java) + } catch (e: Exception) { + // If the base URL is not valid, we cannot retrieve the well-known data + Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl") + null + } + } +} + +private fun InternalElementWellKnown.map() = ElementWellKnown( + registrationHelperUrl = registrationHelperUrl, + enforceElementPro = enforceElementPro, +) + +private fun InternalWellKnown.map() = WellKnown( + homeServer = homeServer?.map(), + identityServer = identityServer?.map(), +) + +private fun InternalWellKnownBaseConfig.map() = WellKnownBaseConfig( + baseURL = baseURL, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt similarity index 80% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt rename to libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index 4f0073455a..9e902d9b92 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/ElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -1,11 +1,11 @@ /* - * Copyright 2023, 2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.resolver.network +package io.element.android.libraries.wellknown.impl import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,7 +20,7 @@ import kotlinx.serialization.Serializable * . */ @Serializable -data class ElementWellKnown( +data class InternalElementWellKnown( @SerialName("registration_helper_url") val registrationHelperUrl: String? = null, @SerialName("enforce_element_pro") diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt similarity index 65% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt rename to libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt index 5c2843bb3d..8915359771 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.resolver.network +package io.element.android.libraries.wellknown.impl -import io.element.android.libraries.core.bool.orFalse import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -26,13 +25,9 @@ import kotlinx.serialization.Serializable * . */ @Serializable -data class WellKnown( +data class InternalWellKnown( @SerialName("m.homeserver") - val homeServer: WellKnownBaseConfig? = null, + val homeServer: InternalWellKnownBaseConfig? = null, @SerialName("m.identity_server") - val identityServer: WellKnownBaseConfig? = null, -) { - fun isValid(): Boolean { - return homeServer?.baseURL?.isNotBlank().orFalse() - } -} + val identityServer: InternalWellKnownBaseConfig? = null, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt similarity index 84% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt rename to libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt index b67ec9cb95..76f1a2af4e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.resolver.network +package io.element.android.libraries.wellknown.impl import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,7 +20,7 @@ import kotlinx.serialization.Serializable * . */ @Serializable -data class WellKnownBaseConfig( +data class InternalWellKnownBaseConfig( @SerialName("base_url") val baseURL: String? = null ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/WellknownAPI.kt similarity index 67% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt rename to libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/WellknownAPI.kt index 29d0b83beb..d109aa7a43 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/WellknownAPI.kt @@ -5,14 +5,14 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.login.impl.resolver.network +package io.element.android.libraries.wellknown.impl import retrofit2.http.GET internal interface WellknownAPI { @GET(".well-known/matrix/client") - suspend fun getWellKnown(): WellKnown + suspend fun getWellKnown(): InternalWellKnown @GET(".well-known/element/element.json") - suspend fun getElementWellKnown(): ElementWellKnown + suspend fun getElementWellKnown(): InternalElementWellKnown } diff --git a/libraries/wellknown/test/build.gradle.kts b/libraries/wellknown/test/build.gradle.kts new file mode 100644 index 0000000000..7c7f4a368b --- /dev/null +++ b/libraries/wellknown/test/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.wellknown.test" +} + +dependencies { + implementation(projects.libraries.wellknown.api) + implementation(projects.tests.testutils) +} diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt new file mode 100644 index 0000000000..c8bbbde26d --- /dev/null +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.wellknown.test + +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.libraries.wellknown.api.WellknownRetriever +import io.element.android.tests.testutils.simulateLongTask + +class FakeWellknownRetriever( + private val getWellKnownResult: (String) -> WellKnown? = { null }, + private val getElementWellKnownResult: (String) -> ElementWellKnown? = { null }, +) : WellknownRetriever { + override suspend fun getWellKnown(baseUrl: String): WellKnown? = simulateLongTask { + getWellKnownResult(baseUrl) + } + + override suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? = simulateLongTask { + getElementWellKnownResult(baseUrl) + } +} From 76849c4374ce464e09d296312f69c7e4ac5a4bc7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 8 Aug 2025 13:35:44 +0200 Subject: [PATCH 02/14] Introduce SessionWellknownRetriever and implementation that uses a MatrixClient. --- .../api/SessionWellknownRetriever.kt | 13 +++++ libraries/wellknown/impl/build.gradle.kts | 1 + .../impl/DefaultSessionWellknownRetriever.kt | 49 +++++++++++++++++++ .../impl/DefaultWellknownRetriever.kt | 14 ------ .../libraries/wellknown/impl/Mapper.kt | 26 ++++++++++ 5 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt create mode 100644 libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt create mode 100644 libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt new file mode 100644 index 0000000000..7f7b9b983f --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.api + +interface SessionWellknownRetriever { + suspend fun getWellKnown(): WellKnown? + suspend fun getElementWellKnown(): ElementWellKnown? +} diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index a9c1a1e9bf..0754ce6184 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(libs.serialization.json) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) implementation(projects.libraries.wellknown.api) } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt new file mode 100644 index 0000000000..259f7848b3 --- /dev/null +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.mapCatchingExceptions +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.SessionWellknownRetriever +import io.element.android.libraries.wellknown.api.WellKnown +import kotlinx.serialization.json.Json +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultSessionWellknownRetriever @Inject constructor( + private val matrixClient: MatrixClient, +) : SessionWellknownRetriever { + private val parser by lazy { Json { ignoreUnknownKeys = true } } + private val domain by lazy { matrixClient.userIdServerName() } + + override suspend fun getWellKnown(): WellKnown? { + val url = "https://$domain/.well-known/matrix/client" + return matrixClient + .getUrl(url) + .mapCatchingExceptions { String(it) } + .mapCatchingExceptions { parser.decodeFromString(InternalWellKnown.serializer(), it) } + .onFailure { Timber.e(it, "Failed to retrieve .well-known from $domain") } + .map { it.map() } + .getOrNull() + } + + override suspend fun getElementWellKnown(): ElementWellKnown? { + val url = "https://$domain/.well-known/element/element.json" + return matrixClient + .getUrl(url) + .mapCatchingExceptions { String(it) } + .mapCatchingExceptions { parser.decodeFromString(InternalElementWellKnown.serializer(), it) } + .onFailure { Timber.e(it, "Failed to retrieve Element .well-known from $domain") } + .map { it.map() } + .getOrNull() + } +} diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt index 4149d24df6..0d45f60be5 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -53,17 +53,3 @@ class DefaultWellknownRetriever @Inject constructor( } } } - -private fun InternalElementWellKnown.map() = ElementWellKnown( - registrationHelperUrl = registrationHelperUrl, - enforceElementPro = enforceElementPro, -) - -private fun InternalWellKnown.map() = WellKnown( - homeServer = homeServer?.map(), - identityServer = identityServer?.map(), -) - -private fun InternalWellKnownBaseConfig.map() = WellKnownBaseConfig( - baseURL = baseURL, -) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt new file mode 100644 index 0000000000..8625571c62 --- /dev/null +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.impl + +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.libraries.wellknown.api.WellKnownBaseConfig + +internal fun InternalElementWellKnown.map() = ElementWellKnown( + registrationHelperUrl = registrationHelperUrl, + enforceElementPro = enforceElementPro, +) + +internal fun InternalWellKnown.map() = WellKnown( + homeServer = homeServer?.map(), + identityServer = identityServer?.map(), +) + +internal fun InternalWellKnownBaseConfig.map() = WellKnownBaseConfig( + baseURL = baseURL, +) From d7e4e00b5d62e528cd2fe2bd95deb89e271dbe72 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 8 Aug 2025 15:17:00 +0200 Subject: [PATCH 03/14] Let enterprise build be able to override (or disable) the bug report URL. --- .../element/android/x/ElementXApplication.kt | 6 +--- .../android/appconfig/RageshakeConfig.kt | 6 ---- .../android/appnav/LoggedInFlowNode.kt | 6 +++- enterprise | 2 +- .../features/enterprise/api/BugReportUrl.kt | 16 +++++++++ .../enterprise/api/EnterpriseService.kt | 3 ++ .../api/SessionEnterpriseService.kt | 2 ++ .../impl/DefaultEnterpriseService.kt | 4 +++ .../impl/DefaultSessionEnterpriseService.kt | 1 + .../enterprise/test/FakeEnterpriseService.kt | 9 +++-- .../test/FakeSessionEnterpriseService.kt | 3 ++ .../features/home/impl/HomePresenter.kt | 2 +- .../features/home/impl/HomePresenterTest.kt | 20 +++++++++-- .../screens/onboarding/OnBoardingPresenter.kt | 3 +- ...DefaultAccountProviderAccessControlTest.kt | 19 +++++----- .../changeserver/ChangeServerPresenterTest.kt | 3 +- .../onboarding/OnBoardingPresenterTest.kt | 7 ++-- .../impl/root/PreferencesRootPresenter.kt | 2 +- .../impl/root/PreferencesRootPresenterTest.kt | 7 ++-- .../api/RageshakeFeatureAvailability.kt | 4 ++- features/rageshake/impl/build.gradle.kts | 2 ++ .../DefaultRageshakeFeatureAvailability.kt | 14 +++++--- .../crash/DefaultCrashDetectionPresenter.kt | 22 ++++++++---- .../DefaultRageshakePreferencesPresenter.kt | 2 +- .../impl/reporter/BugReportAppNameProvider.kt | 22 ++++++++++++ .../impl/reporter/BugReporterUrlProvider.kt | 3 +- .../impl/reporter/DefaultBugReporter.kt | 8 ++++- .../reporter/DefaultBugReporterUrlProvider.kt | 23 ++++++++++-- .../crash/ui/CrashDetectionPresenterTest.kt | 3 +- .../RageshakeDetectionPresenterTest.kt | 11 +++--- .../RageshakePreferencesPresenterTest.kt | 9 ++--- .../impl/reporter/DefaultBugReporterTest.kt | 3 +- .../DefaultBugReporterUrlProviderTest.kt | 36 ++++++++++++++++--- .../wellknown/api/ElementWellKnown.kt | 5 +-- .../libraries/wellknown/api/WellKnown.kt | 6 ++-- .../impl/DefaultWellknownRetriever.kt | 1 - .../impl/InternalElementWellKnown.kt | 2 ++ .../libraries/wellknown/impl/Mapper.kt | 1 + .../test/FakeSessionWellknownRetriever.kt | 26 ++++++++++++++ .../features/wellknown/test/Fixtures.kt | 20 +++++++++++ 40 files changed, 266 insertions(+), 78 deletions(-) create mode 100644 features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt create mode 100644 features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt create mode 100644 libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt create mode 100644 libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index 30bef76339..a4bfe0c60d 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -9,8 +9,6 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer -import io.element.android.appconfig.RageshakeConfig -import io.element.android.appconfig.isEnabled import io.element.android.features.cachecleaner.api.CacheCleanerInitializer import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppComponent @@ -25,9 +23,7 @@ class ElementXApplication : Application(), DaggerComponentOwner { override fun onCreate() { super.onCreate() AppInitializer.getInstance(this).apply { - if (RageshakeConfig.isEnabled) { - initializeComponent(CrashInitializer::class.java) - } + initializeComponent(CrashInitializer::class.java) initializeComponent(PlatformInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java) } diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt index 8c836bc8a2..1dd482cc40 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt @@ -25,9 +25,3 @@ object RageshakeConfig { */ const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L } - -/** - * Whether the rageshake feature is enabled. - */ -val RageshakeConfig.isEnabled: Boolean - get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 043dc75a92..25c89abce9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -47,6 +47,7 @@ import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.createroom.api.CreateRoomEntryPoint +import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState @@ -125,6 +126,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val logoutEntryPoint: LogoutEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, + private val sessionEnterpriseService: SessionEnterpriseService, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( backstack = BackStack( @@ -182,7 +184,9 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onBuilt() { super.onBuilt() - + lifecycleScope.launch { + sessionEnterpriseService.init() + } lifecycle.subscribe( onCreate = { appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) diff --git a/enterprise b/enterprise index b7ababb953..f3546ac27a 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit b7ababb9537da8bec254b8ed00b5a4122e9f3e3b +Subproject commit f3546ac27aff0b9b0875bbf7fcc496a16ac22595 diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt new file mode 100644 index 0000000000..8c1b659ab6 --- /dev/null +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/BugReportUrl.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.enterprise.api + +sealed interface BugReportUrl { + data object UseDefault : BugReportUrl + data object Disabled : BugReportUrl + data class Custom( + val url: String, + ) : BugReportUrl +} diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 4e2a3cc1f2..03ecda80c0 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -9,6 +9,7 @@ package io.element.android.features.enterprise.api import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.flow.Flow interface EnterpriseService { val isEnterpriseBuild: Boolean @@ -22,6 +23,8 @@ interface EnterpriseService { fun firebasePushGateway(): String? fun unifiedPushDefaultPushGateway(): String? + val bugReportUrlFlow: Flow + companion object { const val ANY_ACCOUNT_PROVIDER = "*" } diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt index c67b94476b..aff3d93e8e 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt @@ -9,4 +9,6 @@ package io.element.android.features.enterprise.api interface SessionEnterpriseService { suspend fun isElementCallAvailable(): Boolean + + suspend fun init() } diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 7f4f4b387b..d5c23788d6 100644 --- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -11,9 +11,11 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.compound.tokens.generated.compoundColorsDark import io.element.android.compound.tokens.generated.compoundColorsLight +import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -31,4 +33,6 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService { override fun firebasePushGateway(): String? = null override fun unifiedPushDefaultPushGateway(): String? = null + + override val bugReportUrlFlow = flowOf(BugReportUrl.UseDefault) } diff --git a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt index 2728bdad6a..47f52f6ac1 100644 --- a/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt +++ b/features/enterprise/impl/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultSessionEnterpriseService.kt @@ -14,5 +14,6 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService { + override suspend fun init() = Unit override suspend fun isElementCallAvailable(): Boolean = true } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 14195e1243..30ccc4c48d 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -8,10 +8,14 @@ package io.element.android.features.enterprise.test import io.element.android.compound.tokens.generated.SemanticColors +import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow class FakeEnterpriseService( override val isEnterpriseBuild: Boolean = false, @@ -51,7 +55,6 @@ class FakeEnterpriseService( return unifiedPushDefaultPushGatewayResult() } - companion object { - const val A_FAKE_HOMESERVER = "a_fake_homeserver" - } + val bugReportUrlMutableFlow = MutableStateFlow(BugReportUrl.UseDefault) + override val bugReportUrlFlow: Flow = bugReportUrlMutableFlow.asStateFlow() } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt index a1e811ed10..e3125a19e6 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeSessionEnterpriseService.kt @@ -14,6 +14,9 @@ import io.element.android.tests.testutils.simulateLongTask class FakeSessionEnterpriseService( private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, ) : SessionEnterpriseService { + override suspend fun init() { + } + override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { isElementCallAvailableResult() } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index 894ed29e8d..c5eea40796 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -43,7 +43,7 @@ class HomePresenter @Inject constructor( override fun present(): HomeState { val matrixUser = client.userProfile.collectAsState() val isOnline by syncService.isOnline.collectAsState() - val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } + val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) val roomListState = roomListPresenter.present() val isSpaceFeatureEnabled by remember { featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt index 6913ea19bf..a7917af658 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/HomePresenterTest.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -49,7 +50,7 @@ class HomePresenterTest { matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) val presenter = createHomePresenter( client = matrixClient, - rageshakeFeatureAvailability = { false }, + rageshakeFeatureAvailability = { flowOf(false) }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -66,6 +67,21 @@ class HomePresenterTest { } } + @Test + fun `present - can report bug`() = runTest { + val presenter = createHomePresenter( + rageshakeFeatureAvailability = { flowOf(true) }, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.canReportBug).isFalse() + val finalState = awaitItem() + assertThat(finalState.canReportBug).isTrue() + } + } + @Test fun `present - space feature enabled`() = runTest { val presenter = createHomePresenter( @@ -132,7 +148,7 @@ class HomePresenterTest { client: MatrixClient = FakeMatrixClient(), syncService: SyncService = FakeSyncService(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), - rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) }, indicatorService: IndicatorService = FakeIndicatorService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService() ) = HomePresenter( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 90e4e99c37..ba60cfd222 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -82,7 +83,7 @@ class OnBoardingPresenter @AssistedInject constructor( value = linkAccountProvider == null && featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) } - val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } + val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) var showReportBug by rememberSaveable { mutableStateOf(false) } val loginMode by loginHelper.collectLoginMode() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt index b0ee59cea8..f559c57bf0 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accesscontrol/DefaultAccountProviderAccessControlTest.kt @@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.changeserver.AccountProviderAccessException import io.element.android.features.wellknown.test.FakeWellknownRetriever +import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL @@ -25,7 +26,7 @@ class DefaultAccountProviderAccessControlTest { val accessControl = createDefaultAccountProviderAccessControl( isEnterpriseBuild = false, isAllowedToConnectToHomeserver = true, - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = true, ), ) @@ -38,7 +39,7 @@ class DefaultAccountProviderAccessControlTest { isEnterpriseBuild = false, // false here. isAllowedToConnectToHomeserver = false, - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = true, ), ) @@ -50,7 +51,7 @@ class DefaultAccountProviderAccessControlTest { val accessControl = createDefaultAccountProviderAccessControl( isEnterpriseBuild = false, isAllowedToConnectToHomeserver = true, - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = false, ), ) @@ -62,7 +63,7 @@ class DefaultAccountProviderAccessControlTest { val accessControl = createDefaultAccountProviderAccessControl( isEnterpriseBuild = false, isAllowedToConnectToHomeserver = true, - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = null, ), ) @@ -85,7 +86,7 @@ class DefaultAccountProviderAccessControlTest { isEnterpriseBuild = false, isAllowedToConnectToHomeserver = false, allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2), - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = false, ), ) @@ -97,7 +98,7 @@ class DefaultAccountProviderAccessControlTest { val accessControl = createDefaultAccountProviderAccessControl( isEnterpriseBuild = true, isAllowedToConnectToHomeserver = true, - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = true, ), ) @@ -109,7 +110,7 @@ class DefaultAccountProviderAccessControlTest { val accessControl = createDefaultAccountProviderAccessControl( isEnterpriseBuild = true, isAllowedToConnectToHomeserver = true, - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = false, ), ) @@ -122,7 +123,7 @@ class DefaultAccountProviderAccessControlTest { isEnterpriseBuild = true, isAllowedToConnectToHomeserver = false, allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2), - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = true, ), ) @@ -135,7 +136,7 @@ class DefaultAccountProviderAccessControlTest { isEnterpriseBuild = true, isAllowedToConnectToHomeserver = false, allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2), - elementWellKnown = ElementWellKnown( + elementWellKnown = anElementWellKnown( enforceElementPro = false, ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index dc2b552ff5..620df9bba4 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -15,6 +15,7 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.wellknown.test.FakeWellknownRetriever +import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.matrix.test.A_HOMESERVER @@ -114,7 +115,7 @@ class ChangeServerPresenterTest { @Test fun `present - change server element pro required error`() = runTest { val getElementWellKnownResult = lambdaRecorder { - ElementWellKnown( + anElementWellKnown( enforceElementPro = true, ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 035be8f31b..c559db9056 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -78,7 +80,6 @@ class OnBoardingPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) }, ), - rageshakeFeatureAvailability = { true }, ) presenter.test { val initialState = awaitItem() @@ -94,7 +95,7 @@ class OnBoardingPresenterTest { @Test fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest { val presenter = createPresenter( - rageshakeFeatureAvailability = { false }, + rageshakeFeatureAvailability = { flowOf(false) }, ) presenter.test { skipItems(1) @@ -239,7 +240,7 @@ private fun createPresenter( featureFlagService: FeatureFlagService = FakeFeatureFlagService(), enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), - rageshakeFeatureAvailability: () -> Boolean = { true }, + rageshakeFeatureAvailability: () -> Flow = { flowOf(true) }, loginHelper: LoginHelper = createLoginHelper(), ) = OnBoardingPresenter( params = params, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 6fd2d635e1..ee73b7f721 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -83,7 +83,7 @@ class PreferencesRootPresenter @Inject constructor( var canDeactivateAccount by remember { mutableStateOf(false) } - val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } + val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) LaunchedEffect(Unit) { canDeactivateAccount = matrixClient.canDeactivateAccount() } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 1f1b527a45..a65e61661e 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -32,6 +32,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -102,7 +103,7 @@ class PreferencesRootPresenterTest { ) createPresenter( matrixClient = matrixClient, - rageshakeFeatureAvailability = { false }, + rageshakeFeatureAvailability = { flowOf(false) }, ).test { val initialState = awaitItem() assertThat(initialState.canReportBug).isFalse() @@ -119,7 +120,7 @@ class PreferencesRootPresenterTest { val indicatorService = FakeIndicatorService() createPresenter( matrixClient = matrixClient, - rageshakeFeatureAvailability = { false }, + rageshakeFeatureAvailability = { flowOf(false) }, indicatorService = indicatorService, ).test { skipItems(1) @@ -185,7 +186,7 @@ class PreferencesRootPresenterTest { matrixClient: FakeMatrixClient = FakeMatrixClient(), sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), - rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) }, indicatorService: IndicatorService = FakeIndicatorService(), ) = PreferencesRootPresenter( matrixClient = matrixClient, diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt index 34e740d4ab..a22040355e 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt @@ -7,6 +7,8 @@ package io.element.android.features.rageshake.api +import kotlinx.coroutines.flow.Flow + fun interface RageshakeFeatureAvailability { - fun isAvailable(): Boolean + fun isAvailable(): Flow } diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts index 2deeff4955..addfe3d794 100644 --- a/features/rageshake/impl/build.gradle.kts +++ b/features/rageshake/impl/build.gradle.kts @@ -26,6 +26,7 @@ setupAnvil() dependencies { implementation(projects.appconfig) + implementation(projects.features.enterprise.api) implementation(projects.services.toolbox.api) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) @@ -50,6 +51,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.test.mockk) + testImplementation(projects.features.enterprise.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.libraries.sessionStorage.test) diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt index 5f7548e9ec..c9f78a23ce 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt @@ -8,15 +8,19 @@ package io.element.android.features.rageshake.impl import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.RageshakeConfig -import io.element.android.appconfig.isEnabled import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.features.rageshake.impl.reporter.BugReporterUrlProvider import io.element.android.libraries.di.AppScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability { - override fun isAvailable(): Boolean { - return RageshakeConfig.isEnabled +class DefaultRageshakeFeatureAvailability @Inject constructor( + private val bugReporterUrlProvider: BugReporterUrlProvider, +) : RageshakeFeatureAvailability { + override fun isAvailable(): Flow { + return bugReporterUrlProvider.provide() + .map { it != null } } } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt index fffb87722a..7136393238 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt @@ -5,10 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.rageshake.impl.crash import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import com.squareup.anvil.annotations.ContributesBinding @@ -19,6 +22,8 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,12 +37,15 @@ class DefaultCrashDetectionPresenter @Inject constructor( @Composable override fun present(): CrashDetectionState { val localCoroutineScope = rememberCoroutineScope() - val crashDetected = remember { - if (rageshakeFeatureAvailability.isAvailable()) { - crashDataStore.appHasCrashed() - } else { - flowOf(false) - } + val crashDetected by remember { + rageshakeFeatureAvailability.isAvailable() + .flatMapLatest { isAvailable -> + if (isAvailable) { + crashDataStore.appHasCrashed() + } else { + flowOf(false) + } + } }.collectAsState(false) fun handleEvents(event: CrashDetectionEvents) { @@ -49,7 +57,7 @@ class DefaultCrashDetectionPresenter @Inject constructor( return CrashDetectionState( appName = buildMeta.applicationName, - crashDetected = crashDetected.value, + crashDetected = crashDetected, eventSink = ::handleEvents ) } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt index b4f45a3bdc..6096388e21 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt @@ -39,7 +39,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor( val isSupported: MutableState = rememberSaveable { mutableStateOf(rageshake.isAvailable()) } - val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() } + val isFeatureAvailable by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false) val isEnabled by remember { rageshakeDataStore.isEnabled() }.collectAsState(initial = false) diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt new file mode 100644 index 0000000000..b63dfcf669 --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReportAppNameProvider.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.impl.reporter + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.RageshakeConfig +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +fun interface BugReportAppNameProvider { + fun provide(): String +} + +@ContributesBinding(AppScope::class) +class DefaultBugReportAppNameProvider @Inject constructor() : BugReportAppNameProvider { + override fun provide(): String = RageshakeConfig.BUG_REPORT_APP_NAME +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterUrlProvider.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterUrlProvider.kt index d516a9492a..e7e0e4e060 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterUrlProvider.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/BugReporterUrlProvider.kt @@ -7,8 +7,9 @@ package io.element.android.features.rageshake.impl.reporter +import kotlinx.coroutines.flow.Flow import okhttp3.HttpUrl fun interface BugReporterUrlProvider { - fun provide(): HttpUrl + fun provide(): Flow } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 028b671eb6..d56c75f464 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -114,6 +114,12 @@ class DefaultBugReporter @Inject constructor( canContact: Boolean, listener: BugReporterListener, ) { + val url = bugReporterUrlProvider.provide().first() + if (url == null) { + // It should not happen, but if the URL is null, we cannot proceed + Timber.e("## sendBugReport() : bug report URL is null") + error("Bug report URL is null, cannot send bug report") + } // enumerate files to delete val bugReportFiles: MutableList = ArrayList() var response: Response? = null @@ -243,7 +249,7 @@ class DefaultBugReporter @Inject constructor( } // build the request val request = Request.Builder() - .url(bugReporterUrlProvider.provide()) + .url(url) .post(requestBody) .build() var errorMessage: String? = null diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt index e29b85cf77..3ebe5c1c29 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProvider.kt @@ -9,14 +9,31 @@ package io.element.android.features.rageshake.impl.reporter import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.RageshakeConfig +import io.element.android.features.enterprise.api.BugReportUrl +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.di.AppScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultBugReporterUrlProvider @Inject constructor() : BugReporterUrlProvider { - override fun provide(): HttpUrl { - return RageshakeConfig.BUG_REPORT_URL.toHttpUrl() +class DefaultBugReporterUrlProvider @Inject constructor( + private val bugReportAppNameProvider: BugReportAppNameProvider, + private val enterpriseService: EnterpriseService, +) : BugReporterUrlProvider { + override fun provide(): Flow { + if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null) + return enterpriseService.bugReportUrlFlow + .map { bugReportUrl -> + when (bugReportUrl) { + is BugReportUrl.Custom -> bugReportUrl.url + BugReportUrl.Disabled -> null + BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() } + } + } + .map { it?.toHttpUrl() } } } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt index 92dfbb02a2..7c14177399 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt @@ -18,6 +18,7 @@ import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -104,6 +105,6 @@ class CrashDetectionPresenterTest { ) = DefaultCrashDetectionPresenter( buildMeta = buildMeta, crashDataStore = crashDataStore, - rageshakeFeatureAvailability = { isFeatureAvailable }, + rageshakeFeatureAvailability = { flowOf(isFeatureAvailable) }, ) } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt index 6f433a78d7..451bfd82ed 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.BeforeClass import org.junit.Rule @@ -52,7 +53,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -77,7 +78,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -103,7 +104,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -138,7 +139,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -173,7 +174,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) ) moleculeFlow(RecompositionMode.Immediate) { diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt index ba68345440..df34e1cf48 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt @@ -16,6 +16,7 @@ import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY import io.element.android.features.rageshake.impl.rageshake.FakeRageShake import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -29,7 +30,7 @@ class RageshakePreferencesPresenterTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), FakeRageshakeDataStore(isEnabled = true), - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -46,7 +47,7 @@ class RageshakePreferencesPresenterTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = false), FakeRageshakeDataStore(isEnabled = true), - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -63,7 +64,7 @@ class RageshakePreferencesPresenterTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), FakeRageshakeDataStore(isEnabled = true), - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -83,7 +84,7 @@ class RageshakePreferencesPresenterTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), FakeRageshakeDataStore(isEnabled = true), - rageshakeFeatureAvailability = { true }, + rageshakeFeatureAvailability = { flowOf(true) }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index c7c424bfae..1a9592739d 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionSt import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import okhttp3.MultipartReader @@ -464,7 +465,7 @@ class DefaultBugReporterTest { userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")), sessionStore = sessionStore, buildMeta = buildMeta, - bugReporterUrlProvider = { server.url("/") }, + bugReporterUrlProvider = { flowOf(server.url("/")) }, sdkMetadata = FakeSdkMetadata("123456789"), matrixClientProvider = matrixClientProvider, tracingService = tracingService, diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt index 71563892dc..fb7464adf7 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt @@ -7,18 +7,44 @@ package io.element.android.features.rageshake.impl.reporter +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.RageshakeConfig +import io.element.android.features.enterprise.api.BugReportUrl +import io.element.android.features.enterprise.test.FakeEnterpriseService +import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.Test class DefaultBugReporterUrlProviderTest { @Test - fun `test DefaultBugReporterUrlProvider`() { - val sut = DefaultBugReporterUrlProvider() - if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) { - val result = sut.provide() - assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl()) + fun `provide return values when there is an rageshake app name`() = runTest { + val enterpriseService = FakeEnterpriseService() + val sut = DefaultBugReporterUrlProvider( + bugReportAppNameProvider = { "rageshakeAppName" }, + enterpriseService = enterpriseService, + ) + sut.provide().test { + assertThat(awaitItem()).isEqualTo( + RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }?.toHttpUrl() + ) + enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Disabled) + assertThat(awaitItem()).isNull() + enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Custom("https://aURL.org")) + assertThat(awaitItem()).isEqualTo("https://aURL.org".toHttpUrl()) + } + } + + @Test + fun `provide return null when there is no rageshake app name`() = runTest { + val enterpriseService = FakeEnterpriseService() + val sut = DefaultBugReporterUrlProvider( + bugReportAppNameProvider = { "" }, + enterpriseService = enterpriseService, + ) + sut.provide().test { + assertThat(awaitItem()).isNull() + awaitComplete() } } } diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index 5d6563e3c3..064416eec1 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.wellknown.api data class ElementWellKnown( - val registrationHelperUrl: String? = null, - val enforceElementPro: Boolean? = null, + val registrationHelperUrl: String?, + val enforceElementPro: Boolean?, + val rageshakeUrl: String?, ) diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt index 9b41b52e2f..59f63d1655 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt @@ -8,10 +8,10 @@ package io.element.android.libraries.wellknown.api data class WellKnown( - val homeServer: WellKnownBaseConfig? = null, - val identityServer: WellKnownBaseConfig? = null, + val homeServer: WellKnownBaseConfig?, + val identityServer: WellKnownBaseConfig?, ) data class WellKnownBaseConfig( - val baseURL: String? = null + val baseURL: String? ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt index 0d45f60be5..5d67cd32f0 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -13,7 +13,6 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellKnown -import io.element.android.libraries.wellknown.api.WellKnownBaseConfig import io.element.android.libraries.wellknown.api.WellknownRetriever import timber.log.Timber import javax.inject.Inject diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index 9e902d9b92..e81d78d498 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -25,4 +25,6 @@ data class InternalElementWellKnown( val registrationHelperUrl: String? = null, @SerialName("enforce_element_pro") val enforceElementPro: Boolean? = null, + @SerialName("rageshake_url") + val rageshakeUrl: String? = null, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index 8625571c62..9c1618f699 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.wellknown.api.WellKnownBaseConfig internal fun InternalElementWellKnown.map() = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, + rageshakeUrl = rageshakeUrl, ) internal fun InternalWellKnown.map() = WellKnown( diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt new file mode 100644 index 0000000000..6c2c141622 --- /dev/null +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.wellknown.test + +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.SessionWellknownRetriever +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.tests.testutils.simulateLongTask + +class FakeSessionWellknownRetriever( + private val getWellKnownResult: () -> WellKnown? = { null }, + private val getElementWellKnownResult: () -> ElementWellKnown? = { null }, +) : SessionWellknownRetriever { + override suspend fun getWellKnown(): WellKnown? = simulateLongTask { + getWellKnownResult() + } + + override suspend fun getElementWellKnown(): ElementWellKnown? = simulateLongTask { + getElementWellKnownResult() + } +} diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt new file mode 100644 index 0000000000..686026b78d --- /dev/null +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.wellknown.test + +import io.element.android.libraries.wellknown.api.ElementWellKnown + +fun anElementWellKnown( + registrationHelperUrl: String? = null, + enforceElementPro: Boolean? = null, + rageshakeUrl: String? = null, +) = ElementWellKnown( + registrationHelperUrl = registrationHelperUrl, + enforceElementPro = enforceElementPro, + rageshakeUrl = rageshakeUrl, +) From 7810bf49fd6b260d12beaaa3cc7132479e2e9273 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 8 Aug 2025 18:08:30 +0200 Subject: [PATCH 04/14] Fix compilation issue --- libraries/wellknown/impl/build.gradle.kts | 2 +- plugins/src/main/kotlin/extension/DependencyHandleScope.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index 0754ce6184..3fe073ee0c 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -20,6 +20,7 @@ android { setupAnvil() dependencies { + api(projects.libraries.wellknown.api) implementation(libs.androidx.annotationjvm) implementation(libs.coroutines.core) implementation(platform(libs.network.retrofit.bom)) @@ -29,5 +30,4 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) - implementation(projects.libraries.wellknown.api) } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 7319c03111..5e4ea91618 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -89,6 +89,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:mediaviewer:impl")) implementation(project(":libraries:troubleshoot:impl")) implementation(project(":libraries:fullscreenintent:impl")) + implementation(project(":libraries:wellknown:impl")) implementation(project(":libraries:oidc:impl")) } From 1fe15e7b234eca4a9c6fc713d11363b71760c659 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 10:36:40 +0200 Subject: [PATCH 05/14] Add Konsist test to check that files do not have double license header. It seems that sometimes Android Studio is doing this mistake. --- .../tests/konsist/KonsistLicenseTest.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt index 9f20f0fcac..9c0355f01e 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt @@ -68,4 +68,30 @@ class KonsistLicenseTest { enterpriseLicense.containsMatchIn(it.text) } } + + @Test + fun `assert that files do not have double license header`() { + Konsist + .scopeFromProject() + .files + .filter { + it.nameWithExtension != "locales.kt" && + it.nameWithExtension != "KonsistLicenseTest.kt" && + it.name.startsWith("Template ").not() + } + .assertTrue { it -> + it.text.count("New Vector") == 1 + } + } +} + +private fun String.count(subString: String): Int { + var count = 0 + var index = 0 + while (true) { + index = indexOf(subString, index) + if (index == -1) return count + count++ + index += subString.length + } } From 6f1fc5500b5be7f9d787ab48a89cb486a00b708e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 10:37:33 +0200 Subject: [PATCH 06/14] Remove duplicated licence header --- enterprise | 2 +- .../io/element/android/libraries/core/coroutine/Flow.kt | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/enterprise b/enterprise index f3546ac27a..568a735dd1 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit f3546ac27aff0b9b0875bbf7fcc496a16ac22595 +Subproject commit 568a735dd13af8b92f62f30976d0944ac541671b diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt index 263cc91648..66fbb7f988 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/coroutine/Flow.kt @@ -10,13 +10,6 @@ package io.element.android.libraries.core.coroutine import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - /** * Returns the first element of the flow that is an instance of [T], waiting for it if necessary. */ From 25d54f42bb8150508237989d67fa5aac2a556857 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 10:47:52 +0200 Subject: [PATCH 07/14] Update submodule ref. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 568a735dd1..a1b0f1b14c 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 568a735dd13af8b92f62f30976d0944ac541671b +Subproject commit a1b0f1b14c27fe597c40ea5ff9657c771c623d9b From 0666d43e37b076208ec8cc60b7b8e1defd64d902 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 10:49:39 +0200 Subject: [PATCH 08/14] Inject the Json parser --- .../wellknown/impl/DefaultSessionWellknownRetriever.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index 259f7848b3..b19ab5cac3 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -21,8 +21,8 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultSessionWellknownRetriever @Inject constructor( private val matrixClient: MatrixClient, + private val parser: Json, ) : SessionWellknownRetriever { - private val parser by lazy { Json { ignoreUnknownKeys = true } } private val domain by lazy { matrixClient.userIdServerName() } override suspend fun getWellKnown(): WellKnown? { From 78bd174d104886687cd433950f458d6b6ab45762 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 10:55:42 +0200 Subject: [PATCH 09/14] Merge 2 mapCatchingExceptions into a single one. --- .../impl/DefaultSessionWellknownRetriever.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index b19ab5cac3..0c948af1d9 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -29,8 +29,10 @@ class DefaultSessionWellknownRetriever @Inject constructor( val url = "https://$domain/.well-known/matrix/client" return matrixClient .getUrl(url) - .mapCatchingExceptions { String(it) } - .mapCatchingExceptions { parser.decodeFromString(InternalWellKnown.serializer(), it) } + .mapCatchingExceptions { + val data = String(it) + parser.decodeFromString(InternalWellKnown.serializer(), data) + } .onFailure { Timber.e(it, "Failed to retrieve .well-known from $domain") } .map { it.map() } .getOrNull() @@ -40,8 +42,10 @@ class DefaultSessionWellknownRetriever @Inject constructor( val url = "https://$domain/.well-known/element/element.json" return matrixClient .getUrl(url) - .mapCatchingExceptions { String(it) } - .mapCatchingExceptions { parser.decodeFromString(InternalElementWellKnown.serializer(), it) } + .mapCatchingExceptions { + val data = String(it) + parser.decodeFromString(InternalElementWellKnown.serializer(), data) + } .onFailure { Timber.e(it, "Failed to retrieve Element .well-known from $domain") } .map { it.map() } .getOrNull() From 50809d2adbaeacc245db7a9c85cdbd14fe133423 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 11:10:32 +0200 Subject: [PATCH 10/14] Add missing test on CrashDetectionPresenter --- .../impl/crash/FakeCrashDataStore.kt | 1 + .../crash/ui/CrashDetectionPresenterTest.kt | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt index 1a89a52bf7..8ab5d33eb3 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt @@ -21,6 +21,7 @@ class FakeCrashDataStore( override fun setCrashData(crashData: String) { crashDataFlow.value = crashData + appHasCrashedFlow.value = true } override suspend fun resetAppHasCrashed() { diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt index 7c14177399..a0172e3d39 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt @@ -18,6 +18,8 @@ import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -56,7 +58,7 @@ class CrashDetectionPresenterTest { fun `present - initial state crash is ignored if the feature is not available`() = runTest { val presenter = createPresenter( FakeCrashDataStore(appHasCrashed = true), - isFeatureAvailable = false, + isFeatureAvailableFlow = flowOf(false), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -98,13 +100,42 @@ class CrashDetectionPresenterTest { } } + @Test + fun `present - crashDetected is false if the feature is not available`() = runTest { + val isFeatureAvailableFlow = MutableStateFlow(false) + val crashDataStore = FakeCrashDataStore(appHasCrashed = false) + val presenter = createPresenter( + crashDataStore = crashDataStore, + isFeatureAvailableFlow = isFeatureAvailableFlow, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.crashDetected).isFalse() + crashDataStore.setCrashData("Some crash data") + // No new state + crashDataStore.resetAppHasCrashed() + // No new state + isFeatureAvailableFlow.value = true + crashDataStore.setCrashData("Some crash data") + assertThat(awaitItem().crashDetected).isTrue() + crashDataStore.resetAppHasCrashed() + assertThat(awaitItem().crashDetected).isFalse() + crashDataStore.setCrashData("Some crash data") + assertThat(awaitItem().crashDetected).isTrue() + isFeatureAvailableFlow.value = false + assertThat(awaitItem().crashDetected).isFalse() + } + } + private fun createPresenter( crashDataStore: FakeCrashDataStore = FakeCrashDataStore(), buildMeta: BuildMeta = aBuildMeta(), - isFeatureAvailable: Boolean = true, + isFeatureAvailableFlow: Flow = flowOf(true), ) = DefaultCrashDetectionPresenter( buildMeta = buildMeta, crashDataStore = crashDataStore, - rageshakeFeatureAvailability = { flowOf(isFeatureAvailable) }, + rageshakeFeatureAvailability = { isFeatureAvailableFlow }, ) } From e566bb37ed5bbbe9f32305f740df6cff3ae6c5cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 11:18:29 +0200 Subject: [PATCH 11/14] Add unit test on DefaultRageshakeFeatureAvailability --- ...DefaultRageshakeFeatureAvailabilityTest.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailabilityTest.kt diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailabilityTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailabilityTest.kt new file mode 100644 index 0000000000..7f59245647 --- /dev/null +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailabilityTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.impl + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Test + +class DefaultRageshakeFeatureAvailabilityTest { + @Test + fun `test isAvailable returns true when bug reporter URL is provided`() = runTest { + val flow = MutableStateFlow(null) + val sut = DefaultRageshakeFeatureAvailability( + bugReporterUrlProvider = { flow }, + ) + sut.isAvailable().test { + assertThat(awaitItem()).isFalse() + flow.value = "https://example.com/bugreport".toHttpUrl() + assertThat(awaitItem()).isTrue() + flow.value = null + assertThat(awaitItem()).isFalse() + } + } +} From ad01327c39046896d58606c384d281f231cbb637 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 12:12:19 +0200 Subject: [PATCH 12/14] Add unit test on DefaultSessionWellknownRetriever --- libraries/wellknown/impl/build.gradle.kts | 9 + .../DefaultSessionWellknownRetrieverTest.kt | 249 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt diff --git a/libraries/wellknown/impl/build.gradle.kts b/libraries/wellknown/impl/build.gradle.kts index 3fe073ee0c..3bc23953a0 100644 --- a/libraries/wellknown/impl/build.gradle.kts +++ b/libraries/wellknown/impl/build.gradle.kts @@ -30,4 +30,13 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.network) + + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) + testImplementation(libs.coroutines.core) + testImplementation(libs.coroutines.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.toolbox.test) } diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt new file mode 100644 index 0000000000..73f614e91e --- /dev/null +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.wellknown.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.wellknown.api.ElementWellKnown +import io.element.android.libraries.wellknown.api.WellKnown +import io.element.android.libraries.wellknown.api.WellKnownBaseConfig +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Test + +class DefaultSessionWellknownRetrieverTest { + @Test + fun `get empty wellknown`() = runTest { + val getUrlLambda = lambdaRecorder>() { + Result.success("{}".toByteArray()) + } + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = getUrlLambda, + ) + assertThat(sut.getWellKnown()).isEqualTo( + WellKnown( + homeServer = null, + identityServer = null, + ) + ) + getUrlLambda.assertions().isCalledOnce() + .with(value("https://user.domain.org/.well-known/matrix/client")) + } + + @Test + fun `get wellknown with full content`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "m.homeserver": { + "base_url": "https://example.org" + }, + "m.identity_server": { + "base_url": "https://identity.example.org" + } + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getWellKnown()).isEqualTo( + WellKnown( + homeServer = WellKnownBaseConfig( + baseURL = "https://example.org", + ), + identityServer = WellKnownBaseConfig( + baseURL = "https://identity.example.org", + ), + ) + ) + } + + @Test + fun `get wellknown with full content empty base_url`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "m.homeserver": { + "base_url": "https://example.org" + }, + "m.identity_server": {} + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getWellKnown()).isEqualTo( + WellKnown( + homeServer = WellKnownBaseConfig( + baseURL = "https://example.org", + ), + identityServer = WellKnownBaseConfig( + baseURL = null, + ), + ) + ) + } + + @Test + fun `get wellknown with unknown key`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "m.homeserver": { + "base_url": "https://example.org" + }, + "m.identity_server": { + "base_url": "https://identity.example.org" + }, + "other": true + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getWellKnown()).isEqualTo( + WellKnown( + homeServer = WellKnownBaseConfig( + baseURL = "https://example.org", + ), + identityServer = WellKnownBaseConfig( + baseURL = "https://identity.example.org", + ), + ) + ) + } + + @Test + fun `get wellknown json error`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "m.homeserver": { + "base_url": "https://example.org" + }, + error + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getWellKnown()).isNull() + } + + @Test + fun `get wellknown network error`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.failure(AN_EXCEPTION) + } + ) + assertThat(sut.getWellKnown()).isNull() + } + + @Test + fun `get empty element wellknown`() = runTest { + val getUrlLambda = lambdaRecorder>() { + Result.success("{}".toByteArray()) + } + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = getUrlLambda, + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + ElementWellKnown( + registrationHelperUrl = null, + enforceElementPro = null, + rageshakeUrl = null, + ) + ) + getUrlLambda.assertions().isCalledOnce() + .with(value("https://user.domain.org/.well-known/element/element.json")) + } + + @Test + fun `get element wellknown with full content`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "registration_helper_url": "a_registration_url", + "enforce_element_pro": true, + "rageshake_url": "a_rageshake_url" + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + ) + ) + } + + @Test + fun `get element wellknown with unknown key`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "registration_helper_url": "a_registration_url", + "enforce_element_pro": true, + "rageshake_url": "a_rageshake_url", + "other": true + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getElementWellKnown()).isEqualTo( + ElementWellKnown( + registrationHelperUrl = "a_registration_url", + enforceElementPro = true, + rageshakeUrl = "a_rageshake_url", + ) + ) + } + + @Test + fun `get element wellknown json error`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.success( + """{ + "registration_helper_url" = "a_registration_url", + error + }""".trimIndent().toByteArray() + ) + } + ) + assertThat(sut.getElementWellKnown()).isNull() + } + + @Test + fun `get element wellknown network error`() = runTest { + val sut = createDefaultSessionWellknownRetriever( + getUrlLambda = { + Result.failure(AN_EXCEPTION) + } + ) + assertThat(sut.getElementWellKnown()).isNull() + } + + private fun createDefaultSessionWellknownRetriever( + getUrlLambda: (String) -> Result, + ) = DefaultSessionWellknownRetriever( + matrixClient = FakeMatrixClient( + userIdServerNameLambda = { "user.domain.org" }, + getUrlLambda = getUrlLambda, + ), + parser = Json { ignoreUnknownKeys = true } + ) +} From aa1c51f5c9bec1682a02323aa123743c4bd7c1e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 14:53:40 +0200 Subject: [PATCH 13/14] Code cleanup --- .../wellknown/impl/DefaultSessionWellknownRetrieverTest.kt | 4 ++-- .../io/element/android/tests/konsist/KonsistLicenseTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index 73f614e91e..624370466a 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -22,7 +22,7 @@ import org.junit.Test class DefaultSessionWellknownRetrieverTest { @Test fun `get empty wellknown`() = runTest { - val getUrlLambda = lambdaRecorder>() { + val getUrlLambda = lambdaRecorder> { Result.success("{}".toByteArray()) } val sut = createDefaultSessionWellknownRetriever( @@ -150,7 +150,7 @@ class DefaultSessionWellknownRetrieverTest { @Test fun `get empty element wellknown`() = runTest { - val getUrlLambda = lambdaRecorder>() { + val getUrlLambda = lambdaRecorder> { Result.success("{}".toByteArray()) } val sut = createDefaultSessionWellknownRetriever( diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt index 9c0355f01e..c3a3d3e9e2 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt @@ -79,7 +79,7 @@ class KonsistLicenseTest { it.nameWithExtension != "KonsistLicenseTest.kt" && it.name.startsWith("Template ").not() } - .assertTrue { it -> + .assertTrue { it.text.count("New Vector") == 1 } } From e8f58afb29adacf50981dbe598b1fdcba83dadfd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 11 Aug 2025 17:36:45 +0200 Subject: [PATCH 14/14] Update submodule ref. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index a1b0f1b14c..76e10f6fa4 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit a1b0f1b14c27fe597c40ea5ff9657c771c623d9b +Subproject commit 76e10f6fa4db4196df245a3d29131a95d9e60a4d