mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 20:09:59 +03:00
Decouple unlock and sync (#264)
This commit is contained in:
parent
feba8595b3
commit
e8ae15ddc3
7 changed files with 436 additions and 172 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue