Benoit Marty
1 year ago
10 changed files with 432 additions and 403 deletions
@ -0,0 +1,120 @@ |
|||||||
|
/* |
||||||
|
* 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 com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest |
||||||
|
import io.element.android.libraries.architecture.Async |
||||||
|
import io.element.android.libraries.core.bool.orFalse |
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers |
||||||
|
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.di.AppScope |
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
import kotlinx.coroutines.Job |
||||||
|
import kotlinx.coroutines.async |
||||||
|
import kotlinx.coroutines.delay |
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow |
||||||
|
import kotlinx.coroutines.flow.StateFlow |
||||||
|
import kotlinx.coroutines.joinAll |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
import kotlinx.coroutines.withContext |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve homeserver base on search terms |
||||||
|
*/ |
||||||
|
@ContributesBinding(AppScope::class) |
||||||
|
class DefaultHomeserverResolver @Inject constructor( |
||||||
|
private val dispatchers: CoroutineDispatchers, |
||||||
|
private val wellknownRequest: WellknownRequest, |
||||||
|
): HomeserverResolver { |
||||||
|
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized) |
||||||
|
|
||||||
|
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow |
||||||
|
|
||||||
|
private var currentJob: Job? = null |
||||||
|
|
||||||
|
override suspend fun accept(userInput: String) { |
||||||
|
currentJob?.cancel() |
||||||
|
val cleanedUpUserInput = userInput.trim() |
||||||
|
mutableFlow.tryEmit(Async.Uninitialized) |
||||||
|
if (cleanedUpUserInput.length > 3) { |
||||||
|
delay(300) |
||||||
|
mutableFlow.tryEmit(Async.Loading()) |
||||||
|
withContext(dispatchers.io) { |
||||||
|
val list = getUrlCandidate(cleanedUpUserInput) |
||||||
|
currentJob = resolveList(userInput, list) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun CoroutineScope.resolveList(userInput: String, list: List<String>): Job { |
||||||
|
val currentList = mutableListOf<HomeserverData>() |
||||||
|
return launch { |
||||||
|
list.map { |
||||||
|
async { |
||||||
|
val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse() |
||||||
|
if (isValid) { |
||||||
|
// Emit the list as soon as possible |
||||||
|
currentList.add(HomeserverData(userInput, it, true)) |
||||||
|
mutableFlow.tryEmit(Async.Success(currentList)) |
||||||
|
} |
||||||
|
} |
||||||
|
}.joinAll() |
||||||
|
.also { |
||||||
|
// If list is empty, and the user as entered an URL, do not block the user. |
||||||
|
if (currentList.isEmpty()) { |
||||||
|
if (userInput.isValidUrl()) { |
||||||
|
mutableFlow.tryEmit( |
||||||
|
Async.Success( |
||||||
|
listOf( |
||||||
|
HomeserverData( |
||||||
|
userInput = userInput, |
||||||
|
homeserverUrl = userInput, |
||||||
|
isWellknownValid = false |
||||||
|
) |
||||||
|
) |
||||||
|
) |
||||||
|
) |
||||||
|
} else { |
||||||
|
mutableFlow.tryEmit(Async.Uninitialized) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun getUrlCandidate(data: String): List<String> { |
||||||
|
return buildList { |
||||||
|
val s = data.ensureProtocol() |
||||||
|
.removeSuffix("/") |
||||||
|
|
||||||
|
// Always try what the user has entered |
||||||
|
add(s) |
||||||
|
|
||||||
|
if (s.contains(".")) { |
||||||
|
// TLD detected? |
||||||
|
} else { |
||||||
|
add("$s.org") |
||||||
|
add("$s.com") |
||||||
|
add("$s.io") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
/* |
||||||
|
* 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 app.cash.molecule.RecompositionClock |
||||||
|
import app.cash.molecule.moleculeFlow |
||||||
|
import app.cash.turbine.test |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.libraries.architecture.Async |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
class ChangeAccountProviderFormPresenterTest { |
||||||
|
@Test |
||||||
|
fun `present - initial state`() = runTest { |
||||||
|
val homeServerResolver = FakeHomeServerResolver() |
||||||
|
val presenter = ChangeAccountProviderFormPresenter( |
||||||
|
homeServerResolver |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
assertThat(initialState.userInput).isEmpty() |
||||||
|
assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - enter text no result`() = runTest { |
||||||
|
val homeServerResolver = FakeHomeServerResolver() |
||||||
|
val presenter = ChangeAccountProviderFormPresenter( |
||||||
|
homeServerResolver |
||||||
|
) |
||||||
|
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.Uninitialized) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - enter text one then two results`() = runTest { |
||||||
|
val homeServerResolver = FakeHomeServerResolver() |
||||||
|
homeServerResolver.givenResult( |
||||||
|
listOf( |
||||||
|
listOf(aHomeserverData()), |
||||||
|
listOf(aHomeserverData(), aHomeserverData()), |
||||||
|
) |
||||||
|
) |
||||||
|
val presenter = ChangeAccountProviderFormPresenter( |
||||||
|
homeServerResolver |
||||||
|
) |
||||||
|
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()))) |
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData(), aHomeserverData()))) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
/* |
||||||
|
* 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.MutableStateFlow |
||||||
|
import kotlinx.coroutines.flow.StateFlow |
||||||
|
|
||||||
|
class FakeHomeServerResolver : HomeserverResolver { |
||||||
|
private var pendingResult: List<List<HomeserverData>> = emptyList() |
||||||
|
fun givenResult(result: List<List<HomeserverData>>) { |
||||||
|
pendingResult = result |
||||||
|
} |
||||||
|
|
||||||
|
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized) |
||||||
|
|
||||||
|
override fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow |
||||||
|
|
||||||
|
override suspend fun accept(userInput: String) { |
||||||
|
mutableFlow.tryEmit(Async.Uninitialized) |
||||||
|
delay(FAKE_DELAY_IN_MS) |
||||||
|
mutableFlow.tryEmit(Async.Loading()) |
||||||
|
// Sending the pending result |
||||||
|
if (pendingResult.isEmpty()) { |
||||||
|
mutableFlow.tryEmit(Async.Uninitialized) |
||||||
|
} else { |
||||||
|
pendingResult.forEach { |
||||||
|
delay(FAKE_DELAY_IN_MS) |
||||||
|
mutableFlow.tryEmit(Async.Success(it)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,161 @@ |
|||||||
|
/* |
||||||
|
* 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.loginpassword |
||||||
|
|
||||||
|
import app.cash.molecule.RecompositionClock |
||||||
|
import app.cash.molecule.moleculeFlow |
||||||
|
import app.cash.turbine.test |
||||||
|
import com.google.common.truth.Truth.assertThat |
||||||
|
import io.element.android.features.login.impl.datasource.AccountProviderDataSource |
||||||
|
import io.element.android.features.login.impl.util.defaultAccountProvider |
||||||
|
import io.element.android.libraries.architecture.Async |
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId |
||||||
|
import io.element.android.libraries.matrix.test.A_HOMESERVER |
||||||
|
import io.element.android.libraries.matrix.test.A_PASSWORD |
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID |
||||||
|
import io.element.android.libraries.matrix.test.A_THROWABLE |
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME |
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
class LoginPasswordPresenterTest { |
||||||
|
@Test |
||||||
|
fun `present - initial state`() = runTest { |
||||||
|
val authenticationService = FakeAuthenticationService() |
||||||
|
val accountProviderDataSource = AccountProviderDataSource() |
||||||
|
val presenter = LoginPasswordPresenter( |
||||||
|
authenticationService, |
||||||
|
accountProviderDataSource, |
||||||
|
) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) |
||||||
|
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) |
||||||
|
assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized) |
||||||
|
assertThat(initialState.submitEnabled).isFalse() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - enter login and password`() = runTest { |
||||||
|
val authenticationService = FakeAuthenticationService() |
||||||
|
val accountProviderDataSource = AccountProviderDataSource() |
||||||
|
val presenter = LoginPasswordPresenter( |
||||||
|
authenticationService, |
||||||
|
accountProviderDataSource, |
||||||
|
) |
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) |
||||||
|
val loginState = awaitItem() |
||||||
|
assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = "")) |
||||||
|
assertThat(loginState.submitEnabled).isFalse() |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) |
||||||
|
val loginAndPasswordState = awaitItem() |
||||||
|
assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) |
||||||
|
assertThat(loginAndPasswordState.submitEnabled).isTrue() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - submit`() = runTest { |
||||||
|
val authenticationService = FakeAuthenticationService() |
||||||
|
val accountProviderDataSource = AccountProviderDataSource() |
||||||
|
val presenter = LoginPasswordPresenter( |
||||||
|
authenticationService, |
||||||
|
accountProviderDataSource, |
||||||
|
) |
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) |
||||||
|
skipItems(1) |
||||||
|
val loginAndPasswordState = awaitItem() |
||||||
|
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) |
||||||
|
val submitState = awaitItem() |
||||||
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) |
||||||
|
val loggedInState = awaitItem() |
||||||
|
assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - submit with error`() = runTest { |
||||||
|
val authenticationService = FakeAuthenticationService() |
||||||
|
val accountProviderDataSource = AccountProviderDataSource() |
||||||
|
val presenter = LoginPasswordPresenter( |
||||||
|
authenticationService, |
||||||
|
accountProviderDataSource, |
||||||
|
) |
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) |
||||||
|
skipItems(1) |
||||||
|
val loginAndPasswordState = awaitItem() |
||||||
|
authenticationService.givenLoginError(A_THROWABLE) |
||||||
|
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) |
||||||
|
val submitState = awaitItem() |
||||||
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) |
||||||
|
val loggedInState = awaitItem() |
||||||
|
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `present - clear error`() = runTest { |
||||||
|
val authenticationService = FakeAuthenticationService() |
||||||
|
val accountProviderDataSource = AccountProviderDataSource() |
||||||
|
val presenter = LoginPasswordPresenter( |
||||||
|
authenticationService, |
||||||
|
accountProviderDataSource, |
||||||
|
) |
||||||
|
authenticationService.givenHomeserver(A_HOMESERVER) |
||||||
|
moleculeFlow(RecompositionClock.Immediate) { |
||||||
|
presenter.present() |
||||||
|
}.test { |
||||||
|
val initialState = awaitItem() |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) |
||||||
|
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) |
||||||
|
skipItems(1) |
||||||
|
val loginAndPasswordState = awaitItem() |
||||||
|
authenticationService.givenLoginError(A_THROWABLE) |
||||||
|
loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) |
||||||
|
val submitState = awaitItem() |
||||||
|
assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) |
||||||
|
val loggedInState = awaitItem() |
||||||
|
// Check an error was returned |
||||||
|
assertThat(loggedInState.loginAction).isEqualTo(Async.Failure<SessionId>(A_THROWABLE)) |
||||||
|
// Assert the error is then cleared |
||||||
|
loggedInState.eventSink(LoginPasswordEvents.ClearError) |
||||||
|
val clearedState = awaitItem() |
||||||
|
assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,308 +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.root |
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock |
|
||||||
import app.cash.molecule.moleculeFlow |
|
||||||
import app.cash.turbine.test |
|
||||||
import com.google.common.truth.Truth.assertThat |
|
||||||
import io.element.android.features.login.api.oidc.OidcAction |
|
||||||
import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow |
|
||||||
import io.element.android.features.login.impl.util.LoginConstants |
|
||||||
import io.element.android.libraries.architecture.Async |
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails |
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER |
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC |
|
||||||
import io.element.android.libraries.matrix.test.A_PASSWORD |
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID |
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE |
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME |
|
||||||
import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA |
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService |
|
||||||
import kotlinx.coroutines.test.runTest |
|
||||||
import org.junit.Test |
|
||||||
|
|
||||||
class LoginRootPresenterTest { |
|
||||||
@Test |
|
||||||
fun `present - initial state`() = runTest { |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
FakeAuthenticationService(), |
|
||||||
DefaultOidcActionFlow(), |
|
||||||
) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) |
|
||||||
assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) |
|
||||||
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) |
|
||||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) |
|
||||||
assertThat(initialState.submitEnabled).isFalse() |
|
||||||
cancelAndIgnoreRemainingEvents() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - initial state server load`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) |
|
||||||
assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) |
|
||||||
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) |
|
||||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) |
|
||||||
assertThat(initialState.submitEnabled).isFalse() |
|
||||||
val loadingState = awaitItem() |
|
||||||
assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading<MatrixHomeServerDetails>()) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER) |
|
||||||
skipItems(1) |
|
||||||
val loadedState = awaitItem() |
|
||||||
assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - initial state server load error and retry`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) |
|
||||||
assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) |
|
||||||
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) |
|
||||||
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) |
|
||||||
assertThat(initialState.submitEnabled).isFalse() |
|
||||||
val loadingState = awaitItem() |
|
||||||
assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading<MatrixHomeServerDetails>()) |
|
||||||
val aThrowable = Throwable("Error") |
|
||||||
authenticationService.givenChangeServerError(aThrowable) |
|
||||||
val errorState = awaitItem() |
|
||||||
assertThat(errorState.homeserverDetails).isEqualTo(Async.Failure<MatrixHomeServerDetails>(aThrowable)) |
|
||||||
// Retry |
|
||||||
errorState.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo) |
|
||||||
val loadingState2 = awaitItem() |
|
||||||
assertThat(loadingState2.homeserverDetails).isEqualTo(Async.Loading<MatrixHomeServerDetails>()) |
|
||||||
authenticationService.givenChangeServerError(null) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER) |
|
||||||
skipItems(1) |
|
||||||
val loadedState = awaitItem() |
|
||||||
assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - enter login and password`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) |
|
||||||
val loginState = awaitItem() |
|
||||||
assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = "")) |
|
||||||
assertThat(loginState.submitEnabled).isFalse() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) |
|
||||||
val loginAndPasswordState = awaitItem() |
|
||||||
assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) |
|
||||||
assertThat(loginAndPasswordState.submitEnabled).isTrue() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - oidc login`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
assertThat(initialState.submitEnabled).isTrue() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.Submit) |
|
||||||
val oidcState = awaitItem() |
|
||||||
assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - oidc login error`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) |
|
||||||
authenticationService.givenOidcError(A_THROWABLE) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
assertThat(initialState.submitEnabled).isTrue() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.Submit) |
|
||||||
val oidcState = awaitItem() |
|
||||||
assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - oidc custom tab login`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER_OIDC) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
assertThat(initialState.submitEnabled).isTrue() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.Submit) |
|
||||||
val oidcState = awaitItem() |
|
||||||
assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA)) |
|
||||||
// Oidc cancel, sdk error |
|
||||||
authenticationService.givenOidcCancelError(A_THROWABLE) |
|
||||||
oidcActionFlow.post(OidcAction.GoBack) |
|
||||||
val stateCancelSdkError = awaitItem() |
|
||||||
assertThat(stateCancelSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) |
|
||||||
// Oidc cancel, sdk OK |
|
||||||
authenticationService.givenOidcCancelError(null) |
|
||||||
oidcActionFlow.post(OidcAction.GoBack) |
|
||||||
val stateCancel = awaitItem() |
|
||||||
assertThat(stateCancel.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) |
|
||||||
// Oidc success, sdk error |
|
||||||
authenticationService.givenLoginError(A_THROWABLE) |
|
||||||
oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url)) |
|
||||||
val stateSuccessSdkErrorLoading = awaitItem() |
|
||||||
assertThat(stateSuccessSdkErrorLoading.loggedInState).isEqualTo(LoggedInState.LoggingIn) |
|
||||||
val stateSuccessSdkError = awaitItem() |
|
||||||
assertThat(stateSuccessSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) |
|
||||||
// Oidc success |
|
||||||
authenticationService.givenLoginError(null) |
|
||||||
oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url)) |
|
||||||
val stateSuccess = awaitItem() |
|
||||||
assertThat(stateSuccess.loggedInState).isEqualTo(LoggedInState.LoggingIn) |
|
||||||
val stateSuccessLoggedIn = awaitItem() |
|
||||||
assertThat(stateSuccessLoggedIn.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - submit`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) |
|
||||||
skipItems(1) |
|
||||||
val loginAndPasswordState = awaitItem() |
|
||||||
loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) |
|
||||||
val submitState = awaitItem() |
|
||||||
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) |
|
||||||
val loggedInState = awaitItem() |
|
||||||
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - submit with error`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) |
|
||||||
initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) |
|
||||||
skipItems(1) |
|
||||||
val loginAndPasswordState = awaitItem() |
|
||||||
authenticationService.givenLoginError(A_THROWABLE) |
|
||||||
loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) |
|
||||||
val submitState = awaitItem() |
|
||||||
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) |
|
||||||
val loggedInState = awaitItem() |
|
||||||
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
fun `present - clear error`() = runTest { |
|
||||||
val authenticationService = FakeAuthenticationService() |
|
||||||
val oidcActionFlow = DefaultOidcActionFlow() |
|
||||||
val presenter = LoginRootPresenter( |
|
||||||
authenticationService, |
|
||||||
oidcActionFlow, |
|
||||||
) |
|
||||||
authenticationService.givenHomeserver(A_HOMESERVER) |
|
||||||
moleculeFlow(RecompositionClock.Immediate) { |
|
||||||
presenter.present() |
|
||||||
}.test { |
|
||||||
val initialState = awaitItem() |
|
||||||
// Submit will return an error |
|
||||||
authenticationService.givenLoginError(A_THROWABLE) |
|
||||||
initialState.eventSink(LoginRootEvents.Submit) |
|
||||||
awaitItem() // Skip LoggingIn state |
|
||||||
|
|
||||||
// Check an error was returned |
|
||||||
val submittedState = awaitItem() |
|
||||||
assertThat(submittedState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) |
|
||||||
|
|
||||||
// Assert the error is then cleared |
|
||||||
submittedState.eventSink(LoginRootEvents.ClearError) |
|
||||||
val clearedState = awaitItem() |
|
||||||
assertThat(clearedState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue