From 355332d604bbc9938f34f159420ef18db80eeb26 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 7 Jul 2026 16:56:37 -0400 Subject: [PATCH] Handle additional error cases in regV5. --- .../phonenumber/PhoneNumberEntryScreen.kt | 6 +- .../screens/pincreation/PinCreationScreen.kt | 28 ++++++++ .../pincreation/PinCreationScreenEvents.kt | 1 + .../screens/pincreation/PinCreationState.kt | 11 ++- .../pincreation/PinCreationViewModel.kt | 13 ++-- .../PinEntryForRegistrationLockViewModel.kt | 22 ++++-- .../VerificationCodeViewModel.kt | 20 +++--- .../src/main/res/values/strings.xml | 8 +++ .../pincreation/PinCreationViewModelTest.kt | 68 +++++++++++++++++++ ...inEntryForRegistrationLockViewModelTest.kt | 59 ++++++++++++++++ .../VerificationCodeViewModelTest.kt | 12 ++-- 11 files changed, 215 insertions(+), 33 deletions(-) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt index 1c4b4c6d53..85bae65a03 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt @@ -185,7 +185,11 @@ fun PhoneNumberScreen( onEvent(PhoneNumberEntryScreenEvents.ConsumeOneTimeEvent) when (state.oneTimeEvent) { OneTimeEvent.NetworkError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__network_error) - is OneTimeEvent.RateLimited -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.oneTimeEvent.retryAfter.toString()) + is OneTimeEvent.RateLimited -> simpleErrorMessage = if (state.oneTimeEvent.retryAfter.isPositive()) { + resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.oneTimeEvent.retryAfter.toString()) + } else { + resources.getString(R.string.VerificationCodeScreen__too_many_attempts) + } OneTimeEvent.UnknownError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__an_unexpected_error_occurred) OneTimeEvent.CouldNotRequestCodeWithSelectedTransport -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method) OneTimeEvent.UnableToSendSms -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__unable_to_send_sms) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt index 0d6d4725c5..582d51b362 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -92,11 +93,38 @@ fun PinCreationScreen( ) { val activePin = remember { mutableStateOf("") } val canSubmitPin = activePin.value.length >= 4 + val resources = LocalResources.current + var errorMessage: String? by remember { mutableStateOf(null) } BackHandler(enabled = state.isConfirmEnabled) { onEvent(PinCreationScreenEvents.BackToPinEntry) } + LaunchedEffect(state.oneTimeEvent) { + val event = state.oneTimeEvent ?: return@LaunchedEffect + onEvent(PinCreationScreenEvents.ConsumeOneTimeEvent) + errorMessage = when (event) { + is PinCreationState.OneTimeEvent.ServiceError -> { + resources.getString(R.string.PinCreationScreen__service_error) + } + is PinCreationState.OneTimeEvent.NetworkError -> { + if (event.retryAfter != null) { + resources.getString(R.string.PinCreationScreen__network_error_try_again_in_s, event.retryAfter.toString()) + } else { + resources.getString(R.string.PinCreationScreen__network_error) + } + } + } + } + + errorMessage?.let { message -> + Dialogs.SimpleMessageDialog( + message = message, + dismiss = stringResource(android.R.string.ok), + onDismiss = { errorMessage = null } + ) + } + when (val params = RegistrationScaffold.rememberLayoutParams()) { is RegistrationScaffold.Params.OnePane -> OnePaneLayout( params = params, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt index 49b36472ad..5f5a6b029e 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt @@ -13,4 +13,5 @@ sealed class PinCreationScreenEvents { data object LearnMore : PinCreationScreenEvents() data object OptOut : PinCreationScreenEvents() data object BackToPinEntry : PinCreationScreenEvents() + data object ConsumeOneTimeEvent : PinCreationScreenEvents() } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt index 238f7949a8..a817211fe8 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt @@ -7,6 +7,7 @@ package org.signal.registration.screens.pincreation import org.signal.core.models.AccountEntropyPool import org.signal.core.util.censor +import kotlin.time.Duration data class PinCreationState( val isAlphanumericKeyboard: Boolean = false, @@ -14,9 +15,15 @@ data class PinCreationState( val pinMismatch: Boolean = false, val loading: Boolean = false, val firstPin: String? = null, - val accountEntropyPool: AccountEntropyPool? = null + val accountEntropyPool: AccountEntropyPool? = null, + val oneTimeEvent: OneTimeEvent? = null ) { override fun toString(): String { - return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()})" + return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()}, oneTimeEvent=$oneTimeEvent)" + } + + sealed interface OneTimeEvent { + data object ServiceError : OneTimeEvent + data class NetworkError(val retryAfter: Duration?) : OneTimeEvent } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt index e6a44b3218..fd95892bdd 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt @@ -23,6 +23,7 @@ import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RestoreDecision import org.signal.registration.screens.EventDrivenViewModel +import kotlin.time.toKotlinDuration /** * ViewModel for the PIN creation screen. @@ -91,6 +92,9 @@ class PinCreationViewModel( _state.value = state.copy(isConfirmEnabled = false) applyOptOut() } + is PinCreationScreenEvents.ConsumeOneTimeEvent -> { + _state.value = state.copy(oneTimeEvent = null) + } } } @@ -129,8 +133,7 @@ class PinCreationViewModel( when (val error = result.error) { is NetworkController.BackupMasterKeyError.EnclaveNotFound -> { Log.w(TAG, "[PinSubmitted] SVR enclave not found.") - // TODO [registration] - Report to UI and indicate to library user that pin could not be created - throw NotImplementedError("Report to UI and indicate to library user that pin could not be created") + state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError) } is NetworkController.BackupMasterKeyError.NotRegistered -> { @@ -143,14 +146,12 @@ class PinCreationViewModel( is RequestResult.RetryableNetworkError -> { Log.w(TAG, "[PinSubmitted] Network error when backing up master key.", result.networkError) - // TODO [registration] - Report to UI and indicate to library user that pin could not be created - throw NotImplementedError("Report to UI and indicate to library user that pin could not be created") + state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.NetworkError(result.retryAfter?.toKotlinDuration())) } is RequestResult.ApplicationError -> { Log.w(TAG, "[PinSubmitted] Application error when backing up master key.", result.cause) - // TODO [registration] - Report to UI and indicate to library user that pin could not be created - throw NotImplementedError("Report to UI and indicate to library user that pin could not be created") + state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError) } } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt index b2fd928ea7..49e8077102 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt @@ -23,6 +23,7 @@ import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RegistrationRoute import org.signal.registration.screens.EventDrivenViewModel +import org.signal.registration.screens.util.navigateBack import org.signal.registration.screens.util.navigateTo /** @@ -90,7 +91,13 @@ class PinEntryForRegistrationLockViewModel( return when (val error = restoreResult.error) { is NetworkController.RestoreMasterKeyError.WrongPin -> { Log.w(TAG, "[PinEntered] Wrong PIN. Tries remaining: ${error.triesRemaining}") - state.copy(loading = false, triesRemaining = error.triesRemaining) + if (error.triesRemaining <= 0) { + Log.w(TAG, "[PinEntered] Out of PIN attempts. Account is locked.") + parentEventEmitter.navigateTo(RegistrationRoute.AccountLocked(timeRemainingMs = timeRemaining)) + state + } else { + state.copy(loading = false, triesRemaining = error.triesRemaining) + } } is NetworkController.RestoreMasterKeyError.NoDataFound -> { Log.w(TAG, "[PinEntered] No SVR data found. Account is locked.") @@ -144,9 +151,9 @@ class PinEntryForRegistrationLockViewModel( is RequestResult.NonSuccess -> { when (val error = registerResult.error) { is NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified -> { - Log.w(TAG, "[PinEntered] Session not found or verified: ${error.message}") - // TODO [registration] - Handle session not found or verified. - throw NotImplementedError("Handle session not found or verified") + Log.w(TAG, "[PinEntered] Session not found or verified: ${error.message}. Resetting.") + parentEventEmitter(RegistrationFlowEvent.ResetState) + state } is NetworkController.RegisterAccountError.RegistrationLock -> { Log.w(TAG, "[PinEntered] Still getting registration lock error after providing token. This shouldn't happen. Resetting state.") @@ -166,9 +173,10 @@ class PinEntryForRegistrationLockViewModel( state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError) } is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> { - Log.w(TAG, "[PinEntered] Registration recovery password incorrect: ${error.message}") - // TODO [registration] - Handle incorrect password - throw NotImplementedError("Handle incorrect password") + Log.w(TAG, "[PinEntered] Registration recovery password incorrect: ${error.message}. Marking recovery password invalid and navigating back.") + parentEventEmitter(RegistrationFlowEvent.RecoveryPasswordInvalid) + parentEventEmitter.navigateBack() + state } } } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt index 5ab7cf7e81..1a60522f6f 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt @@ -307,9 +307,8 @@ class VerificationCodeViewModel( return state.copy(oneTimeEvent = OneTimeEvent.IncorrectVerificationCode, incorrectCodeAttempts = newAttempts, digits = VerificationCodeState.emptyDigits(), focusedDigitIndex = 0) } is NetworkController.SubmitVerificationCodeError.SessionNotFound -> { - Log.w(TAG, "[SubmitCode] Session not found: ${error.message}") - // TODO don't start over, go back to phone number entry - parentEventEmitter(RegistrationFlowEvent.ResetState) + Log.w(TAG, "[SubmitCode] Session not found: ${error.message}. Navigating back to phone number entry.") + parentEventEmitter.navigateBack() return state } is NetworkController.SubmitVerificationCodeError.SessionAlreadyVerifiedOrNoCodeRequested -> { @@ -365,8 +364,9 @@ class VerificationCodeViewModel( is RequestResult.NonSuccess -> { when (val error = registerResult.error) { is NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified -> { - // TODO [registration] Handle session not found or not verified case. - throw NotImplementedError("Handle session not found or not verified case.") + Log.w(TAG, "[Register] Session not found or not verified: ${error.message}. Navigating back to phone number entry.") + parentEventEmitter.navigateBack() + state } is NetworkController.RegisterAccountError.DeviceTransferPossible -> { error("[Register] Got told a device transfer is possible. We should never get into this state. Resetting.") @@ -454,9 +454,8 @@ class VerificationCodeViewModel( ) } is NetworkController.RequestVerificationCodeError.InvalidSessionId -> { - Log.w(TAG, "[RequestCode][$transport] Invalid session ID: ${error.message}") - // TODO don't start over, go back to phone number entry - parentEventEmitter(RegistrationFlowEvent.ResetState) + Log.w(TAG, "[RequestCode][$transport] Invalid session ID: ${error.message}. Navigating back to phone number entry.") + parentEventEmitter.navigateBack() state } is NetworkController.RequestVerificationCodeError.MissingRequestInformationOrAlreadyVerified -> { @@ -469,9 +468,8 @@ class VerificationCodeViewModel( ) } is NetworkController.RequestVerificationCodeError.SessionNotFound -> { - Log.w(TAG, "[RequestCode][$transport] Session not found: ${error.message}") - // TODO don't start over, go back to phone number entry - parentEventEmitter(RegistrationFlowEvent.ResetState) + Log.w(TAG, "[RequestCode][$transport] Session not found: ${error.message}. Navigating back to phone number entry.") + parentEventEmitter.navigateBack() state } is NetworkController.RequestVerificationCodeError.ThirdPartyServiceError -> { diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml index 18673a7a8b..1d5bc3e874 100644 --- a/feature/registration/src/main/res/values/strings.xml +++ b/feature/registration/src/main/res/values/strings.xml @@ -91,6 +91,8 @@ Unable to connect. Please check your network and try again. Too many attempts. Try again in %s. + + Too many attempts. Please try again later. An unexpected error occurred. Please try again. @@ -361,6 +363,12 @@ Disable PIN Cancel + + There was an issue connecting to the service. + + Encountered a network error. + + Encountered a network error. You can try again in %1$s. Registration Lock diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt index 14e9aa28fa..19bed9d14d 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt @@ -9,8 +9,11 @@ import assertk.assertThat import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue +import assertk.assertions.prop import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk @@ -34,6 +37,9 @@ import org.signal.registration.RegistrationFlowEvent import org.signal.registration.RegistrationFlowState import org.signal.registration.RegistrationRepository import org.signal.registration.RestoreDecision +import java.io.IOException +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration @OptIn(ExperimentalCoroutinesApi::class) class PinCreationViewModelTest { @@ -167,6 +173,68 @@ class PinCreationViewModelTest { assertThat(emittedParentEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState) } + @Test + fun `PinSubmitted with EnclaveNotFound error surfaces a service error and stops loading`() = runTest(testDispatcher) { + val states = collectStates() + val aep = AccountEntropyPool.generate() + val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456") + + coEvery { mockRepository.setNewlyCreatedPin(any(), any(), any()) } returns + RequestResult.NonSuccess(NetworkController.BackupMasterKeyError.EnclaveNotFound) + + viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456")) + + assertThat(emittedParentEvents).hasSize(0) + assertThat(states.last().oneTimeEvent).isEqualTo(PinCreationState.OneTimeEvent.ServiceError) + assertThat(states.last().loading).isFalse() + } + + @Test + fun `PinSubmitted with application error surfaces a service error and stops loading`() = runTest(testDispatcher) { + val states = collectStates() + val aep = AccountEntropyPool.generate() + val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456") + + coEvery { mockRepository.setNewlyCreatedPin(any(), any(), any()) } returns + RequestResult.ApplicationError(RuntimeException("Unexpected")) + + viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456")) + + assertThat(emittedParentEvents).hasSize(0) + assertThat(states.last().oneTimeEvent).isEqualTo(PinCreationState.OneTimeEvent.ServiceError) + assertThat(states.last().loading).isFalse() + } + + @Test + fun `PinSubmitted with retryable network error surfaces a network error with retryAfter`() = runTest(testDispatcher) { + val states = collectStates() + val aep = AccountEntropyPool.generate() + val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456") + val retryAfter = 30.seconds + + coEvery { mockRepository.setNewlyCreatedPin(any(), any(), any()) } returns + RequestResult.RetryableNetworkError(IOException("Network error"), retryAfter.toJavaDuration()) + + viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456")) + + assertThat(emittedParentEvents).hasSize(0) + assertThat(states.last().oneTimeEvent).isNotNull() + .isInstanceOf() + .prop(PinCreationState.OneTimeEvent.NetworkError::retryAfter) + .isEqualTo(retryAfter) + assertThat(states.last().loading).isFalse() + } + + @Test + fun `ConsumeOneTimeEvent clears the one-time event`() = runTest(testDispatcher) { + val states = collectStates() + val stateWithEvent = PinCreationState(oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError) + + viewModel.applyEvent(stateWithEvent, PinCreationScreenEvents.ConsumeOneTimeEvent) + + assertThat(states.last().oneTimeEvent).isNull() + } + // ==================== OptOut Tests ==================== @Test diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt index a2375182a9..9c86d0c1c7 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt @@ -106,6 +106,26 @@ class PinEntryForRegistrationLockViewModelTest { assertThat(emittedStates.last().loading).isEqualTo(false) } + @Test + fun `PinEntered with wrong PIN and no tries remaining navigates to AccountLocked`() = runTest { + val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock) + + coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns + RequestResult.NonSuccess( + NetworkController.RestoreMasterKeyError.WrongPin(0) + ) + + viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("wrong-pin"), parentEventEmitter, stateEmitter) + + assertThat(emittedParentEvents).hasSize(1) + assertThat(emittedParentEvents.first()) + .isInstanceOf() + .prop(RegistrationFlowEvent.NavigateToScreen::route) + .isInstanceOf() + .prop(RegistrationRoute.AccountLocked::timeRemainingMs) + .isEqualTo(testTimeRemaining) + } + @Test fun `PinEntered with no SVR data navigates to AccountLocked`() = runTest { val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock) @@ -197,6 +217,45 @@ class PinEntryForRegistrationLockViewModelTest { // ==================== Registration Error Tests ==================== + @Test + fun `PinEntered with session not found during registration emits ResetState`() = runTest { + val masterKey = mockk(relaxed = true) + val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock) + + coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns + RequestResult.Success(NetworkController.MasterKeyResponse(masterKey)) + coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns + RequestResult.NonSuccess( + NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified("Session not found") + ) + + viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) + + assertThat(emittedParentEvents).hasSize(2) + assertThat(emittedParentEvents[0]).isInstanceOf() + assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.ResetState) + } + + @Test + fun `PinEntered with recovery password incorrect during registration marks it invalid and navigates back`() = runTest { + val masterKey = mockk(relaxed = true) + val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock) + + coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns + RequestResult.Success(NetworkController.MasterKeyResponse(masterKey)) + coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns + RequestResult.NonSuccess( + NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect("Wrong password") + ) + + viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter) + + assertThat(emittedParentEvents).hasSize(3) + assertThat(emittedParentEvents[0]).isInstanceOf() + assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.RecoveryPasswordInvalid) + assertThat(emittedParentEvents[2]).isEqualTo(RegistrationFlowEvent.NavigateBack) + } + @Test fun `PinEntered with registration lock error during registration emits ResetState`() = runTest { val masterKey = mockk(relaxed = true) diff --git a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt index 5a22e381da..293403a8a3 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt @@ -579,7 +579,7 @@ class VerificationCodeViewModelTest { } @Test - fun `CodeEntered with session not found emits ResetState`() = runTest { + fun `CodeEntered with session not found navigates back to phone number entry`() = runTest { val sessionMetadata = createSessionMetadata() val initialState = VerificationCodeState( sessionMetadata = sessionMetadata, @@ -594,7 +594,7 @@ class VerificationCodeViewModelTest { viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CodeEntered("123456"), stateEmitter) assertThat(emittedEvents).hasSize(1) - assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState) + assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.NavigateBack) } @Test @@ -953,7 +953,7 @@ class VerificationCodeViewModelTest { } @Test - fun `ResendSms with InvalidSessionId emits ResetState`() = runTest { + fun `ResendSms with InvalidSessionId navigates back to phone number entry`() = runTest { val sessionMetadata = createSessionMetadata() val initialState = VerificationCodeState(sessionMetadata = sessionMetadata) @@ -965,11 +965,11 @@ class VerificationCodeViewModelTest { viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter) assertThat(emittedEvents).hasSize(1) - assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState) + assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.NavigateBack) } @Test - fun `ResendSms with SessionNotFound emits ResetState`() = runTest { + fun `ResendSms with SessionNotFound navigates back to phone number entry`() = runTest { val sessionMetadata = createSessionMetadata() val initialState = VerificationCodeState(sessionMetadata = sessionMetadata) @@ -981,7 +981,7 @@ class VerificationCodeViewModelTest { viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter) assertThat(emittedEvents).hasSize(1) - assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState) + assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.NavigateBack) } @Test