Merge pull request #5408 from vector-im/feature/adm/onboarding-tests

FTUE - Onboarding registration steps unit tests
This commit is contained in:
Adam Brown 2022-03-18 15:57:38 +00:00 committed by GitHub
commit ea9c9ae490
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 490 additions and 239 deletions

1
changelog.d/5408.misc Normal file
View File

@ -0,0 +1 @@
Improved onboarding registration unit test coverage

View File

@ -22,63 +22,49 @@ import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.ServerType import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode import im.vector.app.features.login.SignMode
import org.matrix.android.sdk.api.auth.data.Credentials 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 import org.matrix.android.sdk.internal.network.ssl.Fingerprint
sealed class OnboardingAction : VectorViewModelAction { sealed interface OnboardingAction : VectorViewModelAction {
data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction() data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction() data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction
data class UpdateServerType(val serverType: ServerType) : OnboardingAction() data class UpdateServerType(val serverType: ServerType) : OnboardingAction
data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction() data class UpdateHomeServer(val homeServerUrl: String) : OnboardingAction
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction() data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction
object ResetUseCase : OnboardingAction() object ResetUseCase : OnboardingAction
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction() data class UpdateSignMode(val signMode: SignMode) : OnboardingAction
data class LoginWithToken(val loginToken: String) : OnboardingAction() data class LoginWithToken(val loginToken: String) : OnboardingAction
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction() data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction() data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
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
// Login or Register, depending on the signMode // Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction() data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
object StopEmailValidationCheck : OnboardingAction
// Register actions data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction
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()
// Reset actions // Reset actions
open class ResetAction : OnboardingAction() sealed interface ResetAction : OnboardingAction
object ResetHomeServerType : ResetAction() object ResetHomeServerType : ResetAction
object ResetHomeServerUrl : ResetAction() object ResetHomeServerUrl : ResetAction
object ResetSignMode : ResetAction() object ResetSignMode : ResetAction
object ResetLogin : ResetAction() object ResetLogin : ResetAction
object ResetResetPassword : ResetAction() object ResetResetPassword : ResetAction
// Homeserver history // Homeserver history
object ClearHomeServerHistory : OnboardingAction() object ClearHomeServerHistory : OnboardingAction
data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction() data class PostViewEvent(val viewEvent: OnboardingViewEvents) : OnboardingAction
data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction() data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction
object PersonalizeProfile : OnboardingAction() object PersonalizeProfile : OnboardingAction
data class UpdateDisplayName(val displayName: String) : OnboardingAction() data class UpdateDisplayName(val displayName: String) : OnboardingAction
object UpdateDisplayNameSkipped : OnboardingAction() object UpdateDisplayNameSkipped : OnboardingAction
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction() data class ProfilePictureSelected(val uri: Uri) : OnboardingAction
object SaveSelectedProfilePicture : OnboardingAction() object SaveSelectedProfilePicture : OnboardingAction
object UpdateProfilePictureSkipped : OnboardingAction() object UpdateProfilePictureSkipped : OnboardingAction
} }

View File

@ -83,6 +83,7 @@ class OnboardingViewModel @AssistedInject constructor(
private val vectorFeatures: VectorFeatures, private val vectorFeatures: VectorFeatures,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val uriFilenameResolver: UriFilenameResolver, private val uriFilenameResolver: UriFilenameResolver,
private val registrationActionHandler: RegistrationActionHandler,
private val vectorOverrides: VectorOverrides private val vectorOverrides: VectorOverrides
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) { ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@ -116,16 +117,16 @@ class OnboardingViewModel @AssistedInject constructor(
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
private val registrationWizard: RegistrationWizard
get() = authenticationService.getRegistrationWizard()
val currentThreePid: String? val currentThreePid: String?
get() = registrationWizard?.currentThreePid get() = registrationWizard.currentThreePid
// True when login and password has been sent with success to the homeserver // True when login and password has been sent with success to the homeserver
val isRegistrationStarted: Boolean val isRegistrationStarted: Boolean
get() = authenticationService.isRegistrationStarted get() = authenticationService.isRegistrationStarted
private val registrationWizard: RegistrationWizard?
get() = authenticationService.getRegistrationWizard()
private val loginWizard: LoginWizard? private val loginWizard: LoginWizard?
get() = authenticationService.getLoginWizard() get() = authenticationService.getLoginWizard()
@ -153,7 +154,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action) is OnboardingAction.ResetPassword -> handleResetPassword(action)
is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() is OnboardingAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is OnboardingAction.RegisterAction -> handleRegisterAction(action) is OnboardingAction.PostRegisterAction -> handleRegisterAction(action.registerAction)
is OnboardingAction.ResetAction -> handleResetAction(action) is OnboardingAction.ResetAction -> handleResetAction(action)
is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action)
OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory()
@ -164,6 +165,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action)
OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture()
is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation()
}.exhaustive }.exhaustive
} }
@ -266,131 +268,41 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun handleRegisterAction(action: OnboardingAction.RegisterAction) { private fun handleRegisterAction(action: 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()) }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
try { if (action.hasLoadingState()) {
registrationWizard?.addThreePid(action.threePid) setState { copy(asyncRegistration = Loading()) }
} catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
} }
setState { runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
copy( .fold(
asyncRegistration = Uninitialized onSuccess = {
) when {
} action.ignoresResult() -> {
} // do nothing
} }
else -> when (it) {
private fun handleSendAgainThreePid() { is RegistrationResult.Success -> onSessionCreated(it.session, isAccountCreated = true)
setState { copy(asyncRegistration = Loading()) } is RegistrationResult.FlowResponse -> onFlowResponse(it.flowResult)
currentJob = viewModelScope.launch { }
try { }
registrationWizard?.sendAgainThreePid() },
} catch (failure: Throwable) { onFailure = {
_viewEvents.post(OnboardingViewEvents.Failure(failure)) if (it !is CancellationException) {
} _viewEvents.post(OnboardingViewEvents.Failure(it))
setState { }
copy( }
asyncRegistration = Uninitialized )
) setState { copy(asyncRegistration = Uninitialized) }
}
}
}
private fun handleAcceptTerms() {
currentJob = executeRegistrationStep {
it.acceptTerms()
}
}
private fun handleRegisterDummy() {
currentJob = executeRegistrationStep {
it.dummy()
} }
} }
private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) { private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) {
reAuthHelper.data = action.password reAuthHelper.data = action.password
currentJob = executeRegistrationStep { handleRegisterAction(RegisterAction.CreateAccount(
it.createAccount( action.username,
action.username, action.password,
action.password, action.initialDeviceName
action.initialDeviceName ))
)
}
}
private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) {
currentJob = executeRegistrationStep {
it.performReCaptcha(action.captchaResponse)
}
} }
private fun handleResetAction(action: OnboardingAction.ResetAction) { private fun handleResetAction(action: OnboardingAction.ResetAction) {
@ -461,7 +373,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
when (action.signMode) { when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow() SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
SignMode.SignIn -> startAuthenticationFlow() SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit SignMode.Unknown -> Unit
@ -499,7 +411,7 @@ class OnboardingViewModel @AssistedInject constructor(
// If there is a pending email validation continue on this step // If there is a pending email validation continue on this step
try { try {
if (registrationWizard?.isRegistrationStarted == true) { if (registrationWizard.isRegistrationStarted) {
currentThreePid?.let { currentThreePid?.let {
handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it))) handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnSendEmailSuccess(it)))
} }
@ -730,12 +642,6 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun startRegistrationFlow() {
currentJob = executeRegistrationStep {
it.getRegistrationFlow()
}
}
private fun startAuthenticationFlow() { private fun startAuthenticationFlow() {
// Ensure Wizard is ready // Ensure Wizard is ready
loginWizard loginWizard
@ -745,8 +651,7 @@ class OnboardingViewModel @AssistedInject constructor(
private fun onFlowResponse(flowResult: FlowResult) { private fun onFlowResponse(flowResult: FlowResult) {
// If dummy stage is mandatory, and password is already sent, do the dummy stage now // If dummy stage is mandatory, and password is already sent, do the dummy stage now
if (isRegistrationStarted && if (isRegistrationStarted && flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
flowResult.missingStages.any { it is Stage.Dummy && it.mandatory }) {
handleRegisterDummy() handleRegisterDummy()
} else { } else {
// Notify the user // Notify the user
@ -754,6 +659,10 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun handleRegisterDummy() {
handleRegisterAction(RegisterAction.RegisterDummy)
}
private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) { private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
val state = awaitState() val state = awaitState()
state.useCase?.let { useCase -> state.useCase?.let { useCase ->
@ -1006,6 +915,10 @@ class OnboardingViewModel @AssistedInject constructor(
private fun completePersonalization() { private fun completePersonalization() {
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete) _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
} }
private fun cancelWaitForEmailValidation() {
currentJob = null
}
} }
private fun LoginMode.supportsSignModeScreen(): Boolean { private fun LoginMode.supportsSignModeScreen(): Boolean {

View File

@ -0,0 +1,67 @@
/*
* 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.ignoresResult() = when (this) {
is RegisterAction.AddThreePid -> true
is RegisterAction.SendAgainThreePid -> true
else -> false
}
fun RegisterAction.hasLoadingState() = when (this) {
is RegisterAction.CheckIfEmailHasBeenValidated -> false
else -> true
}

View File

@ -39,6 +39,7 @@ import im.vector.app.databinding.FragmentLoginCaptchaBinding
import im.vector.app.features.login.JavascriptResponse import im.vector.app.features.login.JavascriptResponse
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.RegisterAction
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber import timber.log.Timber
@ -181,7 +182,7 @@ class FtueAuthCaptchaFragment @Inject constructor(
val response = javascriptResponse?.response val response = javascriptResponse?.response
if (javascriptResponse?.action == "verifyCallback" && response != null) { if (javascriptResponse?.action == "verifyCallback" && response != null) {
viewModel.handle(OnboardingAction.CaptchaDone(response)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CaptchaDone(response)))
} }
} }
return true return true

View File

@ -37,6 +37,7 @@ import im.vector.app.databinding.FragmentLoginGenericTextInputFormBinding
import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.TextInputFormFragmentMode
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.RegisterAction
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -138,7 +139,7 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
private fun onOtherButtonClicked() { private fun onOtherButtonClicked() {
when (params.mode) { when (params.mode) {
TextInputFormFragmentMode.ConfirmMsisdn -> { TextInputFormFragmentMode.ConfirmMsisdn -> {
viewModel.handle(OnboardingAction.SendAgainThreePid) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.SendAgainThreePid))
} }
else -> { else -> {
// Should not happen, button is not displayed // Should not happen, button is not displayed
@ -152,19 +153,19 @@ class FtueAuthGenericTextInputFormFragment @Inject constructor() : AbstractFtueA
if (text.isEmpty()) { if (text.isEmpty()) {
// Perform dummy action // Perform dummy action
viewModel.handle(OnboardingAction.RegisterDummy) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.RegisterDummy))
} else { } else {
when (params.mode) { when (params.mode) {
TextInputFormFragmentMode.SetEmail -> { TextInputFormFragmentMode.SetEmail -> {
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Email(text))) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(text))))
} }
TextInputFormFragmentMode.SetMsisdn -> { TextInputFormFragmentMode.SetMsisdn -> {
getCountryCodeOrShowError(text)?.let { countryCode -> getCountryCodeOrShowError(text)?.let { countryCode ->
viewModel.handle(OnboardingAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Msisdn(text, countryCode))))
} }
} }
TextInputFormFragmentMode.ConfirmMsisdn -> { TextInputFormFragmentMode.ConfirmMsisdn -> {
viewModel.handle(OnboardingAction.ValidateThreePid(text)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.ValidateThreePid(text)))
} }
} }
} }

View File

@ -25,6 +25,7 @@ import com.airbnb.mvrx.args
import im.vector.app.R import im.vector.app.R
import im.vector.app.databinding.FragmentLoginWaitForEmailBinding import im.vector.app.databinding.FragmentLoginWaitForEmailBinding
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.RegisterAction
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.failure.is401 import org.matrix.android.sdk.api.failure.is401
import javax.inject.Inject import javax.inject.Inject
@ -54,7 +55,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(0)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(0)))
} }
override fun onPause() { override fun onPause() {
@ -70,7 +71,7 @@ class FtueAuthWaitForEmailFragment @Inject constructor() : AbstractFtueAuthFragm
override fun onError(throwable: Throwable) { override fun onError(throwable: Throwable) {
if (throwable.is401()) { if (throwable.is401()) {
// Try again, with a delay // Try again, with a delay
viewModel.handle(OnboardingAction.CheckIfEmailHasBeenValidated(10_000)) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.CheckIfEmailHasBeenValidated(10_000)))
} else { } else {
super.onError(throwable) super.onError(throwable)
} }

View File

@ -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.login.terms.PolicyController
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.RegisterAction
import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment import im.vector.app.features.onboarding.ftueauth.AbstractFtueAuthFragment
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms
@ -111,7 +112,7 @@ class FtueAuthTermsFragment @Inject constructor(
} }
private fun submit() { private fun submit() {
viewModel.handle(OnboardingAction.AcceptTerms) viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AcceptTerms))
} }
override fun updateWithState(state: OnboardingViewState) { override fun updateWithState(state: OnboardingViewState) {

View File

@ -23,12 +23,14 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.SignMode
import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAnalyticsTracker import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeAuthenticationService import im.vector.app.test.fakes.FakeAuthenticationService
import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory import im.vector.app.test.fakes.FakeHomeServerConnectionConfigFactory
import im.vector.app.test.fakes.FakeHomeServerHistoryService 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.FakeRegistrationWizard
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fakes.FakeStringProvider
@ -36,20 +38,27 @@ import im.vector.app.test.fakes.FakeUri
import im.vector.app.test.fakes.FakeUriFilenameResolver import im.vector.app.test.fakes.FakeUriFilenameResolver
import im.vector.app.test.fakes.FakeVectorFeatures import im.vector.app.test.fakes.FakeVectorFeatures
import im.vector.app.test.fakes.FakeVectorOverrides import im.vector.app.test.fakes.FakeVectorOverrides
import im.vector.app.test.fixtures.aHomeServerCapabilities
import im.vector.app.test.test import im.vector.app.test.test
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.auth.registration.FlowResult
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.Stage
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
private const val A_DISPLAY_NAME = "a display name" private const val A_DISPLAY_NAME = "a display name"
private const val A_PICTURE_FILENAME = "a-picture.png" private const val A_PICTURE_FILENAME = "a-picture.png"
private val AN_ERROR = RuntimeException("an error!") private val AN_ERROR = RuntimeException("an error!")
private val AN_UNSUPPORTED_PERSONALISATION_STATE = PersonalizationState( private val A_LOADABLE_REGISTER_ACTION = RegisterAction.StartRegistration
supportsChangingDisplayName = false, private val A_NON_LOADABLE_REGISTER_ACTION = RegisterAction.CheckIfEmailHasBeenValidated(delayMillis = -1L)
supportsChangingProfilePicture = false private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.AddThreePid(RegisterThreePid.Email("an email"))
) private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT)
class OnboardingViewModelTest { class OnboardingViewModelTest {
@ -63,6 +72,7 @@ class OnboardingViewModelTest {
private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakeUriFilenameResolver = FakeUriFilenameResolver()
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession) private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
private val fakeAuthenticationService = FakeAuthenticationService() private val fakeAuthenticationService = FakeAuthenticationService()
private val fakeRegisterActionHandler = FakeRegisterActionHandler()
lateinit var viewModel: OnboardingViewModel lateinit var viewModel: OnboardingViewModel
@ -72,7 +82,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `when handling PostViewEvent then emits contents as view event`() = runBlockingTest { fun `when handling PostViewEvent, then emits contents as view event`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
@ -83,7 +93,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given supports changing display name when handling PersonalizeProfile then emits contents choose display name`() = runBlockingTest { fun `given supports changing display name, when handling PersonalizeProfile, then emits contents choose display name`() = runBlockingTest {
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false)) val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = true, supportsChangingProfilePicture = false))
viewModel = createViewModel(initialState) viewModel = createViewModel(initialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -96,7 +106,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given only supports changing profile picture when handling PersonalizeProfile then emits contents choose profile picture`() = runBlockingTest { fun `given only supports changing profile picture, when handling PersonalizeProfile, then emits contents choose profile picture`() = runBlockingTest {
val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true)) val initialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingDisplayName = false, supportsChangingProfilePicture = true))
viewModel = createViewModel(initialState) viewModel = createViewModel(initialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -109,34 +119,109 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given homeserver does not support personalisation when registering account then updates state and emits account created event`() = runBlockingTest { fun `when handling SignUp then sets sign mode to sign up and starts registration`() = runBlockingTest {
fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(HomeServerCapabilities(canChangeDisplayName = false, canChangeAvatar = false)) givenRegistrationResultFor(RegisterAction.StartRegistration, ANY_CONTINUING_REGISTRATION_RESULT)
givenSuccessfullyCreatesAccount()
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.RegisterDummy) viewModel.handle(OnboardingAction.UpdateSignMode(SignMode.SignUp))
test test
.assertStates( .assertStatesChanges(
initialState, initialState,
initialState.copy(asyncRegistration = Loading()), { copy(signMode = SignMode.SignUp) },
initialState.copy( { copy(asyncRegistration = Loading()) },
asyncLoginAction = Success(Unit), { copy(asyncRegistration = Uninitialized) }
asyncRegistration = Loading(), )
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE .assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
), .finish()
initialState.copy( }
asyncLoginAction = Success(Unit),
asyncRegistration = Uninitialized, @Test
personalizationState = AN_UNSUPPORTED_PERSONALISATION_STATE fun `given register action requires more steps, when handling action, then posts next steps`() = runBlockingTest {
) val test = viewModel.test(this)
givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, ANY_CONTINUING_REGISTRATION_RESULT)
viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncRegistration = Uninitialized) }
)
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish()
}
@Test
fun `given register action is non loadable, when handling action, then posts next steps without loading`() = runBlockingTest {
val test = viewModel.test(this)
givenRegistrationResultFor(A_NON_LOADABLE_REGISTER_ACTION, ANY_CONTINUING_REGISTRATION_RESULT)
viewModel.handle(OnboardingAction.PostRegisterAction(A_NON_LOADABLE_REGISTER_ACTION))
test
.assertState(initialState)
.assertEvents(OnboardingViewEvents.RegistrationFlowResult(ANY_CONTINUING_REGISTRATION_RESULT.flowResult, isRegistrationStarted = true))
.finish()
}
@Test
fun `given register action ignores result, when handling action, then does nothing on success`() = runBlockingTest {
val test = viewModel.test(this)
givenRegistrationResultFor(A_RESULT_IGNORED_REGISTER_ACTION, RegistrationResult.FlowResponse(AN_IGNORED_FLOW_RESULT))
viewModel.handle(OnboardingAction.PostRegisterAction(A_RESULT_IGNORED_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncRegistration = Uninitialized) }
)
.assertNoEvents()
.finish()
}
@Test
fun `when registering account, then updates state and emits account created event`() = runBlockingTest {
givenRegistrationResultFor(A_LOADABLE_REGISTER_ACTION, RegistrationResult.Success(fakeSession))
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
val test = viewModel.test(this)
viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) },
{ copy(asyncLoginAction = Success(Unit), asyncRegistration = Uninitialized) }
) )
.assertEvents(OnboardingViewEvents.OnAccountCreated) .assertEvents(OnboardingViewEvents.OnAccountCreated)
.finish() .finish()
} }
@Test @Test
fun `given changing profile picture is supported when updating display name then updates upstream user display name and moves to choose profile picture`() = runBlockingTest { fun `given registration has started and has dummy step to do, when handling action, then ignores other steps and executes dummy`() = runBlockingTest {
givenSuccessfulRegistrationForStartAndDummySteps(missingStages = listOf(Stage.Dummy(mandatory = true)))
val test = viewModel.test(this)
viewModel.handle(OnboardingAction.PostRegisterAction(A_LOADABLE_REGISTER_ACTION))
test
.assertStatesChanges(
initialState,
{ copy(asyncRegistration = Loading()) },
{ copy(asyncLoginAction = Success(Unit), personalizationState = A_HOMESERVER_CAPABILITIES.toPersonalisationState()) },
{ copy(asyncRegistration = Uninitialized) }
)
.assertEvents(OnboardingViewEvents.OnAccountCreated)
.finish()
}
@Test
fun `given changing profile picture is supported, when updating display name, then updates upstream user display name and moves to choose profile picture`() = runBlockingTest {
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true)) val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = true))
viewModel = createViewModel(personalisedInitialState) viewModel = createViewModel(personalisedInitialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -144,14 +229,14 @@ class OnboardingViewModelTest {
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
test test
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState)) .assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
.assertEvents(OnboardingViewEvents.OnChooseProfilePicture) .assertEvents(OnboardingViewEvents.OnChooseProfilePicture)
.finish() .finish()
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
} }
@Test @Test
fun `given changing profile picture is not supported when updating display name then updates upstream user display name and completes personalization`() = runBlockingTest { fun `given changing profile picture is not supported, when updating display name, then updates upstream user display name and completes personalization`() = runBlockingTest {
val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false)) val personalisedInitialState = initialState.copy(personalizationState = PersonalizationState(supportsChangingProfilePicture = false))
viewModel = createViewModel(personalisedInitialState) viewModel = createViewModel(personalisedInitialState)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -159,31 +244,31 @@ class OnboardingViewModelTest {
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
test test
.assertStates(expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState)) .assertStatesChanges(personalisedInitialState, expectedSuccessfulDisplayNameUpdateStates())
.assertEvents(OnboardingViewEvents.OnPersonalizationComplete) .assertEvents(OnboardingViewEvents.OnPersonalizationComplete)
.finish() .finish()
fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME) fakeSession.fakeProfileService.verifyUpdatedName(fakeSession.myUserId, A_DISPLAY_NAME)
} }
@Test @Test
fun `given upstream failure when handling display name update then emits failure event`() = runBlockingTest { fun `given upstream failure, when handling display name update, then emits failure event`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
fakeSession.fakeProfileService.givenSetDisplayNameErrors(AN_ERROR) fakeSession.fakeProfileService.givenSetDisplayNameErrors(AN_ERROR)
viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME)) viewModel.handle(OnboardingAction.UpdateDisplayName(A_DISPLAY_NAME))
test test
.assertStates( .assertStatesChanges(
initialState, initialState,
initialState.copy(asyncDisplayName = Loading()), { copy(asyncDisplayName = Loading()) },
initialState.copy(asyncDisplayName = Fail(AN_ERROR)), { copy(asyncDisplayName = Fail(AN_ERROR)) },
) )
.assertEvents(OnboardingViewEvents.Failure(AN_ERROR)) .assertEvents(OnboardingViewEvents.Failure(AN_ERROR))
.finish() .finish()
} }
@Test @Test
fun `when handling profile picture selected then updates selected picture state`() = runBlockingTest { fun `when handling profile picture selected, then updates selected picture state`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.ProfilePictureSelected(fakeUri.instance)) viewModel.handle(OnboardingAction.ProfilePictureSelected(fakeUri.instance))
@ -198,7 +283,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given a selected picture when handling save selected profile picture then updates upstream avatar and completes personalization`() = runBlockingTest { fun `given a selected picture, when handling save selected profile picture, then updates upstream avatar and completes personalization`() = runBlockingTest {
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME) val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
viewModel = createViewModel(initialStateWithPicture) viewModel = createViewModel(initialStateWithPicture)
val test = viewModel.test(this) val test = viewModel.test(this)
@ -213,7 +298,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given upstream update avatar fails when saving selected profile picture then emits failure event`() = runBlockingTest { fun `given upstream update avatar fails, when saving selected profile picture, then emits failure event`() = runBlockingTest {
fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR) fakeSession.fakeProfileService.givenUpdateAvatarErrors(AN_ERROR)
val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME) val initialStateWithPicture = givenPictureSelected(fakeUri.instance, A_PICTURE_FILENAME)
viewModel = createViewModel(initialStateWithPicture) viewModel = createViewModel(initialStateWithPicture)
@ -228,7 +313,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `given no selected picture when saving selected profile picture then emits failure event`() = runBlockingTest { fun `given no selected picture, when saving selected profile picture, then emits failure event`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.SaveSelectedProfilePicture) viewModel.handle(OnboardingAction.SaveSelectedProfilePicture)
@ -240,7 +325,7 @@ class OnboardingViewModelTest {
} }
@Test @Test
fun `when handling profile picture skipped then completes personalization`() = runBlockingTest { fun `when handling profile skipped, then completes personalization`() = runBlockingTest {
val test = viewModel.test(this) val test = viewModel.test(this)
viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped) viewModel.handle(OnboardingAction.UpdateProfilePictureSkipped)
@ -264,6 +349,7 @@ class OnboardingViewModelTest {
FakeVectorFeatures(), FakeVectorFeatures(),
FakeAnalyticsTracker(), FakeAnalyticsTracker(),
fakeUriFilenameResolver.instance, fakeUriFilenameResolver.instance,
fakeRegisterActionHandler.instance,
FakeVectorOverrides() FakeVectorOverrides()
) )
} }
@ -286,22 +372,42 @@ class OnboardingViewModelTest {
state.copy(asyncProfilePicture = Fail(cause)) state.copy(asyncProfilePicture = Fail(cause))
) )
private fun givenSuccessfullyCreatesAccount() { private fun expectedSuccessfulDisplayNameUpdateStates(): List<OnboardingViewState.() -> OnboardingViewState> {
return listOf(
{ copy(asyncDisplayName = Loading()) },
{ copy(asyncDisplayName = Success(Unit), personalizationState = personalizationState.copy(displayName = A_DISPLAY_NAME)) }
)
}
private fun givenSuccessfulRegistrationForStartAndDummySteps(missingStages: List<Stage>) {
val flowResult = FlowResult(missingStages = missingStages, completedStages = emptyList())
givenRegistrationResultsFor(listOf(
A_LOADABLE_REGISTER_ACTION to RegistrationResult.FlowResponse(flowResult),
RegisterAction.RegisterDummy to RegistrationResult.Success(fakeSession)
))
givenSuccessfullyCreatesAccount(A_HOMESERVER_CAPABILITIES)
}
private fun givenSuccessfullyCreatesAccount(homeServerCapabilities: HomeServerCapabilities) {
fakeSession.fakeHomeServerCapabilitiesService.givenCapabilities(homeServerCapabilities)
fakeActiveSessionHolder.expectSetsActiveSession(fakeSession) fakeActiveSessionHolder.expectSetsActiveSession(fakeSession)
val registrationWizard = FakeRegistrationWizard().also { it.givenSuccessfulDummy(fakeSession) }
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
fakeAuthenticationService.expectReset() fakeAuthenticationService.expectReset()
fakeSession.expectStartsSyncing() fakeSession.expectStartsSyncing()
} }
private fun expectedSuccessfulDisplayNameUpdateStates(personalisedInitialState: OnboardingViewState): List<OnboardingViewState> { private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationResult) {
return listOf( givenRegistrationResultsFor(listOf(action to result))
personalisedInitialState, }
personalisedInitialState.copy(asyncDisplayName = Loading()),
personalisedInitialState.copy( private fun givenRegistrationResultsFor(results: List<Pair<RegisterAction, RegistrationResult>>) {
asyncDisplayName = Success(Unit), fakeAuthenticationService.givenRegistrationStarted(true)
personalizationState = personalisedInitialState.personalizationState.copy(displayName = A_DISPLAY_NAME) val registrationWizard = FakeRegistrationWizard()
) fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
) fakeRegisterActionHandler.givenResultsFor(registrationWizard, results)
} }
} }
private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState(
supportsChangingDisplayName = canChangeDisplayName,
supportsChangingProfilePicture = canChangeAvatar
)

View File

@ -0,0 +1,74 @@
/*
* 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 io.mockk.coVerifyAll
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 {
@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) {
val registrationActionHandler = RegistrationActionHandler()
val fakeRegistrationWizard = FakeRegistrationWizard()
fakeRegistrationWizard.givenSuccessFor(result = A_SESSION, case.expect)
val result = registrationActionHandler.handleRegisterAction(fakeRegistrationWizard, case.action)
coVerifyAll { case.expect(fakeRegistrationWizard) }
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)

View File

@ -55,6 +55,25 @@ class ViewModelTest<S, VE>(
return this return this
} }
fun assertStatesChanges(initial: S, vararg expected: S.() -> S): ViewModelTest<S, VE> {
return assertStatesChanges(initial, expected.toList())
}
/**
* Asserts the expected states are in the same order as the actual state emissions
* Each expected lambda is given the previous expected state, starting with the initial
*/
fun assertStatesChanges(initial: S, expected: List<S.() -> S>): ViewModelTest<S, VE> {
val reducedExpectedStates = expected.fold(mutableListOf(initial)) { acc, curr ->
val next = curr.invoke(acc.last())
acc.add(next)
acc
}
states.assertValues(reducedExpectedStates)
return this
}
fun assertStates(expected: List<S>): ViewModelTest<S, VE> { fun assertStates(expected: List<S>): ViewModelTest<S, VE> {
states.assertValues(expected) states.assertValues(expected)
return this return this

View File

@ -23,10 +23,15 @@ import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
class FakeAuthenticationService : AuthenticationService by mockk() { class FakeAuthenticationService : AuthenticationService by mockk() {
fun givenRegistrationWizard(registrationWizard: RegistrationWizard) { fun givenRegistrationWizard(registrationWizard: RegistrationWizard) {
every { getRegistrationWizard() } returns registrationWizard every { getRegistrationWizard() } returns registrationWizard
} }
fun givenRegistrationStarted(started: Boolean) {
every { isRegistrationStarted } returns started
}
fun expectReset() { fun expectReset() {
coJustRun { reset() } coJustRun { reset() }
} }

View File

@ -0,0 +1,36 @@
/*
* 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<RegistrationActionHandler>()
fun givenResultsFor(wizard: RegistrationWizard, result: List<Pair<RegisterAction, RegistrationResult>>) {
coEvery { instance.handleRegisterAction(wizard, any()) } answers { call ->
val actionArg = call.invocation.args[1] as RegisterAction
result.first { it.first == actionArg }.second
}
}
}

View File

@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
class FakeRegistrationWizard : RegistrationWizard by mockk() { class FakeRegistrationWizard : RegistrationWizard by mockk(relaxed = false) {
fun givenSuccessfulDummy(session: Session) { fun givenSuccessFor(result: Session, expect: suspend RegistrationWizard.() -> RegistrationResult) {
coEvery { dummy() } returns RegistrationResult.Success(session) coEvery { expect(this@FakeRegistrationWizard) } returns RegistrationResult.Success(result)
} }
} }

View File

@ -23,5 +23,5 @@ class FakeVectorFeatures : VectorFeatures {
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
override fun isOnboardingSplashCarouselEnabled() = true override fun isOnboardingSplashCarouselEnabled() = true
override fun isOnboardingUseCaseEnabled() = true override fun isOnboardingUseCaseEnabled() = true
override fun isOnboardingPersonalizeEnabled() = false override fun isOnboardingPersonalizeEnabled() = true
} }

View File

@ -0,0 +1,40 @@
/*
* 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.fixtures
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities
fun aHomeServerCapabilities(
canChangePassword: Boolean = true,
canChangeDisplayName: Boolean = true,
canChangeAvatar: Boolean = true,
canChange3pid: Boolean = true,
maxUploadFileSize: Long = 100L,
lastVersionIdentityServerSupported: Boolean = false,
defaultIdentityServerUrl: String? = null,
roomVersions: RoomVersionCapabilities? = null
) = HomeServerCapabilities(
canChangePassword,
canChangeDisplayName,
canChangeAvatar,
canChange3pid,
maxUploadFileSize,
lastVersionIdentityServerSupported,
defaultIdentityServerUrl,
roomVersions
)