BIT-1110: Allow account addition via the account switcher (#305)

This commit is contained in:
Brian Yencho
2023-11-30 14:03:46 -06:00
committed by GitHub
parent 004240badc
commit 193ecd1495
13 changed files with 196 additions and 37 deletions

View File

@@ -29,6 +29,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
@@ -148,6 +149,7 @@ class AuthRepositoryTest {
assertEquals(
SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
specialCircumstance = null,
),
repository.userStateFlow.value,
)
@@ -156,6 +158,7 @@ class AuthRepositoryTest {
assertEquals(
MULTI_USER_STATE.toUserState(
vaultState = VAULT_STATE,
specialCircumstance = null,
),
repository.userStateFlow.value,
)
@@ -165,6 +168,7 @@ class AuthRepositoryTest {
assertEquals(
MULTI_USER_STATE.toUserState(
vaultState = emptyVaultState,
specialCircumstance = null,
),
repository.userStateFlow.value,
)
@@ -185,6 +189,31 @@ class AuthRepositoryTest {
assertNull(repository.rememberedEmailAddress)
}
@Test
fun `specialCircumstance update should trigger a change in UserState`() {
// Populate the initial UserState
assertNull(repository.specialCircumstance)
val initialUserState = SINGLE_USER_STATE_1.toUserState(
vaultState = VAULT_STATE,
specialCircumstance = null,
)
mutableVaultStateFlow.value = VAULT_STATE
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
assertEquals(
initialUserState,
repository.userStateFlow.value,
)
repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
assertEquals(
initialUserState.copy(
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
),
repository.userStateFlow.value,
)
}
@Test
fun `delete account fails if not logged in`() = runTest {
val masterPassword = "hello world"
@@ -439,6 +468,94 @@ class AuthRepositoryTest {
)
vaultRepository.sync()
}
assertEquals(
SINGLE_USER_STATE_1,
fakeAuthDiskSource.userState,
)
assertNull(repository.specialCircumstance)
verify(exactly = 0) { vaultRepository.lockVaultIfNecessary(any()) }
verify { vaultRepository.clearUnlockedData() }
}
@Suppress("MaxLineLength")
@Test
fun `login get token succeeds when there is an existing user should switch to the new logged in user and lock the old user's vault`() =
runTest {
// Ensure the initial state for User 2 with a account addition
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
repository.specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition
// Set up login for User 1
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
accountsService.preLogin(email = EMAIL)
} returns Result.success(PRE_LOGIN_SUCCESS)
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
}
.returns(Result.success(successResponse))
coEvery {
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationalKeys = emptyMap(),
masterPassword = PASSWORD,
)
} returns VaultUnlockResult.Success
coEvery { vaultRepository.sync() } just runs
every {
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
previousUserState = SINGLE_USER_STATE_2,
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
)
} returns MULTI_USER_STATE
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
assertEquals(LoginResult.Success, result)
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
coVerify { accountsService.preLogin(email = EMAIL) }
fakeAuthDiskSource.assertPrivateKey(
userId = USER_ID_1,
privateKey = "privateKey",
)
fakeAuthDiskSource.assertUserKey(
userId = USER_ID_1,
userKey = "key",
)
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
vaultRepository.unlockVault(
userId = USER_ID_1,
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationalKeys = emptyMap(),
masterPassword = PASSWORD,
)
vaultRepository.sync()
}
assertEquals(
MULTI_USER_STATE,
fakeAuthDiskSource.userState,
)
assertNull(repository.specialCircumstance)
verify { vaultRepository.lockVaultIfNecessary(userId = USER_ID_2) }
verify { vaultRepository.clearUnlockedData() }
}
@Test

View File

@@ -121,6 +121,7 @@ class UserStateJsonExtensionsTest {
vaultState = VaultState(
unlockedVaultUserIds = setOf("activeUserId"),
),
specialCircumstance = null,
),
)
}
@@ -141,6 +142,7 @@ class UserStateJsonExtensionsTest {
isVaultUnlocked = false,
),
),
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
),
UserStateJson(
activeUserId = "activeUserId",
@@ -162,6 +164,7 @@ class UserStateJsonExtensionsTest {
vaultState = VaultState(
unlockedVaultUserIds = emptySet(),
),
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
),
)
}

View File

@@ -6,6 +6,7 @@ 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.auth.repository.model.UserState.SpecialCircumstance
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
@@ -32,6 +33,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val authRepository = mockk<AuthRepository>() {
every { userStateFlow } returns mutableUserStateFlow
every { specialCircumstance } returns null
every { specialCircumstance = any() } just runs
every { logout() } just runs
}
private val vaultRepository = mockk<VaultRepository>()
@@ -120,12 +123,13 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `on AddAccountClick should emit NavigateToLoginScreen`() = runTest {
fun `on AddAccountClick should update the SpecialCircumstance of the AuthRepository to PendingAccountAddition`() {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultUnlockAction.AddAccountClick)
assertEquals(VaultUnlockEvent.NavigateToLoginScreen, awaitItem())
viewModel.trySendAction(VaultUnlockAction.AddAccountClick)
verify {
authRepository.specialCircumstance = SpecialCircumstance.PendingAccountAddition
}
}

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
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.auth.repository.model.UserState.SpecialCircumstance
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
@@ -12,7 +13,10 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
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 kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
@@ -29,6 +33,8 @@ class VaultViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository =
mockk {
every { userStateFlow } returns mutableUserStateFlow
every { specialCircumstance } returns null
every { specialCircumstance = any() } just runs
}
private val vaultRepository: VaultRepository =
@@ -134,12 +140,13 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `on AddAccountClick should emit NavigateToLoginScreen`() = runTest {
fun `on AddAccountClick should update the SpecialCircumstance of the AuthRepository to PendingAccountAddition`() {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAction.AddAccountClick)
assertEquals(VaultEvent.NavigateToLoginScreen, awaitItem())
viewModel.trySendAction(VaultAction.AddAccountClick)
verify {
authRepository.specialCircumstance = SpecialCircumstance.PendingAccountAddition
}
}