BIT-769: Move to organization functionality (#670)

This commit is contained in:
Ramsey Smith authored and Álison Fernandes committed 2024-06-20 17:08:07 +01:00
1 parent 413677852b
commit 9ba6474c37
15 files changed
+1008 -188

No files matched your search

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest
@@ -71,6 +72,27 @@ class CiphersServiceTest : BaseServiceTest() {
val result = ciphersService.deleteCipher(cipherId = cipherId)
assertEquals(Unit, result.getOrThrow())
}
@Test
fun `shareCipher should execute the share cipher API`() = runTest {
server.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(CREATE_UPDATE_CIPHER_SUCCESS_JSON),
)
val result = ciphersService.shareCipher(
cipherId = "mockId-1",
body = ShareCipherJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("mockId-1"),
),
)
assertEquals(
createMockCipher(number = 1),
result.getOrThrow(),
)
}
}
private const val CREATE_UPDATE_CIPHER_SUCCESS_JSON = """
@@ -23,6 +23,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendFileResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SendTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@@ -58,6 +59,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
@@ -2000,6 +2002,111 @@ class VaultRepositoryTest {
assertEquals(DeleteSendResult.Success, result)
}
@Test
@Suppress("MaxLineLength")
fun `shareCipher with cipherService shareCipher success should return ShareCipherResultSuccess`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
coEvery {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = createMockCipherView(number = 1),
)
} returns createMockSdkCipher(number = 1).asSuccess()
coEvery {
ciphersService.shareCipher(
cipherId = "mockId-1",
body = ShareCipherJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("mockId-1"),
),
)
} returns createMockCipher(number = 1).asSuccess()
coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs
val result = vaultRepository.shareCipher(
cipherId = "mockId-1",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
assertEquals(
ShareCipherResult.Success,
result,
)
}
@Test
@Suppress("MaxLineLength")
fun `shareCipher with cipherService shareCipher failure should return ShareCipherResultError`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
coEvery {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = createMockCipherView(number = 1),
)
} returns createMockSdkCipher(number = 1).asSuccess()
coEvery {
ciphersService.shareCipher(
cipherId = "mockId-1",
body = ShareCipherJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("mockId-1"),
),
)
} returns Throwable("Fail").asFailure()
coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs
val result = vaultRepository.shareCipher(
cipherId = "mockId-1",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
assertEquals(
ShareCipherResult.Error(errorMessage = null),
result,
)
}
@Test
@Suppress("MaxLineLength")
fun `shareCipher with cipherService encryptCipher failure should return ShareCipherResultError`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
coEvery {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = createMockCipherView(number = 1),
)
} returns Throwable("Fail").asFailure()
coEvery {
ciphersService.shareCipher(
cipherId = "mockId-1",
body = ShareCipherJsonRequest(
cipher = createMockCipherJsonRequest(number = 1),
collectionIds = listOf("mockId-1"),
),
)
} returns createMockCipher(number = 1).asSuccess()
coEvery { vaultDiskSource.saveCipher(userId, createMockCipher(number = 1)) } just runs
val result = vaultRepository.shareCipher(
cipherId = "mockId-1",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
assertEquals(
ShareCipherResult.Error(errorMessage = null),
result,
)
}
//region Helper functions
/**
@@ -84,11 +84,11 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
@Test
fun `selecting an organization should send OrganizationSelect action`() {
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 1")
.onNodeWithContentDescriptionAfterScroll(label = "Organization, mockOrganizationName-1")
.performClick()
// Choose the option from the menu
composeTestRule
.onAllNodesWithText(text = "Organization 2")
.onAllNodesWithText(text = "mockOrganizationName-2")
.onLast()
.performScrollTo()
.performClick()
@@ -97,22 +97,12 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
viewModel.trySendAction(
VaultMoveToOrganizationAction.OrganizationSelect(
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "2",
name = "Organization 2",
id = "mockOrganizationId-2",
name = "mockOrganizationName-2",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
id = "mockId-2",
name = "mockName-2",
isSelected = false,
),
),
@@ -125,36 +115,36 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
@Test
fun `the organization option field should display according to state`() {
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 1")
.onNodeWithContentDescriptionAfterScroll(label = "Organization, mockOrganizationName-1")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "2",
selectedOrganizationId = "mockOrganizationId-2",
),
)
}
composeTestRule
.onNodeWithContentDescriptionAfterScroll(label = "Organization, Organization 2")
.onNodeWithContentDescriptionAfterScroll(label = "Organization, mockOrganizationName-2")
.assertIsDisplayed()
}
@Test
fun `selecting a collection should send CollectionSelect action`() {
composeTestRule
.onNodeWithText(text = "Collection 2")
.onNodeWithText(text = "mockName-1")
.performClick()
verify {
viewModel.trySendAction(
VaultMoveToOrganizationAction.CollectionSelect(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
id = "mockId-1",
name = "mockName-1",
isSelected = true,
),
),
)
@@ -164,14 +154,8 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
@Test
fun `the collection list should display according to state`() {
composeTestRule
.onNodeWithText("Collection 1")
.onNodeWithText("mockName-1")
.assertIsOn()
composeTestRule
.onNodeWithText("Collection 2")
.assertIsOff()
composeTestRule
.onNodeWithText("Collection 3")
.assertIsOff()
mutableStateFlow.update { currentState ->
currentState.copy(
@@ -180,31 +164,27 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
.map { organization ->
organization.copy(
collections =
if (organization.id == "1") {
if (organization.id == "mockOrganizationId-1") {
organization
.collections
.map { collection ->
collection.copy(isSelected = collection.id != "1")
collection.copy(
isSelected = collection.id != "mockId-1",
)
}
} else {
organization.collections
},
)
},
selectedOrganizationId = "1",
selectedOrganizationId = "mockOrganizationId-1",
),
)
}
composeTestRule
.onNodeWithText("Collection 1")
.onNodeWithText("mockName-1")
.assertIsOff()
composeTestRule
.onNodeWithText("Collection 2")
.assertIsOn()
composeTestRule
.onNodeWithText("Collection 3")
.assertIsOn()
}
@Test
@@ -251,7 +231,7 @@ private fun createVaultMoveToOrganizationState(): VaultMoveToOrganizationState =
vaultItemId = "mockId",
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "1",
selectedOrganizationId = "mockOrganizationId-1",
),
dialogState = null,
)
@@ -2,10 +2,27 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.x8bit.bitwarden.R
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.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.createMockOrganizationList
import io.mockk.coEvery
import io.mockk.coVerify
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.Test
@@ -17,6 +34,22 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
state = initialState,
)
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val mutableCollectionFlow =
MutableStateFlow<DataState<List<CollectionView>>>(DataState.Loading)
private val vaultRepository: VaultRepository = mockk {
every { getVaultItemStateFlow(DEFAULT_ITEM_ID) } returns mutableVaultItemFlow
every { collectionsStateFlow } returns mutableCollectionFlow
}
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
}
@Test
fun `initial state should be correct when state is null`() = runTest {
val viewModel = createViewModel(
@@ -32,14 +65,13 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct`() = runTest {
val initState = createVaultMoveToOrganizationState()
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = initState,
state = initialState,
),
)
assertEquals(initState, viewModel.stateFlow.value)
assertEquals(initialState, viewModel.stateFlow.value)
}
@Test
@@ -55,27 +87,21 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
@Test
fun `OrganizationSelect should update selected Organization`() = runTest {
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "1",
),
),
),
)
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(value = DataState.Loaded(createMockCipherView(number = 1)))
val action = VaultMoveToOrganizationAction.OrganizationSelect(
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "3",
name = "Organization 3",
id = "mockOrganizationId-3",
name = "mockOrganizationName-3",
collections = emptyList(),
),
)
val expectedState = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "3",
selectedOrganizationId = "mockOrganizationId-3",
cipherToMove = createMockCipherView(number = 1),
),
)
@@ -89,50 +115,34 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
@Test
fun `CollectionSelect should update selected Collections`() = runTest {
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "1",
),
),
),
)
val selectCollection3Action = VaultMoveToOrganizationAction.CollectionSelect(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
)
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(value = DataState.Loaded(createMockCipherView(number = 1)))
val unselectCollection1Action = VaultMoveToOrganizationAction.CollectionSelect(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
id = "mockId-1",
name = "mockName-1",
isSelected = true,
),
)
val expectedState = createVaultMoveToOrganizationState(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList()
.map { organization ->
organization.copy(
collections =
if (organization.id == "1") {
organization.collections.map {
it.copy(isSelected = it.id == "3")
}
} else {
organization.collections
},
)
},
selectedOrganizationId = "1",
organizations = createMockOrganizationList().map { organization ->
organization.copy(
collections = if (organization.id == "mockOrganizationId-1") {
organization.collections.map {
it.copy(isSelected = false)
}
} else {
organization.collections
},
)
},
cipherToMove = createMockCipherView(number = 1),
selectedOrganizationId = "mockOrganizationId-1",
),
)
viewModel.actionChannel.trySend(selectCollection3Action)
viewModel.actionChannel.trySend(unselectCollection1Action)
assertEquals(
@@ -142,40 +152,243 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
}
@Test
fun `MoveClick should show dialog, and remove it once an item is moved`() = runTest {
val viewModel = createViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = initialState,
fun `DataState Loading should show loading state`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(value = DataState.Loading)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(viewState = VaultMoveToOrganizationState.ViewState.Loading),
awaitItem(),
)
}
}
@Test
fun `DataState Pending should show content state`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(
value = DataState.Pending(
data = createMockCipherView(number = 1),
),
)
viewModel.stateFlow.test {
assertEquals(initialState, awaitItem())
assertEquals(
initialState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
), dialogState = null,
),
awaitItem(),
)
}
}
@Test
fun `DataState Error should show Error State`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(
value = DataState.Error(
error = IllegalStateException(),
data = null,
),
)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Error(
message = R.string.generic_error_message.asText(),
),
),
awaitItem(),
)
}
}
@Test
fun `DataState NoNetwork should show Error Dialog`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(
value = DataState.NoNetwork(),
)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(R.string.internet_connection_required_message.asText()),
),
),
awaitItem(),
)
}
}
@Test
fun `MoveClick with shareCipher success should show loading dialog, and remove it`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(value = DataState.Loaded(createMockCipherView(number = 1)))
coEvery {
vaultRepository.shareCipher(
cipherId = "mockCipherId",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
} returns ShareCipherResult.Success
viewModel.stateFlow.test {
assertEquals(
initialState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
),
),
awaitItem(),
)
viewModel.actionChannel.trySend(VaultMoveToOrganizationAction.MoveClick)
assertEquals(
initialState.copy(
dialogState = VaultMoveToOrganizationState.DialogState.Loading(
message = R.string.saving.asText(),
),
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
),
),
awaitItem(),
)
assertEquals(
initialState,
initialState.copy(
dialogState = null,
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
),
),
awaitItem(),
)
}
coVerify {
vaultRepository.shareCipher(
cipherId = "mockCipherId",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
}
}
@Test
fun `MoveClick with shareCipher error should show error dialog`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(value = DataState.Loaded(createMockCipherView(number = 1)))
coEvery {
vaultRepository.shareCipher(
cipherId = "mockCipherId",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
} returns ShareCipherResult.Error(errorMessage = null)
viewModel.stateFlow.test {
assertEquals(
initialState.copy(
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
),
),
awaitItem(),
)
viewModel.actionChannel.trySend(VaultMoveToOrganizationAction.MoveClick)
assertEquals(
initialState.copy(
dialogState = VaultMoveToOrganizationState.DialogState.Loading(
message = R.string.saving.asText(),
),
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
),
),
awaitItem(),
)
assertEquals(
initialState.copy(
dialogState = VaultMoveToOrganizationState.DialogState.Error(
message = R.string.generic_error_message.asText(),
),
viewState = VaultMoveToOrganizationState.ViewState.Content(
organizations = createMockOrganizationList(),
selectedOrganizationId = "mockOrganizationId-1",
cipherToMove = createMockCipherView(number = 1),
),
),
awaitItem(),
)
}
coVerify {
vaultRepository.shareCipher(
cipherId = "mockCipherId",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
}
}
@Test
fun `MoveClick with shareCipher success should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
mutableCollectionFlow.tryEmit(value = DataState.Loaded(DEFAULT_COLLECTIONS))
mutableVaultItemFlow.tryEmit(value = DataState.Loaded(createMockCipherView(number = 1)))
coEvery {
vaultRepository.shareCipher(
cipherId = "mockCipherId",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
} returns ShareCipherResult.Success
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultMoveToOrganizationAction.MoveClick)
assertEquals(
VaultMoveToOrganizationEvent.NavigateBack,
awaitItem(),
)
}
coVerify {
vaultRepository.shareCipher(
cipherId = "mockCipherId",
cipherView = createMockCipherView(number = 1),
collectionIds = listOf("mockId-1"),
)
}
}
private fun createViewModel(
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
): VaultMoveToOrganizationViewModel =
VaultMoveToOrganizationViewModel(
savedStateHandle = savedStateHandle,
)
vaultRepo: VaultRepository = vaultRepository,
authRepo: AuthRepository = authRepository,
): VaultMoveToOrganizationViewModel = VaultMoveToOrganizationViewModel(
savedStateHandle = savedStateHandle,
authRepository = authRepo,
vaultRepository = vaultRepo,
)
private fun createSavedStateHandleWithState(
state: VaultMoveToOrganizationState? = null,
vaultItemId: String = "mockId",
vaultItemId: String = "mockCipherId",
) = SavedStateHandle().apply {
set("state", state)
set("vault_move_to_organization_id", vaultItemId)
@@ -183,13 +396,51 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
private fun createVaultMoveToOrganizationState(
viewState: VaultMoveToOrganizationState.ViewState = VaultMoveToOrganizationState.ViewState.Empty,
vaultItemId: String = "mockId",
viewState: VaultMoveToOrganizationState.ViewState = VaultMoveToOrganizationState.ViewState.Loading,
vaultItemId: String = "mockCipherId",
dialogState: VaultMoveToOrganizationState.DialogState? = null,
): VaultMoveToOrganizationState =
VaultMoveToOrganizationState(
vaultItemId = vaultItemId,
viewState = viewState,
dialogState = dialogState,
)
): VaultMoveToOrganizationState = VaultMoveToOrganizationState(
vaultItemId = vaultItemId,
viewState = viewState,
dialogState = dialogState,
)
}
private const val DEFAULT_ITEM_ID: String = "mockCipherId"
private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = listOf(
Organization(
id = "mockOrganizationId-1",
name = "mockOrganizationName-1",
),
Organization(
id = "mockOrganizationId-2",
name = "mockOrganizationName-2",
),
Organization(
id = "mockOrganizationId-3",
name = "mockOrganizationName-3",
),
),
),
),
)
private val DEFAULT_COLLECTIONS = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
createMockCollectionView(number = 4),
)
@@ -0,0 +1,117 @@
package com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.VaultMoveToOrganizationState
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultMoveToOrganizationExtensionsTest {
@Test
@Suppress("MaxLineLength")
fun `toViewState should transform a valid triple of CipherView, CollectionView list, and UserState into Content ViewState`() {
val triple = Triple(
first = createMockCipherView(number = 1),
second = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
),
third = createMockUserState(),
)
val result = triple.toViewState()
assertEquals(
VaultMoveToOrganizationState.ViewState.Content(
selectedOrganizationId = "mockOrganizationId-1",
organizations = createMockOrganizationList(),
cipherToMove = createMockCipherView(number = 1),
),
result,
)
}
@Test
@Suppress("MaxLineLength")
fun `toViewState should transform a triple of null CipherView, CollectionView list, and UserState into Error ViewState`() {
val triple = Triple(
first = null,
second = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
),
third = createMockUserState(),
)
val result = triple.toViewState()
assertEquals(
VaultMoveToOrganizationState.ViewState.Error(R.string.generic_error_message.asText()),
result,
)
}
@Test
@Suppress("MaxLineLength")
fun `toViewState should transform a triple of CipherView, CollectionView list, and UserState without organizations into Empty ViewState`() {
val triple = Triple(
first = createMockCipherView(number = 1),
second = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
),
third = createMockUserState(hasOrganizations = false),
)
val result = triple.toViewState()
assertEquals(
VaultMoveToOrganizationState.ViewState.Empty,
result,
)
}
}
private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
organizations = if (hasOrganizations) {
listOf(
Organization(
id = "mockOrganizationId-1",
name = "mockOrganizationName-1",
),
Organization(
id = "mockOrganizationId-2",
name = "mockOrganizationName-2",
),
Organization(
id = "mockOrganizationId-3",
name = "mockOrganizationName-3",
),
)
} else {
emptyList()
},
),
),
)
@@ -3,56 +3,31 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util
import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.VaultMoveToOrganizationState
/**
* Creates a list of mock organizations.
* Creates a list of mock [VaultMoveToOrganizationState.ViewState.Content.Organization].
*/
fun createMockOrganizationList():
List<VaultMoveToOrganizationState.ViewState.Content.Organization> =
listOf(
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "1",
name = "Organization 1",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
createMockOrganization(number = 1),
createMockOrganization(number = 2, isCollectionSelected = false),
createMockOrganization(number = 3, isCollectionSelected = false),
)
/**
* Creates a [VaultMoveToOrganizationState.ViewState.Content.Organization] with a given number.
*/
fun createMockOrganization(
number: Int,
isCollectionSelected: Boolean = true,
): VaultMoveToOrganizationState.ViewState.Content.Organization =
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "mockOrganizationId-$number",
name = "mockOrganizationName-$number",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "mockId-$number",
name = "mockName-$number",
isSelected = isCollectionSelected,
),
),
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "2",
name = "Organization 2",
collections = listOf(
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "1",
name = "Collection 1",
isSelected = true,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "2",
name = "Collection 2",
isSelected = false,
),
VaultMoveToOrganizationState.ViewState.Content.Collection(
id = "3",
name = "Collection 3",
isSelected = false,
),
),
),
VaultMoveToOrganizationState.ViewState.Content.Organization(
id = "3",
name = "Organization 3",
collections = emptyList(),
),
)