BIT-1082: Implement vault unlock functionality (#263)

This commit is contained in:
Brian Yencho
2023-11-21 12:06:15 -06:00
committed by GitHub
parent 3b0532f7fd
commit ee322ef444
21 changed files with 865 additions and 154 deletions

View File

@@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import io.mockk.clearMocks
import io.mockk.coEvery
@@ -47,6 +48,7 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
@@ -62,7 +64,10 @@ class AuthRepositoryTest {
private val accountsService: AccountsService = mockk()
private val identityService: IdentityService = mockk()
private val haveIBeenPwnedService: HaveIBeenPwnedService = mockk()
private val vaultRepository: VaultRepository = mockk()
private val mutableVaultStateFlow = MutableStateFlow(VAULT_STATE)
private val vaultRepository: VaultRepository = mockk() {
every { vaultStateFlow } returns mutableVaultStateFlow
}
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeEnvironmentRepository =
FakeEnvironmentRepository()
@@ -117,6 +122,41 @@ class AuthRepositoryTest {
unmockkStatic(GET_TOKEN_RESPONSE_EXTENSIONS_PATH)
}
@Test
fun `userStateFlow should update with changes to the UserStateJson and VaultState data`() {
fakeAuthDiskSource.userState = null
assertEquals(
null,
repository.userStateFlow.value,
)
mutableVaultStateFlow.value = VAULT_STATE
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
assertEquals(
SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
),
repository.userStateFlow.value,
)
fakeAuthDiskSource.userState = MULTI_USER_STATE
assertEquals(
MULTI_USER_STATE.toUserState(
vaultState = VAULT_STATE,
),
repository.userStateFlow.value,
)
val emptyVaultState = VaultState(unlockedVaultUserIds = emptySet())
mutableVaultStateFlow.value = emptyVaultState
assertEquals(
MULTI_USER_STATE.toUserState(
vaultState = emptyVaultState,
),
repository.userStateFlow.value,
)
}
@Test
fun `rememberedEmailAddress should pull from and update AuthDiskSource`() {
// AuthDiskSource and the repository start with the same value.
@@ -287,6 +327,7 @@ class AuthRepositoryTest {
.returns(Result.success(successResponse))
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
@@ -321,6 +362,7 @@ class AuthRepositoryTest {
captchaToken = null,
)
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
@@ -710,6 +752,7 @@ class AuthRepositoryTest {
} returns Result.success(successResponse)
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
@@ -770,6 +813,7 @@ class AuthRepositoryTest {
} returns Result.success(successResponse)
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
@@ -934,5 +978,8 @@ class AuthRepositoryTest {
USER_ID_2 to ACCOUNT_2,
),
)
private val VAULT_STATE = VaultState(
unlockedVaultUserIds = setOf(USER_ID_1),
)
}
}

View File

@@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
@@ -82,4 +84,81 @@ class UserStateJsonExtensionsTest {
),
)
}
@Test
fun `toUserState should return the correct UserState for an unlocked vault`() {
assertEquals(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "activeName",
email = "activeEmail",
avatarColorHex = "activeAvatarColorHex",
isVaultUnlocked = true,
),
),
),
UserStateJson(
activeUserId = "activeUserId",
accounts = mapOf(
"activeUserId" to AccountJson(
profile = mockk() {
every { userId } returns "activeUserId"
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns "activeAvatarColorHex"
},
tokens = mockk(),
settings = mockk(),
),
),
)
.toUserState(
vaultState = VaultState(
unlockedVaultUserIds = setOf("activeUserId"),
),
),
)
}
@Test
fun `toUserState return the correct UserState for a locked vault`() {
assertEquals(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "activeName",
email = "activeEmail",
avatarColorHex = "activeAvatarColorHex",
isVaultUnlocked = false,
),
),
),
UserStateJson(
activeUserId = "activeUserId",
accounts = mapOf(
"activeUserId" to AccountJson(
profile = mockk() {
every { userId } returns "activeUserId"
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns "activeAvatarColorHex"
},
tokens = mockk(),
settings = mockk(),
),
),
)
.toUserState(
vaultState = VaultState(
unlockedVaultUserIds = emptySet(),
),
),
)
}
}

View File

@@ -27,6 +27,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import io.mockk.awaits
import io.mockk.coEvery
@@ -498,6 +499,12 @@ class VaultRepositoryTest {
),
)
} returns Result.success(InitializeCryptoResult.Success)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(
masterPassword = "mockPassword-1",
@@ -507,6 +514,12 @@ class VaultRepositoryTest {
VaultUnlockResult.Success,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = setOf("mockId-1"),
),
vaultRepository.vaultStateFlow.value,
)
coVerify { syncService.sync() }
}
@@ -638,6 +651,12 @@ class VaultRepositoryTest {
),
)
} returns Result.failure(IllegalStateException())
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(
masterPassword = "mockPassword-1",
@@ -647,6 +666,12 @@ class VaultRepositoryTest {
VaultUnlockResult.GenericError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@@ -681,12 +706,24 @@ class VaultRepositoryTest {
),
)
} returns Result.success(InitializeCryptoResult.AuthenticationError)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
assertEquals(
VaultUnlockResult.AuthenticationError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@@ -694,6 +731,12 @@ class VaultRepositoryTest {
fun `unlockVaultAndSyncForCurrentUser with missing user state should return InvalidStateError `() =
runTest {
fakeAuthDiskSource.userState = null
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
@@ -701,12 +744,25 @@ class VaultRepositoryTest {
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with missing user key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
@@ -721,12 +777,24 @@ class VaultRepositoryTest {
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSyncForCurrentUser with missing private key should return InvalidStateError `() =
runTest {
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
@@ -741,10 +809,17 @@ class VaultRepositoryTest {
VaultUnlockResult.InvalidStateError,
result,
)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
}
@Test
fun `unlockVault with initializeCrypto success should return Success`() = runTest {
val userId = "userId"
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
@@ -763,7 +838,15 @@ class VaultRepositoryTest {
),
)
} returns InitializeCryptoResult.Success.asSuccess()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVault(
userId = userId,
masterPassword = masterPassword,
kdf = kdf,
email = email,
@@ -771,7 +854,14 @@ class VaultRepositoryTest {
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
assertEquals(VaultUnlockResult.Success, result)
assertEquals(
VaultState(
unlockedVaultUserIds = setOf(userId),
),
vaultRepository.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
@@ -790,6 +880,7 @@ class VaultRepositoryTest {
@Test
fun `unlockVault with initializeCrypto authentication failure should return AuthenticationError`() =
runTest {
val userId = "userId"
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
@@ -808,7 +899,15 @@ class VaultRepositoryTest {
),
)
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVault(
userId = userId,
masterPassword = masterPassword,
kdf = kdf,
email = email,
@@ -816,7 +915,14 @@ class VaultRepositoryTest {
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
assertEquals(VaultUnlockResult.AuthenticationError, result)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
@@ -833,6 +939,7 @@ class VaultRepositoryTest {
@Test
fun `unlockVault with initializeCrypto failure should return GenericError`() = runTest {
val userId = "userId"
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
@@ -851,7 +958,15 @@ class VaultRepositoryTest {
),
)
} returns Throwable("Fail").asFailure()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
val result = vaultRepository.unlockVault(
userId = userId,
masterPassword = masterPassword,
kdf = kdf,
email = email,
@@ -859,7 +974,14 @@ class VaultRepositoryTest {
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
assertEquals(VaultUnlockResult.GenericError, result)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
),
vaultRepository.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
@@ -876,6 +998,7 @@ class VaultRepositoryTest {
@Test
fun `unlockVault with initializeCrypto awaiting should block calls to sync`() = runTest {
val userId = "userId"
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
@@ -898,6 +1021,7 @@ class VaultRepositoryTest {
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVault(
userId = userId,
masterPassword = masterPassword,
kdf = kdf,
email = email,

View File

@@ -4,6 +4,8 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
@@ -16,6 +18,7 @@ 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
@@ -23,6 +26,9 @@ import org.junit.jupiter.api.Test
class VaultUnlockViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val authRepository = mockk<AuthRepository>() {
every { userStateFlow } returns MutableStateFlow(DEFAULT_USER_STATE)
}
private val vaultRepository = mockk<VaultRepository>()
@Test
@@ -191,17 +197,39 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
vaultRepo: VaultRepository = vaultRepository,
): VaultUnlockViewModel = VaultUnlockViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
authRepository = authRepository,
vaultRepo = vaultRepo,
environmentRepo = environmentRepo,
)
}
private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
accountSummaries = emptyList(),
avatarColorString = "0000FF",
email = "bit@bitwarden.com",
initials = "BW",
accountSummaries = listOf(
AccountSummary(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
status = AccountSummary.Status.ACTIVE,
),
),
avatarColorString = "#aa00aa",
email = "active@bitwarden.com",
initials = "AU",
dialog = null,
environmentUrl = Environment.Us.label,
passwordInput = "",
)
private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
isVaultUnlocked = true,
),
),
)

View File

@@ -72,6 +72,15 @@ class RootNavScreenTest : BaseComposeTest() {
}
assertTrue(isSplashScreenRemoved)
// Make sure navigating to vault locked works as expected:
rootNavStateFlow.value = RootNavState.VaultLocked
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "vault_unlock",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to vault unlocked works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlocked
composeTestRule.runOnIdle {

View File

@@ -1,64 +1,69 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.lifecycle.SavedStateHandle
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.AuthState.Authenticated
import com.x8bit.bitwarden.data.auth.repository.model.AuthState.Unauthenticated
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
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 RootNavViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be the state in savedStateHandle`() {
val authRepository = mockk<AuthRepository> {
every { this@mockk.authStateFlow } returns MutableStateFlow(mockk<Authenticated>())
}
val handle = SavedStateHandle(mapOf(("nav_state" to RootNavState.VaultUnlocked)))
val viewModel = RootNavViewModel(
authRepository = authRepository,
savedStateHandle = handle,
)
assertEquals(RootNavState.VaultUnlocked, viewModel.stateFlow.value)
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val authRepository = mockk<AuthRepository>() {
every { userStateFlow } returns mutableUserStateFlow
}
@Test
fun `when auth state is Uninitialized nav state should be Splash`() {
val viewModel = RootNavViewModel(
authRepository = mockk {
every { this@mockk.authStateFlow } returns MutableStateFlow(AuthState.Uninitialized)
},
savedStateHandle = SavedStateHandle(),
)
assertEquals(RootNavState.Splash, viewModel.stateFlow.value)
}
@Test
fun `when auth state is Authenticated nav state should be VaultUnlocked`() {
val authRepository = mockk<AuthRepository> {
every { this@mockk.authStateFlow } returns MutableStateFlow(mockk<Authenticated>())
}
val viewModel = RootNavViewModel(
authRepository = authRepository,
savedStateHandle = SavedStateHandle(),
)
assertEquals(RootNavState.VaultUnlocked, viewModel.stateFlow.value)
}
@Test
fun `when auth state is Unauthenticated nav state should be Auth`() = runTest {
val viewModel = RootNavViewModel(
authRepository = mockk {
every { this@mockk.authStateFlow } returns MutableStateFlow(Unauthenticated)
},
savedStateHandle = SavedStateHandle(),
)
fun `when there are no accounts the nav state should be Auth`() {
mutableUserStateFlow.tryEmit(null)
val viewModel = createViewModel()
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
}
@Test
fun `when the active user has an unlocked vault the nav state should be VaultUnlocked`() {
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
isVaultUnlocked = true,
),
),
),
)
val viewModel = createViewModel()
assertEquals(RootNavState.VaultUnlocked, viewModel.stateFlow.value)
}
@Test
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
isVaultUnlocked = false,
),
),
),
)
val viewModel = createViewModel()
assertEquals(RootNavState.VaultLocked, viewModel.stateFlow.value)
}
private fun createViewModel(): RootNavViewModel =
RootNavViewModel(
authRepository = authRepository,
)
}

View File

@@ -1,7 +1,8 @@
package com.x8bit.bitwarden.ui.vault.feature.vault
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
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
@@ -19,9 +20,17 @@ import org.junit.jupiter.api.Test
class VaultViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow =
MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val mutableVaultDataStateFlow =
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
private val authRepository: AuthRepository =
mockk {
every { userStateFlow } returns mutableUserStateFlow
}
private val vaultRepository: VaultRepository =
mockk {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
@@ -29,19 +38,54 @@ class VaultViewModelTest : BaseViewModelTest() {
}
@Test
fun `initial state should be correct when not set`() {
fun `initial state should be correct`() {
val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `initial state should be correct when set`() {
val state = DEFAULT_STATE.copy(
initials = "WB",
avatarColorString = "00FF00",
fun `UserState updates with a null value should do nothing`() {
val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
mutableUserStateFlow.value = null
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
}
@Test
fun `UserState updates with a non-null value update the account information in the state`() {
val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "Other User",
email = "active@bitwarden.com",
avatarColorHex = "#00aaaa",
isVaultUnlocked = true,
),
),
)
assertEquals(
DEFAULT_STATE.copy(
avatarColorString = "#00aaaa",
initials = "OU",
accountSummaries = listOf(
AccountSummary(
userId = "activeUserId",
name = "Other User",
email = "active@bitwarden.com",
avatarColorHex = "#00aaaa",
status = AccountSummary.Status.ACTIVE,
),
),
),
viewModel.stateFlow.value,
)
val viewModel = createViewModel(state = state)
assertEquals(state, viewModel.stateFlow.value)
}
@Test
@@ -294,23 +338,41 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
private fun createViewModel(
state: VaultState? = DEFAULT_STATE,
): VaultViewModel = VaultViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
vaultRepository = vaultRepository,
)
private fun createViewModel(): VaultViewModel =
VaultViewModel(
authRepository = authRepository,
vaultRepository = vaultRepository,
)
}
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 val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
isVaultUnlocked = true,
),
),
)
private fun createMockVaultState(viewState: VaultState.ViewState): VaultState =
VaultState(
avatarColorString = DEFAULT_COLOR_STRING,
initials = DEFAULE_INITIALS,
accountSummaries = emptyList(),
avatarColorString = "#aa00aa",
initials = "AU",
accountSummaries = listOf(
AccountSummary(
userId = "activeUserId",
name = "Active User",
email = "active@bitwarden.com",
avatarColorHex = "#aa00aa",
status = AccountSummary.Status.ACTIVE,
),
),
viewState = viewState,
)

View File

@@ -0,0 +1,154 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class UserStateExtensionsTest {
@Test
fun `toAccountSummaries should return the correct list`() {
assertEquals(
listOf(
AccountSummary(
userId = "activeUserId",
name = "activeName",
email = "activeEmail",
avatarColorHex = "activeAvatarColorHex",
status = AccountSummary.Status.ACTIVE,
),
AccountSummary(
userId = "lockedUserId",
name = "lockedName",
email = "lockedEmail",
avatarColorHex = "lockedAvatarColorHex",
status = AccountSummary.Status.LOCKED,
),
AccountSummary(
userId = "unlockedUserId",
name = "unlockedName",
email = "unlockedEmail",
avatarColorHex = "unlockedAvatarColorHex",
status = AccountSummary.Status.UNLOCKED,
),
),
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "activeName",
email = "activeEmail",
avatarColorHex = "activeAvatarColorHex",
isVaultUnlocked = true,
),
UserState.Account(
userId = "lockedUserId",
name = "lockedName",
email = "lockedEmail",
avatarColorHex = "lockedAvatarColorHex",
isVaultUnlocked = false,
),
UserState.Account(
userId = "unlockedUserId",
name = "unlockedName",
email = "unlockedEmail",
avatarColorHex = "unlockedAvatarColorHex",
isVaultUnlocked = true,
),
),
)
.toAccountSummaries(),
)
}
@Test
fun `toAccountSummary for an active account should return an active AccountSummary`() {
assertEquals(
AccountSummary(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
status = AccountSummary.Status.ACTIVE,
),
UserState.Account(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
isVaultUnlocked = true,
)
.toAccountSummary(isActive = true),
)
}
@Test
fun `toAccountSummary for an locked account should return a locked AccountSummary`() {
assertEquals(
AccountSummary(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
status = AccountSummary.Status.LOCKED,
),
UserState.Account(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
isVaultUnlocked = false,
)
.toAccountSummary(isActive = false),
)
}
@Test
fun `toAccountSummary for a unlocked account should return a locked AccountSummary`() {
assertEquals(
AccountSummary(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
status = AccountSummary.Status.UNLOCKED,
),
UserState.Account(
userId = "userId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
isVaultUnlocked = true,
)
.toAccountSummary(isActive = false),
)
}
@Suppress("MaxLineLength")
@Test
fun `toActiveAccountSummary should return an active AccountSummary`() {
assertEquals(
AccountSummary(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
status = AccountSummary.Status.ACTIVE,
),
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarColorHex",
isVaultUnlocked = true,
),
),
)
.toActiveAccountSummary(),
)
}
}