diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 5f4563c3a9..279ac27767 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) androidTestImplementation(libs.test.junitext) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 706b954b86..1079e6d64b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -49,10 +49,11 @@ class DefaultHomeserverResolver @Inject constructor( emit(Async.Uninitialized) // Debounce delay(300) - val clean = userInput.trim() - if (clean.length < 4) return@flow + val trimmedUserInput = userInput.trim() + if (trimmedUserInput.length < 4) return@flow emit(Async.Loading()) - val list = getUrlCandidate(clean.ensureProtocol().removeSuffix("/")) + val candidateBase = trimmedUserInput.ensureProtocol().removeSuffix("/") + val list = getUrlCandidates(candidateBase) val currentList = Collections.synchronizedList(mutableListOf()) // Run all the requests in parallel withContext(dispatchers.io) { @@ -77,14 +78,14 @@ class DefaultHomeserverResolver @Inject constructor( } }.awaitAll() } - // If list is empty, and the user as entered an URL, do not block the user. + // If list is empty, and the user has entered an URL, do not block the user. if (currentList.isEmpty()) { - if (userInput.isValidUrl()) { + if (trimmedUserInput.isValidUrl()) { emit( Async.Success( listOf( HomeserverData( - homeserverUrl = userInput, + homeserverUrl = trimmedUserInput, isWellknownValid = false, supportSlidingSync = false, ) @@ -97,7 +98,7 @@ class DefaultHomeserverResolver @Inject constructor( } } - private fun getUrlCandidate(data: String): List { + private fun getUrlCandidates(data: String): List { return buildList { if (data.contains(".")) { // TLD detected? diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt new file mode 100644 index 0000000000..36aa47bbd0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.features.login.impl.changeaccountprovider.form.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/changeaccountprovider/form/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt index 1f18edc695..85d471edbd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt @@ -15,19 +15,10 @@ */ package io.element.android.features.login.impl.changeaccountprovider.form.network -import io.element.android.libraries.network.RetrofitFactory -import javax.inject.Inject - -class WellknownRequest @Inject constructor( - private val retrofitFactory: RetrofitFactory, -) { +interface WellknownRequest { /** - * Return the WellKnown data, if found. + * Return the WellKnown data, or throw an error if not found. * @param baseUrl for instance https://matrix.org */ - suspend fun execute(baseUrl: String): WellKnown { - val wellknownApi = retrofitFactory.create(baseUrl) - .create(WellknownAPI::class.java) - return wellknownApi.getWellKnown() - } + suspend fun execute(baseUrl: String): WellKnown } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt index 1a425993a8..dff15a63c6 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt @@ -21,22 +21,27 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownBaseConfig +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownSlidingSyncConfig import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest import org.junit.Test class ChangeAccountProviderFormPresenterTest { @Test fun `present - initial state`() = runTest { - val homeServerResolver = FakeHomeServerResolver() + val fakeWellknownRequest = FakeWellknownRequest() val changeServerPresenter = ChangeServerPresenter( FakeAuthenticationService(), AccountProviderDataSource() ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver, + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -50,13 +55,13 @@ class ChangeAccountProviderFormPresenterTest { @Test fun `present - enter text no result`() = runTest { - val homeServerResolver = FakeHomeServerResolver() + val fakeWellknownRequest = FakeWellknownRequest() val changeServerPresenter = ChangeServerPresenter( FakeAuthenticationService(), AccountProviderDataSource() ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver, + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -73,12 +78,41 @@ class ChangeAccountProviderFormPresenterTest { } @Test - fun `present - enter text one then two results`() = runTest { - val homeServerResolver = FakeHomeServerResolver() - homeServerResolver.givenResult( - listOf( - listOf(aHomeserverData()), - listOf(aHomeserverData(), aHomeserverData()), + fun `present - enter valid url no wellknown`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = ChangeAccountProviderFormPresenter( + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("https://test.org")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("https://test.org") + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + Async.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false) + ) + ) + ) + } + } + + @Test + fun `present - enter text one result no sliding sync`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + fakeWellknownRequest.givenResultMap( + mapOf( + "https://test.org" to aWellKnown().copy(slidingSyncProxy = null), ) ) val changeServerPresenter = ChangeServerPresenter( @@ -86,7 +120,7 @@ class ChangeAccountProviderFormPresenterTest { AccountProviderDataSource() ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver, + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -98,8 +132,62 @@ class ChangeAccountProviderFormPresenterTest { assertThat(withInputState.userInput).isEqualTo("test") assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) - assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData()))) - assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData(), aHomeserverData()))) + assertThat(awaitItem().userInputResult).isEqualTo( + Async.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false) + ) + ) + ) } } + + @Test + fun `present - enter text one result with sliding sync`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + fakeWellknownRequest.givenResultMap( + mapOf( + "https://test.io" to aWellKnown(), + ) + ) + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = ChangeAccountProviderFormPresenter( + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("test") + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + Async.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.io") + ) + ) + ) + } + } + + private fun aWellKnown(): WellKnown { + return WellKnown( + homeServer = WellKnownBaseConfig( + baseURL = A_HOMESERVER_URL + ), + identityServer = WellKnownBaseConfig( + baseURL = A_HOMESERVER_URL + ), + slidingSyncProxy = WellKnownSlidingSyncConfig( + url = A_HOMESERVER_URL + ) + ) + } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt deleted file mode 100644 index 8a712aa4a0..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.login.impl.changeaccountprovider.form - -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -class FakeHomeServerResolver : HomeserverResolver { - private var pendingResult: List> = emptyList() - fun givenResult(result: List>) { - pendingResult = result - } - - override suspend fun resolve(userInput: String): Flow>> = flow { - emit(Async.Uninitialized) - delay(FAKE_DELAY_IN_MS) - emit(Async.Loading()) - // Sending the pending result - if (pendingResult.isEmpty()) { - emit(Async.Uninitialized) - } else { - pendingResult.forEach { - delay(FAKE_DELAY_IN_MS) - emit(Async.Success(it)) - } - } - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt new file mode 100644 index 0000000000..0cc4a8435d --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.login.impl.changeaccountprovider.form + +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest + +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") + } +}