Benoit Marty
1 year ago
10 changed files with 432 additions and 403 deletions
@ -0,0 +1,120 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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