Merge pull request #6271 from vector-im/feature/adm/full-matrix-id-homeserver-switching
[FTUE] Switch homeserver on full matrix id entry
This commit is contained in:
commit
d37b273eee
|
@ -0,0 +1 @@
|
||||||
|
FTUE - Adds automatic homeserver selection when typing a full matrix id during registration or login
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api
|
||||||
|
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MatrixPatternsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given user id cases, when checking isUserId, then returns expected`() {
|
||||||
|
val cases = listOf(
|
||||||
|
UserIdCase("foobar", isUserId = false),
|
||||||
|
UserIdCase("@foobar", isUserId = false),
|
||||||
|
UserIdCase("foobar@matrix.org", isUserId = false),
|
||||||
|
UserIdCase("@foobar: matrix.org", isUserId = false),
|
||||||
|
UserIdCase("@foobar:matrix.org", isUserId = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
cases.forEach { (input, expected) ->
|
||||||
|
MatrixPatterns.isUserId(input) shouldBeEqualTo expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class UserIdCase(val input: String, val isUserId: Boolean)
|
|
@ -57,3 +57,14 @@ fun TextInputLayout.setOnImeDoneListener(action: () -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TextInputLayout.setOnFocusLostListener(action: () -> Unit) {
|
||||||
|
editText().setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
when (hasFocus) {
|
||||||
|
false -> action()
|
||||||
|
else -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
||||||
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
|
||||||
object ResetPasswordMailConfirmed : OnboardingAction
|
object ResetPasswordMailConfirmed : OnboardingAction
|
||||||
|
|
||||||
|
data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction
|
||||||
sealed interface AuthenticateAction : OnboardingAction {
|
sealed interface AuthenticateAction : OnboardingAction {
|
||||||
data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
|
|
|
@ -50,6 +50,8 @@ import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAut
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
|
@ -142,6 +144,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||||
|
is OnboardingAction.MaybeUpdateHomeserverFromMatrixId -> handleMaybeUpdateHomeserver(action)
|
||||||
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
||||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||||
|
@ -162,6 +165,16 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleMaybeUpdateHomeserver(action: OnboardingAction.MaybeUpdateHomeserverFromMatrixId) {
|
||||||
|
val isFullMatrixId = MatrixPatterns.isUserId(action.userId)
|
||||||
|
if (isFullMatrixId) {
|
||||||
|
val domain = action.userId.getServerName().substringBeforeLast(":").ensureProtocol()
|
||||||
|
handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain))
|
||||||
|
} else {
|
||||||
|
// ignore the action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun withAction(action: OnboardingAction, block: (OnboardingAction) -> Unit) {
|
private fun withAction(action: OnboardingAction, block: (OnboardingAction) -> Unit) {
|
||||||
lastAction = action
|
lastAction = action
|
||||||
block(action)
|
block(action)
|
||||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.app.core.extensions.editText
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
import im.vector.app.core.extensions.hidePassword
|
import im.vector.app.core.extensions.hidePassword
|
||||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
|
import im.vector.app.core.extensions.setOnFocusLostListener
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
|
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
|
||||||
|
@ -59,6 +60,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||||
views.loginRoot.realignPercentagesToParent()
|
views.loginRoot.realignPercentagesToParent()
|
||||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
||||||
|
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubmitButton() {
|
private fun setupSubmitButton() {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.app.core.extensions.hasSurroundingSpaces
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
import im.vector.app.core.extensions.hidePassword
|
import im.vector.app.core.extensions.hidePassword
|
||||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
|
import im.vector.app.core.extensions.setOnFocusLostListener
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
|
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
|
||||||
|
@ -47,6 +48,7 @@ import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
import im.vector.app.features.onboarding.OnboardingViewState
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||||
|
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||||
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
||||||
|
@ -67,6 +69,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||||
views.createAccountRoot.realignPercentagesToParent()
|
views.createAccountRoot.realignPercentagesToParent()
|
||||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
||||||
|
views.createAccountInput.setOnFocusLostListener {
|
||||||
|
viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.createAccountInput.content()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubmitButton() {
|
private fun setupSubmitButton() {
|
||||||
|
@ -129,6 +134,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||||
throwable.isWeakPassword() || throwable.isInvalidPassword() -> {
|
throwable.isWeakPassword() || throwable.isInvalidPassword() -> {
|
||||||
views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable)
|
views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable)
|
||||||
}
|
}
|
||||||
|
throwable.isHomeserverUnavailable() -> {
|
||||||
|
views.createAccountInput.error = getString(R.string.login_error_homeserver_not_found)
|
||||||
|
}
|
||||||
throwable.isRegistrationDisabled() -> {
|
throwable.isRegistrationDisabled() -> {
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
MaterialAlertDialogBuilder(requireActivity())
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(R.string.dialog_title_error)
|
||||||
|
|
|
@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
|
||||||
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
|
private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG"
|
||||||
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
|
private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
|
||||||
|
private const val FRAGMENT_EDIT_HOMESERVER_TAG = "FRAGMENT_EDIT_HOMESERVER"
|
||||||
|
|
||||||
class FtueAuthVariant(
|
class FtueAuthVariant(
|
||||||
private val views: ActivityLoginBinding,
|
private val views: ActivityLoginBinding,
|
||||||
|
@ -220,10 +221,14 @@ class FtueAuthVariant(
|
||||||
activity.addFragmentToBackstack(
|
activity.addFragmentToBackstack(
|
||||||
views.loginFragmentContainer,
|
views.loginFragmentContainer,
|
||||||
FtueAuthCombinedServerSelectionFragment::class.java,
|
FtueAuthCombinedServerSelectionFragment::class.java,
|
||||||
option = commonOption
|
option = commonOption,
|
||||||
|
tag = FRAGMENT_EDIT_HOMESERVER_TAG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
|
OnboardingViewEvents.OnHomeserverEdited -> supportFragmentManager.popBackStack(
|
||||||
|
FRAGMENT_EDIT_HOMESERVER_TAG,
|
||||||
|
FragmentManager.POP_BACK_STACK_INCLUSIVE
|
||||||
|
)
|
||||||
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
|
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
|
||||||
is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents)
|
is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents)
|
||||||
OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog()
|
OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog()
|
||||||
|
|
|
@ -20,6 +20,7 @@ import im.vector.app.R
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.onboarding.ftueauth.LoginErrorParser.LoginErrorResult
|
import im.vector.app.features.onboarding.ftueauth.LoginErrorParser.LoginErrorResult
|
||||||
|
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
import org.matrix.android.sdk.api.failure.isInvalidPassword
|
||||||
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
import org.matrix.android.sdk.api.failure.isInvalidUsername
|
||||||
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
||||||
|
@ -40,6 +41,9 @@ class LoginErrorParser @Inject constructor(
|
||||||
throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> {
|
throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> {
|
||||||
LoginErrorResult(throwable, passwordError = stringProvider.getString(R.string.auth_invalid_login_param_space_in_password))
|
LoginErrorResult(throwable, passwordError = stringProvider.getString(R.string.auth_invalid_login_param_space_in_password))
|
||||||
}
|
}
|
||||||
|
throwable.isHomeserverUnavailable() -> {
|
||||||
|
LoginErrorResult(throwable, usernameOrIdError = stringProvider.getString(R.string.login_error_homeserver_not_found))
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
LoginErrorResult(throwable)
|
LoginErrorResult(throwable)
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:imeOptions="actionNext"
|
android:imeOptions="actionNext"
|
||||||
android:inputType="text"
|
android:inputType="textNoSuggestions"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:nextFocusForward="@id/loginPasswordInput" />
|
android:nextFocusForward="@id/loginPasswordInput" />
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:imeOptions="actionNext"
|
android:imeOptions="actionNext"
|
||||||
android:nextFocusForward="@id/createAccountPasswordInput"
|
android:nextFocusForward="@id/createAccountPasswordInput"
|
||||||
android:inputType="text"
|
android:inputType="textNoSuggestions"
|
||||||
android:maxLines="1" />
|
android:maxLines="1" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
|
@ -271,10 +271,7 @@ class OnboardingViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
fun `given in the sign up flow, when editing homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
||||||
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
givenCanSuccessfullyUpdateHomeserver(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE)
|
||||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE))
|
|
||||||
givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
|
|
||||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||||
|
@ -291,13 +288,45 @@ class OnboardingViewModelTest {
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a full matrix id, when maybe updating homeserver, then updates selected homeserver state and emits edited event`() = runTest {
|
||||||
|
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||||
|
givenCanSuccessfullyUpdateHomeserver(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE)
|
||||||
|
val test = viewModel.test()
|
||||||
|
val fullMatrixId = "@a-user:${A_HOMESERVER_URL.removePrefix("https://")}"
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(fullMatrixId))
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStatesChanges(
|
||||||
|
initialState,
|
||||||
|
{ copy(isLoading = true) },
|
||||||
|
{ copy(selectedHomeserver = SELECTED_HOMESERVER_STATE) },
|
||||||
|
{ copy(isLoading = false) }
|
||||||
|
|
||||||
|
)
|
||||||
|
.assertEvents(OnboardingViewEvents.OnHomeserverEdited)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a username, when maybe updating homeserver, then does nothing`() = runTest {
|
||||||
|
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||||
|
val test = viewModel.test()
|
||||||
|
val onlyUsername = "a-username"
|
||||||
|
|
||||||
|
viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(onlyUsername))
|
||||||
|
|
||||||
|
test
|
||||||
|
.assertStates(initialState)
|
||||||
|
.assertNoEvents()
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given in the sign up flow, when editing homeserver errors, then does not update the selected homeserver state and emits error`() = runTest {
|
fun `given in the sign up flow, when editing homeserver errors, then does not update the selected homeserver state and emits error`() = runTest {
|
||||||
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
viewModelWith(initialState.copy(onboardingFlow = OnboardingFlow.SignUp))
|
||||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
givenUpdatingHomeserverErrors(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE, AN_ERROR)
|
||||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE))
|
|
||||||
givenRegistrationActionErrors(RegisterAction.StartRegistration, AN_ERROR)
|
|
||||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||||
|
@ -552,8 +581,18 @@ class OnboardingViewModelTest {
|
||||||
fakeRegistrationActionHandler.givenResultsFor(results)
|
fakeRegistrationActionHandler.givenResultsFor(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenRegistrationActionErrors(action: RegisterAction, cause: Throwable) {
|
private fun givenCanSuccessfullyUpdateHomeserver(homeserverUrl: String, resultingState: SelectedHomeserverState) {
|
||||||
fakeRegistrationActionHandler.givenThrows(action, cause)
|
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG)
|
||||||
|
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||||
|
givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
|
||||||
|
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
|
||||||
|
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, A_HOMESERVER_CONFIG)
|
||||||
|
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||||
|
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
|
||||||
|
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.onboarding.ftueauth
|
||||||
|
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.test.fakes.FakeErrorFormatter
|
||||||
|
import im.vector.app.test.fakes.FakeStringProvider
|
||||||
|
import im.vector.app.test.fakes.toTestString
|
||||||
|
import im.vector.app.test.fixtures.aHomeserverUnavailableError
|
||||||
|
import im.vector.app.test.fixtures.aLoginEmailUnknownError
|
||||||
|
import im.vector.app.test.fixtures.anInvalidPasswordError
|
||||||
|
import im.vector.app.test.fixtures.anInvalidUserNameError
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_VALID_PASSWORD = "11111111"
|
||||||
|
private const val A_FORMATTED_ERROR_MESSAGE = "error message"
|
||||||
|
private const val ANOTHER_FORMATTED_ERROR_MESSAGE = "error message 2"
|
||||||
|
private val AN_ERROR = RuntimeException()
|
||||||
|
|
||||||
|
class LoginErrorParserTest {
|
||||||
|
|
||||||
|
private val fakeErrorFormatter = FakeErrorFormatter()
|
||||||
|
private val fakeStringProvider = FakeStringProvider()
|
||||||
|
|
||||||
|
private val loginErrorParser = LoginErrorParser(fakeErrorFormatter, fakeStringProvider.instance)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a generic error, when parsing, then has null username and password errors`() {
|
||||||
|
val cause = RuntimeException()
|
||||||
|
|
||||||
|
val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD)
|
||||||
|
|
||||||
|
result shouldBeEqualTo LoginErrorParser.LoginErrorResult(cause, usernameOrIdError = null, passwordError = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an invalid username error, when parsing, then has username error`() {
|
||||||
|
val cause = anInvalidUserNameError()
|
||||||
|
fakeErrorFormatter.given(cause, formatsTo = A_FORMATTED_ERROR_MESSAGE)
|
||||||
|
|
||||||
|
val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD)
|
||||||
|
|
||||||
|
result shouldBeEqualTo LoginErrorParser.LoginErrorResult(
|
||||||
|
cause,
|
||||||
|
usernameOrIdError = A_FORMATTED_ERROR_MESSAGE,
|
||||||
|
passwordError = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a homeserver unavailable error, when parsing, then has username error`() {
|
||||||
|
val cause = aHomeserverUnavailableError()
|
||||||
|
|
||||||
|
val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD)
|
||||||
|
|
||||||
|
result shouldBeEqualTo LoginErrorParser.LoginErrorResult(
|
||||||
|
cause,
|
||||||
|
usernameOrIdError = R.string.login_error_homeserver_not_found.toTestString(),
|
||||||
|
passwordError = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a login email unknown error, when parsing, then has username error`() {
|
||||||
|
val cause = aLoginEmailUnknownError()
|
||||||
|
|
||||||
|
val result = loginErrorParser.parse(throwable = cause, password = A_VALID_PASSWORD)
|
||||||
|
|
||||||
|
result shouldBeEqualTo LoginErrorParser.LoginErrorResult(
|
||||||
|
cause,
|
||||||
|
usernameOrIdError = R.string.login_login_with_email_error.toTestString(),
|
||||||
|
passwordError = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a password with surrounding spaces and an invalid password error, when parsing, then has password error`() {
|
||||||
|
val cause = anInvalidPasswordError()
|
||||||
|
|
||||||
|
val result = loginErrorParser.parse(throwable = cause, password = " $A_VALID_PASSWORD ")
|
||||||
|
|
||||||
|
result shouldBeEqualTo LoginErrorParser.LoginErrorResult(
|
||||||
|
cause,
|
||||||
|
usernameOrIdError = null,
|
||||||
|
passwordError = R.string.auth_invalid_login_param_space_in_password.toTestString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an error result with no known errors, then is unknown`() {
|
||||||
|
val errorResult = LoginErrorParser.LoginErrorResult(AN_ERROR, usernameOrIdError = null, passwordError = null)
|
||||||
|
val captures = Captures(expectUnknownError = true)
|
||||||
|
|
||||||
|
errorResult.callOnMethods(captures)
|
||||||
|
|
||||||
|
captures.unknownResult shouldBeEqualTo AN_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an error result with only username error, then is username or id error`() {
|
||||||
|
val errorResult = LoginErrorParser.LoginErrorResult(AN_ERROR, usernameOrIdError = A_FORMATTED_ERROR_MESSAGE, passwordError = null)
|
||||||
|
val captures = Captures(expectUsernameOrIdError = true)
|
||||||
|
|
||||||
|
errorResult.callOnMethods(captures)
|
||||||
|
|
||||||
|
captures.usernameOrIdError shouldBeEqualTo A_FORMATTED_ERROR_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an error result with only password error, then is password error`() {
|
||||||
|
val errorResult = LoginErrorParser.LoginErrorResult(AN_ERROR, usernameOrIdError = null, passwordError = A_FORMATTED_ERROR_MESSAGE)
|
||||||
|
val captures = Captures(expectPasswordError = true)
|
||||||
|
|
||||||
|
errorResult.callOnMethods(captures)
|
||||||
|
|
||||||
|
captures.passwordError shouldBeEqualTo A_FORMATTED_ERROR_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an error result with username and password error, then triggers both username and password error`() {
|
||||||
|
val errorResult = LoginErrorParser.LoginErrorResult(
|
||||||
|
AN_ERROR,
|
||||||
|
usernameOrIdError = A_FORMATTED_ERROR_MESSAGE,
|
||||||
|
passwordError = ANOTHER_FORMATTED_ERROR_MESSAGE
|
||||||
|
)
|
||||||
|
val captures = Captures(expectPasswordError = true, expectUsernameOrIdError = true)
|
||||||
|
|
||||||
|
errorResult.callOnMethods(captures)
|
||||||
|
|
||||||
|
captures.usernameOrIdError shouldBeEqualTo A_FORMATTED_ERROR_MESSAGE
|
||||||
|
captures.passwordError shouldBeEqualTo ANOTHER_FORMATTED_ERROR_MESSAGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LoginErrorParser.LoginErrorResult.callOnMethods(captures: Captures) {
|
||||||
|
onUnknown(captures.onUnknown)
|
||||||
|
onUsernameOrIdError(captures.onUsernameOrIdError)
|
||||||
|
onPasswordError(captures.onPasswordError)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Captures(
|
||||||
|
val expectUnknownError: Boolean = false,
|
||||||
|
val expectUsernameOrIdError: Boolean = false,
|
||||||
|
val expectPasswordError: Boolean = false,
|
||||||
|
) {
|
||||||
|
var unknownResult: Throwable? = null
|
||||||
|
var usernameOrIdError: String? = null
|
||||||
|
var passwordError: String? = null
|
||||||
|
|
||||||
|
val onUnknown: (Throwable) -> Unit = {
|
||||||
|
if (expectUnknownError) unknownResult = it else throw IllegalStateException("Not expected to be called")
|
||||||
|
}
|
||||||
|
val onUsernameOrIdError: (String) -> Unit = {
|
||||||
|
if (expectUsernameOrIdError) usernameOrIdError = it else throw IllegalStateException("Not expected to be called")
|
||||||
|
}
|
||||||
|
val onPasswordError: (String) -> Unit = {
|
||||||
|
if (expectPasswordError) passwordError = it else throw IllegalStateException("Not expected to be called")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
class FakeErrorFormatter : ErrorFormatter by mockk() {
|
||||||
|
fun given(cause: Throwable, formatsTo: String) {
|
||||||
|
every { toHumanReadable(cause) } returns formatsTo
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,4 +25,16 @@ fun a401ServerError() = Failure.ServerError(
|
||||||
MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED
|
MatrixError(MatrixError.M_UNAUTHORIZED, ""), HttpsURLConnection.HTTP_UNAUTHORIZED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun anInvalidUserNameError() = Failure.ServerError(
|
||||||
|
MatrixError(MatrixError.M_INVALID_USERNAME, ""), HttpsURLConnection.HTTP_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anInvalidPasswordError() = Failure.ServerError(
|
||||||
|
MatrixError(MatrixError.M_FORBIDDEN, "Invalid password"), HttpsURLConnection.HTTP_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aLoginEmailUnknownError() = Failure.ServerError(
|
||||||
|
MatrixError(MatrixError.M_FORBIDDEN, ""), HttpsURLConnection.HTTP_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())
|
fun aHomeserverUnavailableError() = Failure.NetworkConnection(UnknownHostException())
|
||||||
|
|
Loading…
Reference in New Issue