Decouple unlock and sync (#264)

This commit is contained in:
David Perez 2023-11-21 11:17:46 -06:00 committed by Álison Fernandes
parent feba8595b3
commit e8ae15ddc3
7 changed files with 436 additions and 172 deletions

View file

@ -121,33 +121,40 @@ class AuthRepositoryImpl constructor(
}
.fold(
onFailure = { LoginResult.Error(errorMessage = null) },
onSuccess = {
when (it) {
is CaptchaRequired -> LoginResult.CaptchaRequired(it.captchaKey)
onSuccess = { loginResponse ->
when (loginResponse) {
is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey)
is Success -> {
authDiskSource.userState = it
.toUserState(
previousUserState = authDiskSource.userState,
environmentUrlData = environmentRepository
.environment
.environmentUrlData,
)
.also { userState ->
authDiskSource.storeUserKey(
userId = userState.activeUserId,
userKey = it.key,
)
authDiskSource.storePrivateKey(
userId = userState.activeUserId,
privateKey = it.privateKey,
)
}
vaultRepository.unlockVaultAndSync(masterPassword = password)
val userStateJson = loginResponse.toUserState(
previousUserState = authDiskSource.userState,
environmentUrlData = environmentRepository
.environment
.environmentUrlData,
)
vaultRepository.unlockVault(
email = userStateJson.activeAccount.profile.email,
kdf = userStateJson.activeAccount.profile.toSdkParams(),
userKey = loginResponse.key,
privateKey = loginResponse.privateKey,
// TODO use actual organization keys BIT-1091
organizationalKeys = emptyMap(),
masterPassword = password,
)
authDiskSource.userState = userStateJson
authDiskSource.storeUserKey(
userId = userStateJson.activeUserId,
userKey = loginResponse.key,
)
authDiskSource.storePrivateKey(
userId = userStateJson.activeUserId,
privateKey = loginResponse.privateKey,
)
vaultRepository.sync()
LoginResult.Success
}
is GetTokenResponseJson.Invalid -> {
LoginResult.Error(errorMessage = it.errorModel.errorMessage)
LoginResult.Error(errorMessage = loginResponse.errorModel.errorMessage)
}
}
},

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository
import com.bitwarden.core.CipherView
import com.bitwarden.core.FolderView
import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
@ -46,7 +47,20 @@ interface VaultRepository {
fun getVaultFolderStateFlow(folderId: String): StateFlow<DataState<FolderView?>>
/**
* Attempt to initialize crypto and sync the vault data.
* Attempt to unlock the vault and sync the vault data for the currently active user.
*/
suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult
suspend fun unlockVaultAndSyncForCurrentUser(masterPassword: String): VaultUnlockResult
/**
* Attempt to unlock the vault with the specified user information.
*/
@Suppress("LongParameterList")
suspend fun unlockVault(
masterPassword: String,
email: String,
kdf: Kdf,
userKey: String,
privateKey: String,
organizationalKeys: Map<String, String>,
): VaultUnlockResult
}

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository
import com.bitwarden.core.CipherView
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitCryptoRequest
import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
@ -31,7 +32,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -146,18 +146,62 @@ class VaultRepositoryImpl constructor(
initialValue = DataState.Loading,
)
override suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult {
return flow {
willSyncAfterUnlock = true
emit(initializeCrypto(masterPassword = masterPassword))
}
.onEach {
willSyncAfterUnlock = false
if (it is VaultUnlockResult.Success) sync()
@Suppress("ReturnCount")
override suspend fun unlockVaultAndSyncForCurrentUser(
masterPassword: String,
): VaultUnlockResult {
val userState = authDiskSource.userState
?: return VaultUnlockResult.InvalidStateError
val userKey = authDiskSource.getUserKey(userId = userState.activeUserId)
?: return VaultUnlockResult.InvalidStateError
val privateKey = authDiskSource.getPrivateKey(userId = userState.activeUserId)
?: return VaultUnlockResult.InvalidStateError
return unlockVault(
masterPassword = masterPassword,
email = userState.activeAccount.profile.email,
kdf = userState.activeAccount.profile.toSdkParams(),
userKey = userKey,
privateKey = privateKey,
// TODO use actual organization keys BIT-1091
organizationalKeys = emptyMap(),
)
.also {
if (it is VaultUnlockResult.Success) {
sync()
}
}
}
override suspend fun unlockVault(
masterPassword: String,
email: String,
kdf: Kdf,
userKey: String,
privateKey: String,
organizationalKeys: Map<String, String>,
): VaultUnlockResult =
flow {
willSyncAfterUnlock = true
emit(
vaultSdkSource
.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
.fold(
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { it.toVaultUnlockResult() },
),
)
}
.onCompletion { willSyncAfterUnlock = false }
.first()
}
private fun storeUserKeyAndPrivateKey(
userKey: String?,
@ -177,32 +221,6 @@ class VaultRepositoryImpl constructor(
}
}
@Suppress("ReturnCount")
private suspend fun initializeCrypto(masterPassword: String): VaultUnlockResult {
val userState = authDiskSource.userState
?: return VaultUnlockResult.InvalidStateError
val userKey = authDiskSource.getUserKey(userId = userState.activeUserId)
?: return VaultUnlockResult.InvalidStateError
val privateKey = authDiskSource.getPrivateKey(userId = userState.activeUserId)
?: return VaultUnlockResult.InvalidStateError
return vaultSdkSource
.initializeCrypto(
request = InitCryptoRequest(
kdfParams = userState.activeAccount.profile.toSdkParams(),
email = userState.activeAccount.profile.email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
// TODO use actual organization keys BIT-1091
organizationKeys = mapOf(),
),
)
.fold(
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { it.toVaultUnlockResult() },
)
}
private suspend fun decryptSendsAndUpdateSendDataState(sendList: List<SyncResponseJson.Send>?) {
val newState = vaultSdkSource
.decryptSendList(

View file

@ -92,7 +92,7 @@ class VaultUnlockViewModel @Inject constructor(
private fun handleUnlockClick() {
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
viewModelScope.launch {
val vaultUnlockResult = vaultRepo.unlockVaultAndSync(
val vaultUnlockResult = vaultRepo.unlockVaultAndSyncForCurrentUser(
mutableStateFlow.value.passwordInput,
)
sendAction(VaultUnlockAction.Internal.ReceiveVaultUnlockResult(vaultUnlockResult))

View file

@ -42,8 +42,10 @@ import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
@ -269,7 +271,7 @@ class AuthRepositoryTest {
@Test
@Suppress("MaxLineLength")
fun `login get token succeeds should return Success, update AuthState, update stored keys, and unlockVaultAndSync`() =
fun `login get token succeeds should return Success, unlockVault, update AuthState, update stored keys, and sync`() =
runTest {
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
@ -284,8 +286,16 @@ class AuthRepositoryTest {
}
.returns(Result.success(successResponse))
coEvery {
vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD)
vaultRepository.unlockVault(
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 = null,
@ -310,9 +320,15 @@ class AuthRepositoryTest {
passwordHash = PASSWORD_HASH,
captchaToken = null,
)
}
coVerify {
vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD)
vaultRepository.unlockVault(
email = EMAIL,
kdf = ACCOUNT_1.profile.toSdkParams(),
userKey = successResponse.key,
privateKey = successResponse.privateKey,
organizationalKeys = emptyMap(),
masterPassword = PASSWORD,
)
vaultRepository.sync()
}
}
@ -679,7 +695,7 @@ class AuthRepositoryTest {
}
@Test
fun `logout for single account should clear the access toke and stored keys`() = runTest {
fun `logout for single account should clear the access token and stored keys`() = runTest {
// First login:
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
coEvery {
@ -693,8 +709,16 @@ class AuthRepositoryTest {
)
} returns Result.success(successResponse)
coEvery {
vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD)
vaultRepository.unlockVault(
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 = null,
@ -745,8 +769,16 @@ class AuthRepositoryTest {
)
} returns Result.success(successResponse)
coEvery {
vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD)
vaultRepository.unlockVault(
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,

View file

@ -8,6 +8,7 @@ import com.bitwarden.core.Kdf
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.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -460,8 +461,9 @@ class VaultRepositoryTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSync with initializeCrypto Success should sync and return Success`() =
fun `unlockVaultAndSyncForCurrentUser with unlockVault Success should sync and return Success`() =
runTest {
coEvery {
syncService.sync()
@ -497,7 +499,9 @@ class VaultRepositoryTest {
)
} returns Result.success(InitializeCryptoResult.Success)
val result = vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(
masterPassword = "mockPassword-1",
)
assertEquals(
VaultUnlockResult.Success,
@ -507,100 +511,102 @@ class VaultRepositoryTest {
}
@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()
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
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(),
),
fun `sync should be able to be called after unlockVaultAndSyncForCurrentUser 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()
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.storePrivateKey(
userId = "mockId-1",
privateKey = "mockPrivateKey-1",
)
} just awaits
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
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(),
),
)
} just awaits
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "mockPassword-1")
}
coVerify(exactly = 0) { syncService.sync() }
scope.cancel()
vaultRepository.sync()
coVerify(exactly = 1) { syncService.sync() }
}
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 = "mockId-1",
privateKey = "mockPrivateKey-1",
)
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
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(),
),
fun `sync should not be able to be called while unlockVaultAndSyncForCurrentUser 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 = "mockId-1",
privateKey = "mockPrivateKey-1",
)
} just awaits
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
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(),
),
)
} just awaits
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVaultAndSyncForCurrentUser(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() }
}
// 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`() =
fun `unlockVaultAndSyncForCurrentUser with unlockVault failure should return GenericError`() =
runTest {
coEvery {
syncService.sync()
@ -633,7 +639,9 @@ class VaultRepositoryTest {
)
} returns Result.failure(IllegalStateException())
val result = vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(
masterPassword = "mockPassword-1",
)
assertEquals(
VaultUnlockResult.GenericError,
@ -643,7 +651,7 @@ class VaultRepositoryTest {
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSync with initializeCrypto AuthenticationError should return AuthenticationError`() =
fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError should return AuthenticationError`() =
runTest {
coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1))
coEvery {
@ -674,19 +682,20 @@ class VaultRepositoryTest {
)
} returns Result.success(InitializeCryptoResult.AuthenticationError)
val result = vaultRepository.unlockVaultAndSync(masterPassword = "")
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
assertEquals(
VaultUnlockResult.AuthenticationError,
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSync with missing user state should return InvalidStateError `() =
fun `unlockVaultAndSyncForCurrentUser with missing user state should return InvalidStateError `() =
runTest {
fakeAuthDiskSource.userState = null
val result = vaultRepository.unlockVaultAndSync(masterPassword = "")
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
assertEquals(
VaultUnlockResult.InvalidStateError,
@ -694,10 +703,11 @@ class VaultRepositoryTest {
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSync with missing user key should return InvalidStateError `() =
fun `unlockVaultAndSyncForCurrentUser with missing user key should return InvalidStateError `() =
runTest {
val result = vaultRepository.unlockVaultAndSync(masterPassword = "")
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = null,
@ -713,10 +723,11 @@ class VaultRepositoryTest {
)
}
@Suppress("MaxLineLength")
@Test
fun `unlockVaultAndSync with missing private key should return InvalidStateError `() =
fun `unlockVaultAndSyncForCurrentUser with missing private key should return InvalidStateError `() =
runTest {
val result = vaultRepository.unlockVaultAndSync(masterPassword = "")
val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "")
fakeAuthDiskSource.storeUserKey(
userId = "mockId-1",
userKey = "mockKey-1",
@ -732,6 +743,188 @@ class VaultRepositoryTest {
)
}
@Test
fun `unlockVault with initializeCrypto success should return Success`() = runTest {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationalKeys = emptyMap<String, String>()
coEvery {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
} returns InitializeCryptoResult.Success.asSuccess()
val result = vaultRepository.unlockVault(
masterPassword = masterPassword,
kdf = kdf,
email = email,
userKey = userKey,
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
assertEquals(VaultUnlockResult.Success, result)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVault with initializeCrypto authentication failure should return AuthenticationError`() =
runTest {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationalKeys = emptyMap<String, String>()
coEvery {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
val result = vaultRepository.unlockVault(
masterPassword = masterPassword,
kdf = kdf,
email = email,
userKey = userKey,
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
assertEquals(VaultUnlockResult.AuthenticationError, result)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
}
}
@Test
fun `unlockVault with initializeCrypto failure should return GenericError`() = runTest {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationalKeys = emptyMap<String, String>()
coEvery {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
} returns Throwable("Fail").asFailure()
val result = vaultRepository.unlockVault(
masterPassword = masterPassword,
kdf = kdf,
email = email,
userKey = userKey,
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
assertEquals(VaultUnlockResult.GenericError, result)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
}
}
@Test
fun `unlockVault with initializeCrypto awaiting should block calls to sync`() = runTest {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationalKeys = emptyMap<String, String>()
coEvery {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
} just awaits
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
vaultRepository.unlockVault(
masterPassword = masterPassword,
kdf = kdf,
email = email,
userKey = userKey,
privateKey = privateKey,
organizationalKeys = organizationalKeys,
)
}
// Does nothing because we are blocking
vaultRepository.sync()
scope.cancel()
coVerify(exactly = 0) { syncService.sync() }
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
request = InitCryptoRequest(
kdfParams = kdf,
email = email,
password = masterPassword,
userKey = userKey,
privateKey = privateKey,
organizationKeys = organizationalKeys,
),
)
}
}
@Test
fun `clearUnlockedData should update the vaultDataStateFlow to Loading`() =
runTest {

View file

@ -103,7 +103,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
} returns VaultUnlockResult.AuthenticationError
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -116,7 +116,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
}
}
@ -126,7 +126,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
} returns VaultUnlockResult.GenericError
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -139,7 +139,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
}
}
@ -149,7 +149,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
} returns VaultUnlockResult.InvalidStateError
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -162,7 +162,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
}
}
@ -172,7 +172,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(passwordInput = password)
val viewModel = createViewModel(state = initialState)
coEvery {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
} returns VaultUnlockResult.Success
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
@ -181,7 +181,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify {
vaultRepository.unlockVaultAndSync(password)
vaultRepository.unlockVaultAndSyncForCurrentUser(password)
}
}