mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
Implement initialization and logic for MigrateToMyItemsViewModel
Update the `MigrateToMyItemsViewModel` to replace placeholder data with actual repository logic, state persistence, and dialog management.
The view model now initializes by fetching the active organization details related to the personal ownership policy. It handles the "Continue" action by displaying a loading state (simulated for now) and processing the result to either navigate to the vault or show an error. Additionally, the help link now points to the correct documentation URL.
- Inject `AuthRepository`, `PolicyManager`, and `SavedStateHandle` into `MigrateToMyItemsViewModel`.
- Initialize `MigrateToMyItemsState` by resolving the `organizationId` via `PolicyManager` and retrieving the corresponding organization name from `AuthRepository` user state.
- Implement state persistence using `SavedStateHandle`.
- Refactor `MigrateToMyItemsState` to include a nested `ViewState` and a `DialogState` sealed class (supporting `Loading` and `Error` states).
- Update `handleContinueClicked` to set the dialog state to `Loading` and initiate a coroutine that currently simulates migration delay.
- Add `Internal` action handling to process migration results:
- On success: Clears the dialog and triggers navigation to the vault.
- On failure: Updates the dialog state to show an error message.
- Update `handleHelpLinkClicked` to launch the specific "transfer-ownership" help URI.
- Add new string resources `migrating_items_to_x` and `failed_to_migrate_items_to_x` for the new dialog states.
This commit is contained in:
parent
7fa3f50f51
commit
e059c3a499
@ -13,7 +13,6 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@ -30,12 +29,15 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.handler.rememberMigrateToMyItemsHandler
|
||||
|
||||
/**
|
||||
* Top level screen component for the MigrateToMyItems screen.
|
||||
@ -48,6 +50,7 @@ fun MigrateToMyItemsScreen(
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handlers = rememberMigrateToMyItemsHandler(viewModel)
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
@ -57,22 +60,17 @@ fun MigrateToMyItemsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val onContinueClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked) }
|
||||
}
|
||||
val onDeclineClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MigrateToMyItemsAction.DeclineAndLeaveClicked) }
|
||||
}
|
||||
val onHelpClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(MigrateToMyItemsAction.HelpLinkClicked) }
|
||||
}
|
||||
MigrateToMyItemsDialogs(
|
||||
dialog = state.dialog,
|
||||
onDismissRequest = handlers.onDismissDialog,
|
||||
)
|
||||
|
||||
BitwardenScaffold {
|
||||
MigrateToMyItemsContent(
|
||||
organizationName = state.organizationName,
|
||||
onContinueClick = onContinueClick,
|
||||
onDeclineClick = onDeclineClick,
|
||||
onHelpClick = onHelpClick,
|
||||
viewState = state.viewState,
|
||||
onContinueClick = handlers.onContinueClick,
|
||||
onDeclineClick = handlers.onDeclineClick,
|
||||
onHelpClick = handlers.onHelpClick,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
@ -80,9 +78,31 @@ fun MigrateToMyItemsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateToMyItemsDialogs(
|
||||
dialog: MigrateToMyItemsState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
when (dialog) {
|
||||
is MigrateToMyItemsState.DialogState.Error -> {
|
||||
BitwardenBasicDialog(
|
||||
title = dialog.title(),
|
||||
message = dialog.message(),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
is MigrateToMyItemsState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(text = dialog.message())
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateToMyItemsContent(
|
||||
organizationName: String,
|
||||
viewState: MigrateToMyItemsState.ViewState,
|
||||
onContinueClick: () -> Unit,
|
||||
onDeclineClick: () -> Unit,
|
||||
onHelpClick: () -> Unit,
|
||||
@ -103,7 +123,7 @@ private fun MigrateToMyItemsContent(
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MigrateToMyItemsTextContent(organizationName = organizationName)
|
||||
MigrateToMyItemsTextContent(organizationName = viewState.organizationName)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
MigrateToMyItemsActions(
|
||||
onContinueClick = onContinueClick,
|
||||
@ -188,7 +208,7 @@ private fun MigrateToMyItemsScreen_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenScaffold {
|
||||
MigrateToMyItemsContent(
|
||||
organizationName = "Test Organization",
|
||||
viewState = MigrateToMyItemsState.ViewState(organizationName = "Bitwarden"),
|
||||
onContinueClick = {},
|
||||
onDeclineClick = {},
|
||||
onHelpClick = {},
|
||||
|
||||
@ -1,31 +1,93 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* View model for the [MigrateToMyItemsScreen].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class MigrateToMyItemsViewModel @Inject constructor() :
|
||||
BaseViewModel<MigrateToMyItemsState, MigrateToMyItemsEvent, MigrateToMyItemsAction>(
|
||||
initialState = MigrateToMyItemsState(
|
||||
// TODO: Get from repository or manager (PM-28468).
|
||||
organizationName = "TODO",
|
||||
),
|
||||
) {
|
||||
class MigrateToMyItemsViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val policyManager: PolicyManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<MigrateToMyItemsState, MigrateToMyItemsEvent, MigrateToMyItemsAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val organizationId = requireNotNull(
|
||||
policyManager.getPersonalOwnershipPolicyOrganizationId(),
|
||||
)
|
||||
val organization = requireNotNull(
|
||||
authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.organizations
|
||||
?.firstOrNull { it.id == organizationId },
|
||||
)
|
||||
|
||||
MigrateToMyItemsState(
|
||||
organizationId = organizationId,
|
||||
viewState = MigrateToMyItemsState.ViewState(
|
||||
organizationName = organization.name.orEmpty(),
|
||||
),
|
||||
dialog = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
init {
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: MigrateToMyItemsAction) {
|
||||
when (action) {
|
||||
MigrateToMyItemsAction.ContinueClicked -> handleContinueClicked()
|
||||
MigrateToMyItemsAction.DeclineAndLeaveClicked -> handleDeclineAndLeaveClicked()
|
||||
MigrateToMyItemsAction.HelpLinkClicked -> handleHelpLinkClicked()
|
||||
MigrateToMyItemsAction.DismissDialogClicked -> handleDismissDialogClicked()
|
||||
is MigrateToMyItemsAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueClicked() {
|
||||
sendEvent(MigrateToMyItemsEvent.NavigateToVault)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MigrateToMyItemsState.DialogState.Loading(
|
||||
message = BitwardenString.migrating_items_to_x.asText(
|
||||
it.viewState.organizationName,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
// TODO: Replace `delay` with actual migration (PM-28444).
|
||||
delay(timeMillis = 100L)
|
||||
trySendAction(
|
||||
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
|
||||
success = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeclineAndLeaveClicked() {
|
||||
@ -33,17 +95,87 @@ class MigrateToMyItemsViewModel @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun handleHelpLinkClicked() {
|
||||
// TODO: Update URL when available.
|
||||
sendEvent(MigrateToMyItemsEvent.LaunchUri("TODO_HELP_URL"))
|
||||
sendEvent(
|
||||
MigrateToMyItemsEvent.LaunchUri(
|
||||
uri = "https://bitwarden.com/help/transfer-ownership/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleDismissDialogClicked() {
|
||||
clearDialog()
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: MigrateToMyItemsAction.Internal) {
|
||||
when (action) {
|
||||
is MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived -> {
|
||||
handleMigrateToMyItemsResultReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMigrateToMyItemsResultReceived(
|
||||
action: MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived,
|
||||
) {
|
||||
if (action.success) {
|
||||
clearDialog()
|
||||
sendEvent(MigrateToMyItemsEvent.NavigateToVault)
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = MigrateToMyItemsState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.failed_to_migrate_items_to_x.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDialog() {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models the state for the [MigrateToMyItemsScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data class MigrateToMyItemsState(
|
||||
val organizationName: String,
|
||||
)
|
||||
val viewState: ViewState,
|
||||
val dialog: DialogState?,
|
||||
// Internal view model properties
|
||||
private val organizationId: String,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Models the view state for the [MigrateToMyItemsScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data class ViewState(
|
||||
val organizationName: String,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Models the dialog state for the [MigrateToMyItemsScreen].
|
||||
*/
|
||||
sealed class DialogState : Parcelable {
|
||||
|
||||
/**
|
||||
* Displays a loading dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Loading(val message: Text) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays an error dialog.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Error(
|
||||
val title: Text,
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models the events that can be sent from the [MigrateToMyItemsViewModel].
|
||||
@ -83,4 +215,23 @@ sealed class MigrateToMyItemsAction {
|
||||
* User clicked the "Why am I seeing this?" help link.
|
||||
*/
|
||||
data object HelpLinkClicked : MigrateToMyItemsAction()
|
||||
|
||||
/**
|
||||
* User dismissed the dialog.
|
||||
*/
|
||||
data object DismissDialogClicked : MigrateToMyItemsAction()
|
||||
|
||||
/**
|
||||
* Models internal actions that the [MigrateToMyItemsViewModel] itself may send.
|
||||
*/
|
||||
sealed class Internal : MigrateToMyItemsAction() {
|
||||
|
||||
/**
|
||||
* The result of the migration has been received.
|
||||
*/
|
||||
data class MigrateToMyItemsResultReceived(
|
||||
// TODO: Replace `success` with actual migration result (PM-28444).
|
||||
val success: Boolean,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.handler
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.MigrateToMyItemsViewModel
|
||||
|
||||
/**
|
||||
* Action handlers for the [MigrateToMyItemsScreen].
|
||||
*/
|
||||
class MigrateToMyItemsHandler(
|
||||
val onContinueClick: () -> Unit,
|
||||
val onDeclineClick: () -> Unit,
|
||||
val onHelpClick: () -> Unit,
|
||||
val onDismissDialog: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates an instance of [MigrateToMyItemsHandler] using the provided
|
||||
* [MigrateToMyItemsViewModel].
|
||||
*/
|
||||
fun create(viewModel: MigrateToMyItemsViewModel) = MigrateToMyItemsHandler(
|
||||
onContinueClick = {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
|
||||
},
|
||||
onDeclineClick = {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.DeclineAndLeaveClicked)
|
||||
},
|
||||
onHelpClick = {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.HelpLinkClicked)
|
||||
},
|
||||
onDismissDialog = {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.DismissDialogClicked)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remember a [MigrateToMyItemsHandler] instance in a [Composable] scope.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberMigrateToMyItemsHandler(viewModel: MigrateToMyItemsViewModel): MigrateToMyItemsHandler =
|
||||
MigrateToMyItemsHandler.create(viewModel)
|
||||
@ -3,10 +3,16 @@ package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Before
|
||||
@ -17,11 +23,19 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
|
||||
private var onNavigateToVaultCalled = false
|
||||
private var onNavigateToLeaveOrganizationCalled = false
|
||||
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<MigrateToMyItemsEvent>()
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(
|
||||
MigrateToMyItemsState(
|
||||
organizationName = "Test Organization",
|
||||
viewState = MigrateToMyItemsState.ViewState(
|
||||
organizationName = "Test Organization",
|
||||
),
|
||||
dialog = null,
|
||||
organizationId = "test-org-id",
|
||||
),
|
||||
)
|
||||
|
||||
@ -32,7 +46,7 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setContent {
|
||||
setContent(intentManager = intentManager) {
|
||||
MigrateToMyItemsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToVault = { onNavigateToVaultCalled = true },
|
||||
@ -98,4 +112,73 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() {
|
||||
mutableEventFlow.tryEmit(MigrateToMyItemsEvent.NavigateToLeaveOrganization)
|
||||
assertTrue(onNavigateToLeaveOrganizationCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LaunchUri event should launch URI via intent manager`() {
|
||||
val testUri = "https://bitwarden.com/help/transfer-ownership/"
|
||||
mutableEventFlow.tryEmit(MigrateToMyItemsEvent.LaunchUri(testUri))
|
||||
verify {
|
||||
intentManager.launchUri(testUri.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Loading dialog should display when dialog state is Loading`() {
|
||||
mutableStateFlow.value = MigrateToMyItemsState(
|
||||
viewState = MigrateToMyItemsState.ViewState(
|
||||
organizationName = "Test Organization",
|
||||
),
|
||||
dialog = MigrateToMyItemsState.DialogState.Loading(
|
||||
message = "Migrating items to Test Organization...".asText(),
|
||||
),
|
||||
organizationId = "test-org-id",
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Migrating items to Test Organization...")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Error dialog should display when dialog state is Error`() {
|
||||
mutableStateFlow.value = MigrateToMyItemsState(
|
||||
viewState = MigrateToMyItemsState.ViewState(
|
||||
organizationName = "Test Organization",
|
||||
),
|
||||
dialog = MigrateToMyItemsState.DialogState.Error(
|
||||
title = "An error has occurred".asText(),
|
||||
message = "Failed to migrate items".asText(),
|
||||
),
|
||||
organizationId = "test-org-id",
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("An error has occurred")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Failed to migrate items")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Error dialog dismiss should send DismissDialogClicked action`() {
|
||||
mutableStateFlow.value = MigrateToMyItemsState(
|
||||
viewState = MigrateToMyItemsState.ViewState(
|
||||
organizationName = "Test Organization",
|
||||
),
|
||||
dialog = MigrateToMyItemsState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = "Failed to migrate items".asText(),
|
||||
),
|
||||
organizationId = "test-org-id",
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Okay")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.DismissDialogClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,63 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.migratetomyitems
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { userStateFlow } returns MutableStateFlow(DEFAULT_USER_STATE)
|
||||
}
|
||||
|
||||
private val policyManager = mockk<PolicyManager> {
|
||||
every { getPersonalOwnershipPolicyOrganizationId() } returns ORGANIZATION_ID
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked sends NavigateToVault event`() = runTest {
|
||||
fun `initial state should be set from organization data`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(ORGANIZATION_NAME, viewModel.stateFlow.value.viewState.organizationName)
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked should show loading dialog and trigger migration`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(null, awaitItem().dialog)
|
||||
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
|
||||
|
||||
val loadingState = awaitItem()
|
||||
assert(loadingState.dialog is MigrateToMyItemsState.DialogState.Loading)
|
||||
assertEquals(
|
||||
BitwardenString.migrating_items_to_x.asText(ORGANIZATION_NAME),
|
||||
(loadingState.dialog as MigrateToMyItemsState.DialogState.Loading).message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked should navigate to vault on success`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
|
||||
@ -17,6 +65,47 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MigrateToMyItemsResultReceived with success should clear dialog and navigate to vault`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
// First show the loading dialog
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.ContinueClicked)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
|
||||
success = true,
|
||||
),
|
||||
)
|
||||
assertEquals(MigrateToMyItemsEvent.NavigateToVault, awaitItem())
|
||||
}
|
||||
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MigrateToMyItemsResultReceived with failure should show error dialog`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
awaitItem() // Initial state
|
||||
|
||||
viewModel.trySendAction(
|
||||
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
|
||||
success = false,
|
||||
),
|
||||
)
|
||||
|
||||
val errorState = awaitItem()
|
||||
assert(errorState.dialog is MigrateToMyItemsState.DialogState.Error)
|
||||
val errorDialog = errorState.dialog as MigrateToMyItemsState.DialogState.Error
|
||||
assertEquals(BitwardenString.an_error_has_occurred.asText(), errorDialog.title)
|
||||
assertEquals(BitwardenString.failed_to_migrate_items_to_x.asText(), errorDialog.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DeclineAndLeaveClicked sends NavigateToLeaveOrganization event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@ -33,10 +122,83 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.HelpLinkClicked)
|
||||
val event = awaitItem()
|
||||
assert(event is MigrateToMyItemsEvent.LaunchUri)
|
||||
assertEquals("TODO_HELP_URL", (event as MigrateToMyItemsEvent.LaunchUri).uri)
|
||||
assertEquals(
|
||||
"https://bitwarden.com/help/transfer-ownership/",
|
||||
(event as MigrateToMyItemsEvent.LaunchUri).uri,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): MigrateToMyItemsViewModel =
|
||||
MigrateToMyItemsViewModel()
|
||||
@Test
|
||||
fun `DismissDialogClicked should clear dialog`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
awaitItem() // Initial state
|
||||
|
||||
// First show an error dialog
|
||||
viewModel.trySendAction(
|
||||
MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived(
|
||||
success = false,
|
||||
),
|
||||
)
|
||||
val errorState = awaitItem()
|
||||
assert(errorState.dialog is MigrateToMyItemsState.DialogState.Error)
|
||||
|
||||
// Dismiss the dialog
|
||||
viewModel.trySendAction(MigrateToMyItemsAction.DismissDialogClicked)
|
||||
val clearedState = awaitItem()
|
||||
assertNull(clearedState.dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle(),
|
||||
): MigrateToMyItemsViewModel =
|
||||
MigrateToMyItemsViewModel(
|
||||
authRepository = authRepository,
|
||||
policyManager = policyManager,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
}
|
||||
|
||||
private const val ORGANIZATION_ID = "test-organization-id"
|
||||
private const val ORGANIZATION_NAME = "Test Organization"
|
||||
|
||||
private val DEFAULT_ORGANIZATION = Organization(
|
||||
id = ORGANIZATION_ID,
|
||||
name = ORGANIZATION_NAME,
|
||||
shouldManageResetPassword = false,
|
||||
shouldUseKeyConnector = false,
|
||||
role = OrganizationType.USER,
|
||||
keyConnectorUrl = null,
|
||||
userIsClaimedByOrganization = false,
|
||||
limitItemDeletion = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = "test-user-id",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "test-user-id",
|
||||
name = "Test User",
|
||||
email = "test@example.com",
|
||||
avatarColorHex = "#ff0000",
|
||||
environment = Environment.Us,
|
||||
isPremium = false,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
needsMasterPassword = false,
|
||||
hasMasterPassword = true,
|
||||
trustedDevice = null,
|
||||
organizations = listOf(DEFAULT_ORGANIZATION),
|
||||
isBiometricsEnabled = false,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
isUsingKeyConnector = false,
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
firstTimeState = FirstTimeState(),
|
||||
isExportable = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -1165,4 +1165,6 @@ Do you want to switch to this account?</string>
|
||||
<string name="transfer_items_description">%1$s is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.</string>
|
||||
<string name="decline_and_leave">Decline and leave</string>
|
||||
<string name="why_am_i_seeing_this">Why am I seeing this?</string>
|
||||
<string name="migrating_items_to_x">Migrating items to %s</string>
|
||||
<string name="failed_to_migrate_items_to_x">Failed to migrate items to %s</string>
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user