diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index 4a5445a262..8b17b318c1 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -22,7 +22,6 @@ import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.internal.network.ssl.Fingerprint sealed interface OnboardingAction : VectorViewModelAction { @@ -42,22 +41,9 @@ sealed interface OnboardingAction : VectorViewModelAction { // Login or Register, depending on the signMode data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction + object StopEmailValidationCheck : OnboardingAction - // Register actions - open class RegisterAction : OnboardingAction - - data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction() - object SendAgainThreePid : RegisterAction() - - // TODO Confirm Email (from link in the email, open in the phone, intercepted by the app) - data class ValidateThreePid(val code: String) : RegisterAction() - - data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction() - object StopEmailValidationCheck : RegisterAction() - - data class CaptchaDone(val captchaResponse: String) : RegisterAction() - object AcceptTerms : RegisterAction() - object RegisterDummy : RegisterAction() + data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction // Reset actions open class ResetAction : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 36020fbe61..303a4d5950 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -83,6 +83,7 @@ class OnboardingViewModel @AssistedInject constructor( private val vectorFeatures: VectorFeatures, private val analyticsTracker: AnalyticsTracker, private val uriFilenameResolver: UriFilenameResolver, + private val registrationActionHandler: RegistrationActionHandler, private val vectorOverrides: VectorOverrides ) : VectorViewModel(initialState) { @@ -116,16 +117,16 @@ class OnboardingViewModel @AssistedInject constructor( private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() + private val registrationWizard: RegistrationWizard + get() = authenticationService.getRegistrationWizard() + val currentThreePid: String? - get() = registrationWizard?.currentThreePid + get() = registrationWizard.currentThreePid // True when login and password has been sent with success to the homeserver val isRegistrationStarted: Boolean get() = authenticationService.isRegistrationStarted - private val registrationWizard: RegistrationWizard? - get() = authenticationService.getRegistrationWizard() - private val loginWizard: LoginWizard? get() = authenticationService.getLoginWizard() @@ -153,7 +154,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() - is OnboardingAction.RegisterAction -> handleRegisterAction(action) + is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction) is OnboardingAction.ResetAction -> handleResetAction(action) is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() @@ -164,6 +165,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + OnboardingAction.StopEmailValidationCheck -> currentJob = null }.exhaustive } @@ -266,131 +268,36 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleRegisterAction(action: OnboardingAction.RegisterAction) { - when (action) { - is OnboardingAction.CaptchaDone -> handleCaptchaDone(action) - is OnboardingAction.AcceptTerms -> handleAcceptTerms() - is OnboardingAction.RegisterDummy -> handleRegisterDummy() - is OnboardingAction.AddThreePid -> handleAddThreePid(action) - is OnboardingAction.SendAgainThreePid -> handleSendAgainThreePid() - is OnboardingAction.ValidateThreePid -> handleValidateThreePid(action) - is OnboardingAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action) - is OnboardingAction.StopEmailValidationCheck -> handleStopEmailValidationCheck() - } - } - - private fun handleCheckIfEmailHasBeenValidated(action: OnboardingAction.CheckIfEmailHasBeenValidated) { - // We do not want the common progress bar to be displayed, so we do not change asyncRegistration value in the state - currentJob = executeRegistrationStep(withLoading = false) { - it.checkIfEmailHasBeenValidated(action.delayMillis) - } - } - - private fun handleStopEmailValidationCheck() { - currentJob = null - } - - private fun handleValidateThreePid(action: OnboardingAction.ValidateThreePid) { - currentJob = executeRegistrationStep { - it.handleValidateThreePid(action.code) - } - } - - private fun executeRegistrationStep(withLoading: Boolean = true, - block: suspend (RegistrationWizard) -> RegistrationResult): Job { - if (withLoading) { - setState { copy(asyncRegistration = Loading()) } - } - return viewModelScope.launch { - try { - registrationWizard?.let { block(it) } - /* - // Simulate registration disabled - throw Failure.ServerError(MatrixError( - code = MatrixError.FORBIDDEN, - message = "Registration is disabled" - ), 403)) - */ - } catch (failure: Throwable) { - if (failure !is CancellationException) { - _viewEvents.post(OnboardingViewEvents.Failure(failure)) - } - null - } - ?.let { data -> - when (data) { - is RegistrationResult.Success -> onSessionCreated(data.session, isAccountCreated = true) - is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) - } - } - - setState { - copy( - asyncRegistration = Uninitialized - ) - } - } - } - - private fun handleAddThreePid(action: OnboardingAction.AddThreePid) { - setState { copy(asyncRegistration = Loading()) } + private fun handleRegisterAction(action: RegisterAction) { currentJob = viewModelScope.launch { - try { - registrationWizard?.addThreePid(action.threePid) - } catch (failure: Throwable) { - _viewEvents.post(OnboardingViewEvents.Failure(failure)) + if (action.hasLoadingState()) { + setState { copy(asyncRegistration = Loading()) } } - setState { - copy( - asyncRegistration = Uninitialized - ) - } - } - } - - private fun handleSendAgainThreePid() { - setState { copy(asyncRegistration = Loading()) } - currentJob = viewModelScope.launch { - try { - registrationWizard?.sendAgainThreePid() - } catch (failure: Throwable) { - _viewEvents.post(OnboardingViewEvents.Failure(failure)) - } - setState { - copy( - asyncRegistration = Uninitialized - ) - } - } - } - - private fun handleAcceptTerms() { - currentJob = executeRegistrationStep { - it.acceptTerms() - } - } - - private fun handleRegisterDummy() { - currentJob = executeRegistrationStep { - it.dummy() + kotlin.runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) } + .fold( + onSuccess = { + when (it) { + is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true) + is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult) + } + }, + onFailure = { + if (it !is CancellationException) { + _viewEvents.post(OnboardingViewEvents.Failure(it)) + } + } + ) + setState { copy(asyncRegistration = Uninitialized) } } } private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { reAuthHelper.data = action.password - currentJob = executeRegistrationStep { - it.createAccount( - action.username, - action.password, - action.initialDeviceName - ) - } - } - - private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) { - currentJob = executeRegistrationStep { - it.performReCaptcha(action.captchaResponse) - } + handleRegisterAction(RegisterAction.CreateAccount( + action.username, + action.password, + action.initialDeviceName + )) } private fun handleResetAction(action: OnboardingAction.ResetAction) { @@ -461,7 +368,7 @@ class OnboardingViewModel @AssistedInject constructor( } when (action.signMode) { - SignMode.SignUp -> startRegistrationFlow() + SignMode.SignUp -> handleRegisterAction(RegisterAction.RegisterDummy) SignMode.SignIn -> startAuthenticationFlow() SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) SignMode.Unknown -> Unit @@ -499,7 +406,7 @@ class OnboardingViewModel @AssistedInject constructor( // If there is a pending email validation continue on this step try { - if (registrationWizard?.isRegistrationStarted == true) { + if (registrationWizard.isRegistrationStarted) { currentThreePid?.let { handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it))) } @@ -730,12 +637,6 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun startRegistrationFlow() { - currentJob = executeRegistrationStep { - it.getRegistrationFlow() - } - } - private fun startAuthenticationFlow() { // Ensure Wizard is ready loginWizard @@ -745,8 +646,7 @@ class OnboardingViewModel @AssistedInject constructor( private fun onFlowResponse(flowResult: FlowResult) { // If dummy stage is mandatory, and password is already sent, do the dummy stage now - if (isRegistrationStarted && - flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { + if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) { handleRegisterDummy() } else { // Notify the user @@ -754,6 +654,10 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun handleRegisterDummy() { + handleRegisterAction(RegisterAction.RegisterDummy) + } + private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) { val state = awaitState() state.useCase?.let { useCase -> diff --git a/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt new file mode 100644 index 0000000000..2938681ce7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/onboarding/RegistrationActionHandler.kt @@ -0,0 +1,61 @@ +/* + * 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 + +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard +import javax.inject.Inject + +class RegistrationActionHandler @Inject constructor() { + + suspend fun handleRegisterAction(registrationWizard: RegistrationWizard, action: RegisterAction): RegistrationResult { + return when (action) { + RegisterAction.StartRegistration -> registrationWizard.getRegistrationFlow() + is RegisterAction.CaptchaDone -> registrationWizard.performReCaptcha(action.captchaResponse) + is RegisterAction.AcceptTerms -> registrationWizard.acceptTerms() + is RegisterAction.RegisterDummy -> registrationWizard.dummy() + is RegisterAction.AddThreePid -> registrationWizard.addThreePid(action.threePid) + is RegisterAction.SendAgainThreePid -> registrationWizard.sendAgainThreePid() + is RegisterAction.ValidateThreePid -> registrationWizard.handleValidateThreePid(action.code) + is RegisterAction.CheckIfEmailHasBeenValidated -> registrationWizard.checkIfEmailHasBeenValidated(action.delayMillis) + is RegisterAction.CreateAccount -> registrationWizard.createAccount(action.username, action.password, action.initialDeviceName) + } + } +} + +sealed interface RegisterAction { + object StartRegistration : RegisterAction + data class CreateAccount(val username: String, val password: String, val initialDeviceName: String) : RegisterAction + + data class AddThreePid(val threePid: RegisterThreePid) : RegisterAction + object SendAgainThreePid : RegisterAction + + // TODO Confirm Email (from link in the email, open in the phone, intercepted by the app) + data class ValidateThreePid(val code: String) : RegisterAction + + data class CheckIfEmailHasBeenValidated(val delayMillis: Long) : RegisterAction + + data class CaptchaDone(val captchaResponse: String) : RegisterAction + object AcceptTerms : RegisterAction + object RegisterDummy : RegisterAction +} + +fun RegisterAction.hasLoadingState() = when (this) { + is RegisterAction.CheckIfEmailHasBeenValidated -> false + else -> true +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt index e2e390ae2d..4773332138 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCaptchaFragment.kt @@ -39,6 +39,7 @@ import im.vector.app.databinding.FragmentLoginCaptchaBinding import im.vector.app.features.login.JavascriptResponse import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState +import im.vector.app.features.onboarding.RegisterAction import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber @@ -181,7 +182,7 @@ class FtueAuthCaptchaFragment @Inject constructor( val response = javascriptResponse?.response if (javascriptResponse?.action == "verifyCallback" && response != null) { - viewModel.handle(OnboardingAction.CaptchaDone(response)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(response))) } } return true diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt index bd5054f646..2800530152 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthGenericTextInputFormFragment.kt @@ -37,6 +37,7 @@ import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.onboarding.RegisterAction import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -138,7 +139,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA private fun onOtherButtonClicked() { when (params.mode) { TextInputFormFragmentMode.ConfirmMsisdn -> { - viewModel.handle(OnboardingAction.SendAgainThreePid) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid)) } else -> { // Should not happen, button is not displayed @@ -152,19 +153,19 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA if (text.isEmpty()) { // Perform dummy action - viewModel.handle(OnboardingAction.RegisterDummy) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.RegisterDummy)) } else { when (params.mode) { TextInputFormFragmentMode.SetEmail -> { - viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Email(text))) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(text)))) } TextInputFormFragmentMode.SetMsisdn -> { getCountryCodeOrShowError(text)?.let { countryCode -> - viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode)))) } } TextInputFormFragmentMode.ConfirmMsisdn -> { - viewModel.handle(OnboardingAction.ValidateThreePid(text)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.ValidateThreePid(text))) } } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt index 94758c7fad..ec72f52b9e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthWaitForEmailFragment.kt @@ -25,6 +25,7 @@ import com.airbnb.mvrx.args import im.vector.app.R import im.vector.app.databinding.FragmentLoginWaitForEmailBinding import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.RegisterAction import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.failure.is401 import javax.inject.Inject @@ -54,7 +55,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm override fun onResume() { super.onResume() - viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(0)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0))) } override fun onPause() { @@ -70,7 +71,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm override fun onError(throwable: Throwable) { if (throwable.is401()) { // Try again, with a delay - viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(10_000)) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000))) } else { super.onError(throwable) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt index 5ce9a5350d..03598d3a47 100755 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt @@ -32,6 +32,7 @@ import im.vector.app.features.login.terms.LoginTermsViewState import im.vector.app.features.login.terms.PolicyController import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewState +import im.vector.app.features.onboarding.RegisterAction import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms @@ -111,7 +112,7 @@ class FtueAuthTermsFragment @Inject constructor( } private fun submit() { - viewModel.handle(OnboardingAction.AcceptTerms) + viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AcceptTerms)) } override fun updateWithState(state: OnboardingViewState) { diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 085e1a8049..44d44a94b2 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -29,6 +29,7 @@ import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerHistoryService +import im.vector.app.test.fakes.FakeRegisterActionHandler import im.vector.app.test.fakes.FakeRegistrationWizard import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeStringProvider @@ -41,6 +42,9 @@ import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities private const val A_DISPLAY_NAME = "a display name" @@ -50,6 +54,7 @@ private val AN_UNSUPPORTED_PERSONALISATION_STATE = PersonalizationState( supportsChangingDisplayName = false, supportsChangingProfilePicture = false ) +private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration class OnboardingViewModelTest { @@ -63,6 +68,7 @@ class OnboardingViewModelTest { private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession) private val fakeAuthenticationService = FakeAuthenticationService() + private val fakeRegisterActionHandler = FakeRegisterActionHandler() lateinit var viewModel: OnboardingViewModel @@ -108,28 +114,67 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given register action requires more steps when handling action then posts next steps`() = runBlockingTest { + val test = viewModel.test(this) + val flowResult = FlowResult(missingStages = listOf(Stage.Email(true)), completedStages = listOf(Stage.Email(true))) + givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.FlowResponse(flowResult)) + + viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) + + test + .assertStatesWithPrevious( + initialState, + { copy(asyncRegistration = Loading()) }, + { copy(asyncRegistration = Uninitialized) } + ) + .assertEvents(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted = true)) + .finish() + } + + @Test + fun `given registration has started and has dummy step to do when handling action then ignores other steps and executes dummy`() = runBlockingTest { + val test = viewModel.test(this) + + val homeServerCapabilities = HomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true) + fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities) + val flowResult = FlowResult(missingStages = listOf(Stage.Dummy(mandatory = true), Stage.Email(true)), completedStages = emptyList()) + givenRegistrationResultsFor(listOf( + A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult), + RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession) + )) + givenSuccessfullyCreatesAccount() + + viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) + + test + .assertStatesWithPrevious( + initialState, + { copy(asyncRegistration = Loading()) }, + { copy(asyncLoginAction = Success(Unit), personalizationState = homeServerCapabilities.toPersonalisationState()) }, + { copy(asyncRegistration = Uninitialized) }, + + ) + .assertEvents(OnboardingViewEvents.OnAccountCreated) + .finish() + } + @Test fun `given homeserver does not support personalisation when registering account then updates state and emits account created event`() = runBlockingTest { - fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(HomeServerCapabilities(canChangeDisplayName = false, canChangeAvatar = false)) + val homeServerCapabilities = HomeServerCapabilities(canChangeDisplayName = false, canChangeAvatar = false) + fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities) + givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Success(fakeSession)) givenSuccessfullyCreatesAccount() val test = viewModel.test(this) - viewModel.handle(OnboardingAction.RegisterDummy) + viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION)) test - .assertStates( + .assertStatesWithPrevious( initialState, - initialState.copy(asyncRegistration = Loading()), - initialState.copy( - asyncLoginAction = Success(Unit), - asyncRegistration = Loading(), - personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE - ), - initialState.copy( - asyncLoginAction = Success(Unit), - asyncRegistration = Uninitialized, - personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE - ) + { copy(asyncRegistration = Loading()) }, + { copy(asyncLoginAction = Success(Unit), personalizationState = homeServerCapabilities.toPersonalisationState()) }, + { copy(asyncLoginAction = Success(Unit), asyncRegistration = Uninitialized) } ) .assertEvents(OnboardingViewEvents.OnAccountCreated) .finish() @@ -173,10 +218,10 @@ class OnboardingViewModelTest { viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) test - .assertStates( + .assertStatesWithPrevious( initialState, - initialState.copy(asyncDisplayName = Loading()), - initialState.copy(asyncDisplayName = Fail(AN_ERROR)), + { copy(asyncDisplayName = Loading()) }, + { copy(asyncDisplayName = Fail(AN_ERROR)) }, ) .assertEvents(OnboardingViewEvents.Failure(AN_ERROR)) .finish() @@ -264,6 +309,7 @@ class OnboardingViewModelTest { FakeVectorFeatures(), FakeAnalyticsTracker(), fakeUriFilenameResolver.instance, + fakeRegisterActionHandler.instance, FakeVectorOverrides() ) } @@ -286,14 +332,6 @@ class OnboardingViewModelTest { state.copy(asyncProfilePicture = Fail(cause)) ) - private fun givenSuccessfullyCreatesAccount() { - fakeActiveSessionHolder.expectSetsActiveSession(fakeSession) - val registrationWizard = FakeRegistrationWizard().also { it.givenSuccessfulDummy(fakeSession) } - fakeAuthenticationService.givenRegistrationWizard(registrationWizard) - fakeAuthenticationService.expectReset() - fakeSession.expectStartsSyncing() - } - private fun expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState: OnboardingViewState): List { return listOf( personalisedInitialState, @@ -304,4 +342,26 @@ class OnboardingViewModelTest { ) ) } + + private fun givenSuccessfullyCreatesAccount() { + fakeActiveSessionHolder.expectSetsActiveSession(fakeSession) + fakeAuthenticationService.expectReset() + fakeSession.expectStartsSyncing() + } + + private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationResult) { + givenRegistrationResultsFor(listOf(action to result)) + } + + private fun givenRegistrationResultsFor(results: List>) { + fakeAuthenticationService.givenRegistrationStarted(true) + val registrationWizard = FakeRegistrationWizard() + fakeAuthenticationService.givenRegistrationWizard(registrationWizard) + fakeRegisterActionHandler.givenResultsFor(registrationWizard, results) + } } + +private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState( + supportsChangingDisplayName = canChangeDisplayName, + supportsChangingProfilePicture = canChangeAvatar +) diff --git a/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt new file mode 100644 index 0000000000..87efa0bddc --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/onboarding/RegistrationActionHandlerTest.kt @@ -0,0 +1,73 @@ +/* + * 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 + +import im.vector.app.test.fakes.FakeRegistrationWizard +import im.vector.app.test.fakes.FakeSession +import kotlinx.coroutines.test.runBlockingTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.auth.registration.RegisterThreePid +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard + +private val A_SESSION = FakeSession() +private val AN_EXPECTED_RESULT = RegistrationResult.Success(A_SESSION) +private const val A_USERNAME = "a username" +private const val A_PASSWORD = "a password" +private const val AN_INITIAL_DEVICE_NAME = "a device name" +private const val A_CAPTCHA_RESPONSE = "a captcha response" +private const val A_PID_CODE = "a pid code" +private const val EMAIL_VALIDATED_DELAY = 10000L +private val A_PID_TO_REGISTER = RegisterThreePid.Email("an email") + +class RegistrationActionHandlerTest { + + private val fakeRegistrationWizard = FakeRegistrationWizard() + private val registrationActionHandler = RegistrationActionHandler() + + @Test + fun `when handling register action then delegates to wizard`() = runBlockingTest { + val cases = listOf( + case(RegisterAction.StartRegistration) { getRegistrationFlow() }, + case(RegisterAction.CaptchaDone(A_CAPTCHA_RESPONSE)) { performReCaptcha(A_CAPTCHA_RESPONSE) }, + case(RegisterAction.AcceptTerms) { acceptTerms() }, + case(RegisterAction.RegisterDummy) { dummy() }, + case(RegisterAction.AddThreePid(A_PID_TO_REGISTER)) { addThreePid(A_PID_TO_REGISTER) }, + case(RegisterAction.SendAgainThreePid) { sendAgainThreePid() }, + case(RegisterAction.ValidateThreePid(A_PID_CODE)) { handleValidateThreePid(A_PID_CODE) }, + case(RegisterAction.CheckIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY)) { checkIfEmailHasBeenValidated(EMAIL_VALIDATED_DELAY) }, + case(RegisterAction.CreateAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME)) { + createAccount(A_USERNAME, A_PASSWORD, AN_INITIAL_DEVICE_NAME) + } + ) + + cases.forEach { testSuccessfulActionDelegation(it) } + } + + private suspend fun testSuccessfulActionDelegation(case: Case) { + fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect) + + val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, case.action) + + result shouldBeEqualTo AN_EXPECTED_RESULT + } +} + +private fun case(action: RegisterAction, expect: suspend RegistrationWizard.() -> RegistrationResult) = Case(action, expect) + +private class Case(val action: RegisterAction, val expect: suspend RegistrationWizard.() -> RegistrationResult) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index e1a605c7df..10daf5de1e 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -23,10 +23,15 @@ import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.registration.RegistrationWizard class FakeAuthenticationService : AuthenticationService by mockk() { + fun givenRegistrationWizard(registrationWizard: RegistrationWizard) { every { getRegistrationWizard() } returns registrationWizard } + fun givenRegistrationStarted(started: Boolean) { + every { isRegistrationStarted } returns started + } + fun expectReset() { coJustRun { reset() } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt new file mode 100644 index 0000000000..6c01779a02 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegisterActionHandler.kt @@ -0,0 +1,43 @@ +/* + * 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.features.onboarding.RegisterAction +import im.vector.app.features.onboarding.RegistrationActionHandler +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.api.auth.registration.RegistrationResult +import org.matrix.android.sdk.api.auth.registration.RegistrationWizard + +class FakeRegisterActionHandler { + + val instance = mockk() + + fun givenResultFor(wizard: RegistrationWizard, action: RegisterAction, result: RegistrationResult) { + coEvery { instance.handleRegisterAction(wizard, action) } answers { + it.invocation.args.first() + result + } + } + + fun givenResultsFor(wizard: RegistrationWizard, result: List>) { + coEvery { instance.handleRegisterAction(wizard, any()) } answers { + val actionArg = it.invocation.args[1] as RegisterAction + result.first { it.first == actionArg }.second + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt index 6ae394eea1..2fc830e94a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRegistrationWizard.kt @@ -25,6 +25,14 @@ import org.matrix.android.sdk.api.session.Session class FakeRegistrationWizard : RegistrationWizard by mockk() { fun givenSuccessfulDummy(session: Session) { - coEvery { dummy() } returns RegistrationResult.Success(session) + givenSuccessFor(session) { dummy() } + } + + fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) { + coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result) + } + + fun givenSuccessfulAcceptTerms(session: Session) { + coEvery { acceptTerms() } returns RegistrationResult.Success(session) } }