Browse Source

Add test for ChangeAccountProviderPresenter and other presenters.

feature/fga/small_timeline_improvements
Benoit Marty 1 year ago
parent
commit
cd860e9de3
  1. 1
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt
  2. 8
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt
  3. 120
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt
  4. 2
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt
  5. 97
      features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt
  6. 87
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt
  7. 49
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt
  8. 161
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt
  9. 308
      features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt
  10. 2
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt

1
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt

@ -43,6 +43,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor(
fun handleEvents(event: ChangeAccountProviderFormEvents) { fun handleEvents(event: ChangeAccountProviderFormEvents) {
when (event) { when (event) {
is ChangeAccountProviderFormEvents.UserInput -> { is ChangeAccountProviderFormEvents.UserInput -> {
userInput.value = event.input
localCoroutineScope.userInput(event.input) localCoroutineScope.userInput(event.input)
} }
} }

8
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt

@ -51,3 +51,11 @@ fun aHomeserverDataList(): List<HomeserverData> {
) )
) )
} }
fun aHomeserverData(): HomeserverData {
return HomeserverData(
userInput = "matrix",
homeserverUrl = "https://matrix.org",
isWellknownValid = true,
)
}

120
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt

@ -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")
}
}
}
}

2
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt

@ -16,7 +16,7 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
data class HomeserverData( data class HomeserverData constructor(
// What the user has entered // What the user has entered
val userInput: String, val userInput: String,
// The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url

97
features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt

@ -16,102 +16,13 @@
package io.element.android.features.login.impl.changeaccountprovider.form package io.element.android.features.login.impl.changeaccountprovider.form
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
import io.element.android.libraries.architecture.Async 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 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.flow.StateFlow
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
/** /**
* Resolve homeserver base on search terms * Resolve homeserver base on search terms.
*/ */
class HomeserverResolver @Inject constructor( interface HomeserverResolver {
private val dispatchers: CoroutineDispatchers, fun flow(): StateFlow<Async<List<HomeserverData>>>
private val wellknownRequest: WellknownRequest, suspend fun accept(userInput: String)
) {
private val mutableFlow: MutableStateFlow<Async<List<HomeserverData>>> = MutableStateFlow(Async.Uninitialized)
fun flow(): StateFlow<Async<List<HomeserverData>>> = mutableFlow
private var currentJob: Job? = null
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")
}
}
}
} }

87
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt

@ -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())))
}
}
}

49
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt

@ -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))
}
}
}
}

161
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt

@ -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)
}
}
}

308
features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt

@ -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)
}
}
}

2
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt

@ -22,5 +22,5 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@Composable @Composable
public fun textFieldState(stateValue: String): MutableState<String> = fun textFieldState(stateValue: String): MutableState<String> =
remember(stateValue) { mutableStateOf(stateValue) } remember(stateValue) { mutableStateOf(stateValue) }

Loading…
Cancel
Save