Skip to content

Commit

Permalink
Merge pull request #5408 from vector-im/feature/adm/onboarding-tests
Browse files Browse the repository at this point in the history
FTUE - Onboarding registration steps unit tests
  • Loading branch information
ouchadam authored Mar 18, 2022
2 parents 4761f77 + ce2c309 commit ea9c9ae
Show file tree
Hide file tree
Showing 16 changed files with 492 additions and 241 deletions.
1 change: 1 addition & 0 deletions changelog.d/5408.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved onboarding registration unit test coverage
Original file line number Diff line number Diff line change
Expand Up @@ -22,63 +22,49 @@ 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 class OnboardingAction : VectorViewModelAction {
data class OnGetStarted(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 UpdateHomeServer(val homeServerUrl: String) : OnboardingAction()
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction()
object ResetUseCase : OnboardingAction()
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction()
data class LoginWithToken(val loginToken: String) : OnboardingAction()
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction()
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction()
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction()
object ResetPasswordMailConfirmed : OnboardingAction()
sealed interface OnboardingAction : VectorViewModelAction {
data class OnGetStarted(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 UpdateHomeServer(val homeServerUrl: String) : OnboardingAction
data class UpdateUseCase(val useCase: FtueUseCase) : OnboardingAction
object ResetUseCase : OnboardingAction
data class UpdateSignMode(val signMode: SignMode) : OnboardingAction
data class LoginWithToken(val loginToken: String) : OnboardingAction
data class WebLoginSuccess(val credentials: Credentials) : OnboardingAction
data class InitWith(val loginConfig: LoginConfig?) : OnboardingAction
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
object ResetPasswordMailConfirmed : OnboardingAction

// Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : 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 LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
object StopEmailValidationCheck : OnboardingAction

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()
sealed interface ResetAction : OnboardingAction

object ResetHomeServerType : ResetAction()
object ResetHomeServerUrl : ResetAction()
object ResetSignMode : ResetAction()
object ResetLogin : ResetAction()
object ResetResetPassword : ResetAction()
object ResetHomeServerType : ResetAction
object ResetHomeServerUrl : ResetAction
object ResetSignMode : ResetAction
object ResetLogin : ResetAction
object ResetResetPassword : ResetAction

// 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()
data class UpdateDisplayName(val displayName: String) : OnboardingAction()
object UpdateDisplayNameSkipped : OnboardingAction()
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction()
object SaveSelectedProfilePicture : OnboardingAction()
object UpdateProfilePictureSkipped : OnboardingAction()
object PersonalizeProfile : OnboardingAction
data class UpdateDisplayName(val displayName: String) : OnboardingAction
object UpdateDisplayNameSkipped : OnboardingAction
data class ProfilePictureSelected(val uri: Uri) : OnboardingAction
object SaveSelectedProfilePicture : OnboardingAction
object UpdateProfilePictureSkipped : OnboardingAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -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 -> cancelWaitForEmailValidation()
}.exhaustive
}

Expand Down Expand Up @@ -266,131 +268,41 @@ 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))
}
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()
if (action.hasLoadingState()) {
setState { copy(asyncRegistration = Loading()) }
}
runCatching { registrationActionHandler.handleRegisterAction(registrationWizard, action) }
.fold(
onSuccess = {
when {
action.ignoresResult() -> {
// do nothing
}
else -> 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) {
Expand Down Expand Up @@ -461,7 +373,7 @@ class OnboardingViewModel @AssistedInject constructor(
}

when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow()
SignMode.SignUp -> handleRegisterAction(RegisterAction.StartRegistration)
SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit
Expand Down Expand Up @@ -499,7 +411,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)))
}
Expand Down Expand Up @@ -730,12 +642,6 @@ class OnboardingViewModel @AssistedInject constructor(
}
}

private fun startRegistrationFlow() {
currentJob = executeRegistrationStep {
it.getRegistrationFlow()
}
}

private fun startAuthenticationFlow() {
// Ensure Wizard is ready
loginWizard
Expand All @@ -745,15 +651,18 @@ 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
_viewEvents.post(OnboardingViewEvents.RegistrationFlowResult(flowResult, isRegistrationStarted))
}
}

private fun handleRegisterDummy() {
handleRegisterAction(RegisterAction.RegisterDummy)
}

private suspend fun onSessionCreated(session: Session, isAccountCreated: Boolean) {
val state = awaitState()
state.useCase?.let { useCase ->
Expand Down Expand Up @@ -1006,6 +915,10 @@ class OnboardingViewModel @AssistedInject constructor(
private fun completePersonalization() {
_viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete)
}

private fun cancelWaitForEmailValidation() {
currentJob = null
}
}

private fun LoginMode.supportsSignModeScreen(): Boolean {
Expand Down
Loading

0 comments on commit ea9c9ae

Please sign in to comment.