mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
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
15 changed files with 351 additions and 14 deletions
1
changelog.d/6162.wip
Normal file
1
changelog.d/6162.wip
Normal file
|
@ -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
|
||||
object ResetPasswordMailConfirmed : OnboardingAction
|
||||
|
||||
data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction
|
||||
sealed interface AuthenticateAction : OnboardingAction {
|
||||
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
|
||||
|
|
|
@ -50,6 +50,8 @@ import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAut
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
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.HomeServerHistoryService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
|
@ -142,6 +144,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||
is OnboardingAction.MaybeUpdateHomeserverFromMatrixId -> handleMaybeUpdateHomeserver(action)
|
||||
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(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) {
|
||||
lastAction = 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.hidePassword
|
||||
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.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
|
||||
|
@ -59,6 +60,7 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
|||
views.loginRoot.realignPercentagesToParent()
|
||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
||||
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) }
|
||||
}
|
||||
|
||||
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.hidePassword
|
||||
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.toReducedUrl
|
||||
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 kotlinx.coroutines.flow.launchIn
|
||||
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.isInvalidUsername
|
||||
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
||||
|
@ -67,6 +69,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||
views.createAccountRoot.realignPercentagesToParent()
|
||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
||||
views.createAccountInput.setOnFocusLostListener {
|
||||
viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.createAccountInput.content()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
|
@ -129,6 +134,9 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||
throwable.isWeakPassword() || throwable.isInvalidPassword() -> {
|
||||
views.createAccountPasswordInput.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
throwable.isHomeserverUnavailable() -> {
|
||||
views.createAccountInput.error = getString(R.string.login_error_homeserver_not_found)
|
||||
}
|
||||
throwable.isRegistrationDisabled() -> {
|
||||
MaterialAlertDialogBuilder(requireActivity())
|
||||
.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_LOGIN_TAG = "FRAGMENT_LOGIN_TAG"
|
||||
private const val FRAGMENT_EDIT_HOMESERVER_TAG = "FRAGMENT_EDIT_HOMESERVER"
|
||||
|
||||
class FtueAuthVariant(
|
||||
private val views: ActivityLoginBinding,
|
||||
|
@ -220,10 +221,14 @@ class FtueAuthVariant(
|
|||
activity.addFragmentToBackstack(
|
||||
views.loginFragmentContainer,
|
||||
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()
|
||||
is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents)
|
||||
OnboardingViewEvents.DisplayRegistrationFallback -> displayFallbackWebDialog()
|
||||
|
|
|
@ -20,6 +20,7 @@ import im.vector.app.R
|
|||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
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.isInvalidUsername
|
||||
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
||||
|
@ -40,6 +41,9 @@ class LoginErrorParser @Inject constructor(
|
|||
throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> {
|
||||
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 -> {
|
||||
LoginErrorResult(throwable)
|
||||
}
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:maxLines="1"
|
||||
android:nextFocusForward="@id/loginPasswordInput" />
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionNext"
|
||||
android:nextFocusForward="@id/createAccountPasswordInput"
|
||||
android:inputType="text"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
|
|
@ -271,10 +271,7 @@ class OnboardingViewModelTest {
|
|||
@Test
|
||||
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))
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
||||
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())
|
||||
givenCanSuccessfullyUpdateHomeserver(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||
|
@ -291,13 +288,45 @@ class OnboardingViewModelTest {
|
|||
.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
|
||||
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))
|
||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(A_HOMESERVER_URL, A_HOMESERVER_CONFIG)
|
||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, SELECTED_HOMESERVER_STATE))
|
||||
givenRegistrationActionErrors(RegisterAction.StartRegistration, AN_ERROR)
|
||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||
givenUpdatingHomeserverErrors(A_HOMESERVER_URL, SELECTED_HOMESERVER_STATE, AN_ERROR)
|
||||
val test = viewModel.test()
|
||||
|
||||
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(A_HOMESERVER_URL))
|
||||
|
@ -552,8 +581,18 @@ class OnboardingViewModelTest {
|
|||
fakeRegistrationActionHandler.givenResultsFor(results)
|
||||
}
|
||||
|
||||
private fun givenRegistrationActionErrors(action: RegisterAction, cause: Throwable) {
|
||||
fakeRegistrationActionHandler.givenThrows(action, cause)
|
||||
private fun givenCanSuccessfullyUpdateHomeserver(homeserverUrl: String, resultingState: SelectedHomeserverState) {
|
||||
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
|
||||
)
|
||||
|
||||
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())
|
||||
|
|
Loading…
Reference in a new issue