extracting registration steps to separate handler to make testing the flow simpler

This commit is contained in:
Adam Brown 2022-03-02 16:49:23 +00:00
parent 4225f62120
commit 3fa415007c
12 changed files with 328 additions and 184 deletions

View File

@ -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

View File

@ -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) {
@ -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))
}
setState {
copy(
asyncRegistration = Uninitialized
)
}
}
}
private fun handleSendAgainThreePid() {
if (action.hasLoadingState()) {
setState { copy(asyncRegistration = Loading()) }
currentJob = viewModelScope.launch {
try {
registrationWizard?.sendAgainThreePid()
} catch (failure: Throwable) {
_viewEvents.post(OnboardingViewEvents.Failure(failure))
}
setState {
copy(
asyncRegistration = Uninitialized
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))
}
}
)
}
}
}
private fun handleAcceptTerms() {
currentJob = executeRegistrationStep {
it.acceptTerms()
}
}
private fun handleRegisterDummy() {
currentJob = executeRegistrationStep {
it.dummy()
setState { copy(asyncRegistration = Uninitialized) }
}
}
private fun handleRegisterWith(action: OnboardingAction.LoginOrRegister) {
reAuthHelper.data = action.password
currentJob = executeRegistrationStep {
it.createAccount(
handleRegisterAction(RegisterAction.CreateAccount(
action.username,
action.password,
action.initialDeviceName
)
}
}
private fun handleCaptchaDone(action: OnboardingAction.CaptchaDone) {
currentJob = executeRegistrationStep {
it.performReCaptcha(action.captchaResponse)
}
))
}
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 ->

View File

@ -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
}

View File

@ -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

View File

@ -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)))
}
}
}

View File

@ -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)
}

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.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) {

View File

@ -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<OnboardingViewState> {
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<Pair<RegisterAction, RegistrationResult>>) {
fakeAuthenticationService.givenRegistrationStarted(true)
val registrationWizard = FakeRegistrationWizard()
fakeAuthenticationService.givenRegistrationWizard(registrationWizard)
fakeRegisterActionHandler.givenResultsFor(registrationWizard, results)
}
}
private fun HomeServerCapabilities.toPersonalisationState() = PersonalizationState(
supportsChangingDisplayName = canChangeDisplayName,
supportsChangingProfilePicture = canChangeAvatar
)

View File

@ -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)

View File

@ -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() }
}

View File

@ -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<RegistrationActionHandler>()
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<Pair<RegisterAction, RegistrationResult>>) {
coEvery { instance.handleRegisterAction(wizard, any()) } answers {
val actionArg = it.invocation.args[1] as RegisterAction
result.first { it.first == actionArg }.second
}
}
}

View File

@ -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)
}
}