BIT-205: Populate vault with login items (#246)

This commit is contained in:
Ramsey Smith
2023-11-15 14:15:04 -07:00
committed by GitHub
parent b0ea7a4229
commit f997ccfead
13 changed files with 688 additions and 90 deletions

View File

@@ -45,18 +45,18 @@ class LocalDateTimeSerializerTest {
LocalDateTimeData(
dataAsLocalDateTime = LocalDateTime.of(
2023,
10,
6,
17,
22,
28,
446666700,
8,
1,
16,
13,
3,
502391000,
),
),
json.decodeFromString<LocalDateTimeData>(
"""
{
"dataAsLocalDateTime": "2023-10-06T17:22:28.4466667Z"
"dataAsLocalDateTime": "2023-08-01T16:13:03.502391Z"
}
""",
),
@@ -69,7 +69,7 @@ class LocalDateTimeSerializerTest {
json.parseToJsonElement(
"""
{
"dataAsLocalDateTime": "2023-10-06T17:22:28.44Z"
"dataAsLocalDateTime": "2023-10-06T17:22:28.4400000Z"
}
""",
),

View File

@@ -121,25 +121,49 @@ class VaultSdkSourceTest {
}
}
@Test
fun `Cipher decryptListCollection should call SDK and return a Result with correct data`() =
runBlocking {
val mockCiphers = mockk<List<Cipher>>()
val expectedResult = mockk<List<CipherListView>>()
coEvery {
clientVault.ciphers().decryptList(
ciphers = mockCiphers,
)
} returns expectedResult
val result = vaultSdkSource.decryptCipherListCollection(
cipherList = mockCiphers,
)
assertEquals(
expectedResult.asSuccess(),
result,
)
coVerify {
clientVault.ciphers().decryptList(
ciphers = mockCiphers,
)
}
}
@Test
fun `Cipher decryptList should call SDK and return a Result with correct data`() = runBlocking {
val mockCiphers = mockk<List<Cipher>>()
val expectedResult = mockk<List<CipherListView>>()
val mockCiphers = mockk<Cipher>()
val expectedResult = mockk<CipherView>()
coEvery {
clientVault.ciphers().decryptList(
ciphers = mockCiphers,
clientVault.ciphers().decrypt(
cipher = mockCiphers,
)
} returns expectedResult
val result = vaultSdkSource.decryptCipherList(
cipherList = mockCiphers,
cipherList = listOf(mockCiphers),
)
assertEquals(
expectedResult.asSuccess(),
listOf(expectedResult).asSuccess(),
result,
)
coVerify {
clientVault.ciphers().decryptList(
ciphers = mockCiphers,
clientVault.ciphers().decrypt(
cipher = mockCiphers,
)
}
}

View File

@@ -0,0 +1,160 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.core.AttachmentView
import com.bitwarden.core.CardView
import com.bitwarden.core.CipherRepromptType
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.FieldType
import com.bitwarden.core.FieldView
import com.bitwarden.core.IdentityView
import com.bitwarden.core.LoginUriView
import com.bitwarden.core.LoginView
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.SecureNoteType
import com.bitwarden.core.SecureNoteView
import com.bitwarden.core.UriMatchType
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* Create a mock [CipherView] with a given [number].
*/
fun createMockCipherView(number: Int): CipherView =
CipherView(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
folderId = "mockId-$number",
collectionIds = listOf("mockCollectionId-$number"),
key = "mockKey-$number",
name = "mockName-$number",
notes = "mockNotes-$number",
type = CipherType.LOGIN,
login = createMockLoginView(number = number),
creationDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
deletedDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
revisionDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
attachments = listOf(createMockAttachmentView(number = number)),
card = createMockCardView(number = number),
fields = listOf(createMockFieldView(number = number)),
identity = createMockIdentityView(number = number),
favorite = false,
passwordHistory = listOf(createMockPasswordHistoryView(number = number)),
reprompt = CipherRepromptType.NONE,
secureNote = createMockSecureNoteView(),
edit = false,
organizationUseTotp = false,
viewPassword = false,
localData = null,
)
/**
* Create a mock [LoginView] with a given [number].
*/
fun createMockLoginView(number: Int): LoginView =
LoginView(
username = "mockUsername-$number",
password = "mockPassword-$number",
passwordRevisionDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
autofillOnPageLoad = false,
uris = listOf(createMockUriView(number = number)),
totp = "mockTotp-$number",
)
/**
* Create a mock [LoginUriView] with a given [number].
*/
fun createMockUriView(number: Int): LoginUriView =
LoginUriView(
uri = "mockUri-$number",
match = UriMatchType.HOST,
)
/**
* Create a mock [AttachmentView] with a given [number].
*/
fun createMockAttachmentView(number: Int): AttachmentView =
AttachmentView(
fileName = "mockFileName-$number",
size = "1",
sizeName = "mockSizeName-$number",
id = "mockId-$number",
url = "mockUrl-$number",
key = "mockKey-$number",
)
/**
* Create a mock [CardView] with a given [number].
*/
fun createMockCardView(number: Int): CardView =
CardView(
number = "mockNumber-$number",
expMonth = "mockExpMonth-$number",
code = "mockCode-$number",
expYear = "mockExpirationYear-$number",
cardholderName = "mockCardholderName-$number",
brand = "mockBrand-$number",
)
/**
* Create a mock [FieldView] with a given [number].
*/
fun createMockFieldView(number: Int): FieldView =
FieldView(
linkedId = 100U,
name = "mockName-$number",
type = FieldType.HIDDEN,
value = "mockValue-$number",
)
/**
* Create a mock [IdentityView] with a given [number].
*/
fun createMockIdentityView(number: Int): IdentityView =
IdentityView(
firstName = "mockFirstName-$number",
middleName = "mockMiddleName-$number",
lastName = "mockLastName-$number",
passportNumber = "mockPassportNumber-$number",
country = "mockCountry-$number",
address1 = "mockAddress1-$number",
address2 = "mockAddress2-$number",
address3 = "mockAddress3-$number",
city = "mockCity-$number",
postalCode = "mockPostalCode-$number",
title = "mockTitle-$number",
ssn = "mockSsn-$number",
phone = "mockPhone-$number",
company = "mockCompany-$number",
licenseNumber = "mockLicenseNumber-$number",
state = "mockState-$number",
email = "mockEmail-$number",
username = "mockUsername-$number",
)
/**
* Create a mock [PasswordHistoryView] with a given [number].
*/
fun createMockPasswordHistoryView(number: Int): PasswordHistoryView =
PasswordHistoryView(
password = "mockPassword-$number",
lastUsedDate = LocalDateTime
.parse("2023-10-27T12:00:00")
.toInstant(ZoneOffset.UTC),
)
/**
* Create a mock [SecureNoteView] with a given [number].
*/
fun createMockSecureNoteView(): SecureNoteView =
SecureNoteView(
type = SecureNoteType.GENERIC,
)

View File

@@ -18,13 +18,18 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResul
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -51,7 +56,7 @@ class VaultRepositoryTest {
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
@@ -70,7 +75,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Loaded(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -86,7 +91,7 @@ class VaultRepositoryTest {
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
@@ -101,7 +106,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Loaded(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -111,7 +116,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Pending(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -120,7 +125,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Loaded(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -161,7 +166,7 @@ class VaultRepositoryTest {
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns mockException.asFailure()
@@ -221,7 +226,7 @@ class VaultRepositoryTest {
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
@@ -236,7 +241,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Loaded(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -249,7 +254,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Pending(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -258,7 +263,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.NoNetwork(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
@@ -275,7 +280,7 @@ class VaultRepositoryTest {
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
@@ -310,6 +315,102 @@ class VaultRepositoryTest {
coVerify { syncService.sync() }
}
@Test
fun `sync should be able to be called after unlockVaultAndSync is canceled`() = runTest {
coEvery {
syncService.sync()
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
fakeAuthDiskSource.storePrivateKey(
userId = "mockUserId",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.storeUserKey(
userId = "mockUserId",
userKey = "mockKey-1",
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()),
email = "email",
password = "mockPassword-1",
userKey = "mockKey-1",
privateKey = "mockPrivateKey-1",
organizationKeys = mapOf(),
),
)
} coAnswers {
delay(Long.MAX_VALUE)
Result.success(InitializeCryptoResult.Success)
}
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
}
coVerify(exactly = 0) { syncService.sync() }
scope.cancel()
vaultRepository.sync()
coVerify(exactly = 1) { syncService.sync() }
}
@Test
fun `sync should not be able to be called while unlockVaultAndSync is called`() = runTest {
coEvery {
syncService.sync()
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
fakeAuthDiskSource.storePrivateKey(
userId = "mockUserId",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.storeUserKey(
userId = "mockUserId",
userKey = "mockKey-1",
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()),
email = "email",
password = "mockPassword-1",
userKey = "mockKey-1",
privateKey = "mockPrivateKey-1",
organizationKeys = mapOf(),
),
)
} coAnswers {
delay(Long.MAX_VALUE)
Result.success(InitializeCryptoResult.Success)
}
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
}
// We call sync here but the call to the SyncService should be blocked
// by the active call to unlockVaultAndSync
vaultRepository.sync()
scope.cancel()
coVerify(exactly = 0) { syncService.sync() }
}
@Test
fun `unlockVaultAndSync with initializeCrypto failure should return GenericError`() =
runTest {
@@ -451,7 +552,7 @@ class VaultRepositoryTest {
} returns Result.success(createMockSyncResponse(number = 1))
coEvery {
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
} returns listOf(createMockCipherView(number = 1)).asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
} returns listOf(createMockFolderView(number = 1)).asSuccess()
@@ -466,7 +567,7 @@ class VaultRepositoryTest {
assertEquals(
DataState.Loaded(
data = VaultData(
cipherListViewList = listOf(createMockCipherListView(number = 1)),
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),

View File

@@ -3,15 +3,31 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
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
class VaultViewModelTest : BaseViewModelTest() {
private val mutableVaultDataStateFlow =
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
private val vaultRepository: VaultRepository =
mockk {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
every { sync() } returns Unit
}
@Test
fun `initial state should be correct when not set`() {
val viewModel = createViewModel()
@@ -82,6 +98,113 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
),
),
)
val viewModel = createViewModel()
assertEquals(
createMockVaultState(
viewState = VaultState.ViewState.Content(
loginItemsCount = 1,
cardItemsCount = 0,
identityItemsCount = 0,
secureNoteItemsCount = 0,
favoriteItems = listOf(),
folderItems = listOf(
VaultState.ViewState.FolderItem(
id = "mockId-1",
name = "mockName-1".asText(),
itemCount = 1,
),
),
noFolderItems = listOf(),
trashItemsCount = 0,
),
),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Loaded with empty items should update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
cipherViewList = emptyList(),
folderViewList = emptyList(),
),
),
)
val viewModel = createViewModel()
assertEquals(
createMockVaultState(viewState = VaultState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Loading should update state to Loading`() = runTest {
mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)
val viewModel = createViewModel()
assertEquals(
createMockVaultState(viewState = VaultState.ViewState.Loading),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow Error should show toast and update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
error = IllegalStateException(),
),
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
assertEquals(
VaultEvent.ShowToast("Vault error state not yet implemented"),
awaitItem(),
)
}
assertEquals(
createMockVaultState(viewState = VaultState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `vaultDataStateFlow NoNetwork should show toast and update state to NoItems`() = runTest {
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(),
)
val viewModel = createViewModel()
viewModel.eventFlow.test {
assertEquals(
VaultEvent.ShowToast("Vault no network state not yet implemented"),
awaitItem(),
)
}
assertEquals(
createMockVaultState(viewState = VaultState.ViewState.NoItems),
viewModel.stateFlow.value,
)
}
@Test
fun `AddItemClick should emit NavigateToAddItemScreen`() = runTest {
val viewModel = createViewModel()
@@ -175,12 +298,19 @@ class VaultViewModelTest : BaseViewModelTest() {
state: VaultState? = DEFAULT_STATE,
): VaultViewModel = VaultViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
vaultRepository = vaultRepository,
)
}
private val DEFAULT_STATE: VaultState = VaultState(
avatarColorString = "FF0000FF",
initials = "BW",
accountSummaries = emptyList(),
viewState = VaultState.ViewState.Loading,
)
private const val DEFAULT_COLOR_STRING: String = "FF0000FF"
private const val DEFAULE_INITIALS: String = "BW"
private val DEFAULT_STATE: VaultState =
createMockVaultState(viewState = VaultState.ViewState.Loading)
private fun createMockVaultState(viewState: VaultState.ViewState): VaultState =
VaultState(
avatarColorString = DEFAULT_COLOR_STRING,
initials = DEFAULE_INITIALS,
accountSummaries = emptyList(),
viewState = viewState,
)

View File

@@ -0,0 +1,57 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultDataExtensionsTest {
@Test
fun `toViewState should transform full VaultData into ViewState Content`() {
val vaultData = VaultData(
cipherViewList = listOf(createMockCipherView(number = 1)),
folderViewList = listOf(createMockFolderView(number = 1)),
)
val actual = vaultData.toViewState()
assertEquals(
VaultState.ViewState.Content(
loginItemsCount = 1,
cardItemsCount = 0,
identityItemsCount = 0,
secureNoteItemsCount = 0,
favoriteItems = listOf(),
folderItems = listOf(
VaultState.ViewState.FolderItem(
id = "mockId-1",
name = "mockName-1".asText(),
itemCount = 1,
),
),
noFolderItems = listOf(),
trashItemsCount = 0,
),
actual,
)
}
@Test
fun `toViewState should transform empty VaultData into ViewState NoItems`() {
val vaultData = VaultData(
cipherViewList = emptyList(),
folderViewList = emptyList(),
)
val actual = vaultData.toViewState()
assertEquals(
VaultState.ViewState.NoItems,
actual,
)
}
}