mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
Refactor cipher logic into CipherManager (#5898)
This commit is contained in:
parent
a39f83349f
commit
2756bd9fde
@ -5,6 +5,7 @@ import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.AttachmentJsonResponse
|
||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
@ -17,7 +18,10 @@ import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.EncryptionContext
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult
|
||||
@ -34,13 +38,18 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toNetworkAttachmentRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* The default implementation of the [CipherManager].
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
class CipherManagerImpl(
|
||||
private val fileManager: FileManager,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
@ -49,9 +58,24 @@ class CipherManagerImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val clock: Clock,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
) : CipherManager {
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
init {
|
||||
pushManager
|
||||
.syncCipherDeleteFlow
|
||||
.onEach(::deleteCipher)
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncCipherUpsertFlow
|
||||
.onEach(::syncCipherIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateCipherResult.Error(
|
||||
@ -641,4 +665,79 @@ class CipherManagerImpl(
|
||||
}
|
||||
return migratedCipherView.asSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the cipher specified by [syncCipherDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteCipher(syncCipherDeleteData: SyncCipherDeleteData) {
|
||||
vaultDiskSource.deleteCipher(
|
||||
userId = syncCipherDeleteData.userId,
|
||||
cipherId = syncCipherDeleteData.cipherId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual cipher contained in [syncCipherUpsertData] to disk if certain criteria
|
||||
* are met. If the resource cannot be found cloud-side, and it was updated, delete it from disk
|
||||
* for now.
|
||||
*/
|
||||
private suspend fun syncCipherIfNecessary(syncCipherUpsertData: SyncCipherUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val cipherId = syncCipherUpsertData.cipherId
|
||||
val organizationId = syncCipherUpsertData.organizationId
|
||||
val collectionIds = syncCipherUpsertData.collectionIds
|
||||
val revisionDate = syncCipherUpsertData.revisionDate
|
||||
val isUpdate = syncCipherUpsertData.isUpdate
|
||||
|
||||
// Return if local cipher is more recent
|
||||
val localCipher = vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
if (localCipher != null &&
|
||||
localCipher.revisionDate.toEpochSecond() > revisionDate.toEpochSecond()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldUpdate: Boolean
|
||||
val shouldCheckCollections: Boolean
|
||||
when {
|
||||
isUpdate -> {
|
||||
shouldUpdate = localCipher != null
|
||||
shouldCheckCollections = true
|
||||
}
|
||||
|
||||
collectionIds == null || organizationId == null -> {
|
||||
shouldUpdate = localCipher == null
|
||||
shouldCheckCollections = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
shouldUpdate = false
|
||||
shouldCheckCollections = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdate && shouldCheckCollections && organizationId != null) {
|
||||
// Check if there are any collections in common
|
||||
shouldUpdate = vaultDiskSource
|
||||
.getCollections(userId = userId)
|
||||
.first()
|
||||
.any { collectionIds?.contains(it.id) == true }
|
||||
}
|
||||
|
||||
if (!shouldUpdate) return
|
||||
|
||||
ciphersService
|
||||
.getCipher(cipherId = cipherId)
|
||||
.fold(
|
||||
onSuccess = { vaultDiskSource.saveCipher(userId = userId, cipher = it) },
|
||||
onFailure = {
|
||||
// Delete any updates if it's missing from the server
|
||||
val httpException = it as? HttpException
|
||||
@Suppress("MagicNumber")
|
||||
if (httpException?.code() == 404 && isUpdate) {
|
||||
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,8 @@ object VaultManagerModule {
|
||||
fileManager: FileManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pushManager: PushManager,
|
||||
): CipherManager = CipherManagerImpl(
|
||||
fileManager = fileManager,
|
||||
authDiskSource = authDiskSource,
|
||||
@ -65,6 +67,8 @@ object VaultManagerModule {
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
pushManager = pushManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@ -15,7 +15,6 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.network.model.UpdateFolderResponseJson
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.util.isNoConnectionError
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
@ -35,8 +34,6 @@ import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
@ -102,7 +99,6 @@ import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.security.GeneralSecurityException
|
||||
import java.time.Clock
|
||||
@ -120,7 +116,6 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
class VaultRepositoryImpl(
|
||||
private val ciphersService: CiphersService,
|
||||
private val folderService: FolderService,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
@ -286,16 +281,6 @@ class VaultRepositoryImpl(
|
||||
.onEach { sync(forced = false) }
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.syncCipherDeleteFlow
|
||||
.onEach(::deleteCipher)
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.syncCipherUpsertFlow
|
||||
.onEach(::syncCipherIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
|
||||
pushManager
|
||||
.syncFolderDeleteFlow
|
||||
.onEach(::deleteFolder)
|
||||
@ -1065,82 +1050,6 @@ class VaultRepositoryImpl(
|
||||
?: DataState.Loading
|
||||
|
||||
//region Push notification helpers
|
||||
/**
|
||||
* Deletes the cipher specified by [syncCipherDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteCipher(syncCipherDeleteData: SyncCipherDeleteData) {
|
||||
vaultDiskSource.deleteCipher(
|
||||
userId = syncCipherDeleteData.userId,
|
||||
cipherId = syncCipherDeleteData.cipherId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual cipher contained in [syncCipherUpsertData] to disk if certain criteria
|
||||
* are met. If the resource cannot be found cloud-side, and it was updated, delete it from disk
|
||||
* for now.
|
||||
*/
|
||||
private suspend fun syncCipherIfNecessary(syncCipherUpsertData: SyncCipherUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val cipherId = syncCipherUpsertData.cipherId
|
||||
val organizationId = syncCipherUpsertData.organizationId
|
||||
val collectionIds = syncCipherUpsertData.collectionIds
|
||||
val revisionDate = syncCipherUpsertData.revisionDate
|
||||
val isUpdate = syncCipherUpsertData.isUpdate
|
||||
|
||||
val localCipher = vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
|
||||
// Return if local cipher is more recent
|
||||
if (localCipher != null &&
|
||||
localCipher.revisionDate.toEpochSecond() > revisionDate.toEpochSecond()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldUpdate: Boolean
|
||||
val shouldCheckCollections: Boolean
|
||||
|
||||
when {
|
||||
isUpdate -> {
|
||||
shouldUpdate = localCipher != null
|
||||
shouldCheckCollections = true
|
||||
}
|
||||
|
||||
collectionIds == null || organizationId == null -> {
|
||||
shouldUpdate = localCipher == null
|
||||
shouldCheckCollections = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
shouldUpdate = false
|
||||
shouldCheckCollections = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdate && shouldCheckCollections && organizationId != null) {
|
||||
// Check if there are any collections in common
|
||||
shouldUpdate = vaultDiskSource
|
||||
.getCollections(userId = userId)
|
||||
.first()
|
||||
.any { collectionIds?.contains(it.id) == true }
|
||||
}
|
||||
|
||||
if (!shouldUpdate) return
|
||||
|
||||
ciphersService
|
||||
.getCipher(cipherId)
|
||||
.fold(
|
||||
onSuccess = { vaultDiskSource.saveCipher(userId, it) },
|
||||
onFailure = {
|
||||
// Delete any updates if it's missing from the server
|
||||
val httpException = it as? HttpException
|
||||
@Suppress("MagicNumber")
|
||||
if (httpException?.code() == 404 && isUpdate) {
|
||||
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the folder specified by [syncFolderDeleteData] from disk.
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository.di
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
@ -34,7 +33,6 @@ object VaultRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVaultRepository(
|
||||
ciphersService: CiphersService,
|
||||
folderService: FolderService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
@ -51,7 +49,6 @@ object VaultRepositoryModule {
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
|
||||
@ -2,8 +2,10 @@ package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.network.model.AttachmentJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
@ -15,6 +17,7 @@ import com.bitwarden.network.model.createMockAttachment
|
||||
import com.bitwarden.network.model.createMockAttachmentResponse
|
||||
import com.bitwarden.network.model.createMockCipher
|
||||
import com.bitwarden.network.model.createMockCipherJsonRequest
|
||||
import com.bitwarden.network.model.createMockCollection
|
||||
import com.bitwarden.network.model.createMockLogin
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.vault.Attachment
|
||||
@ -27,7 +30,10 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
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.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAttachmentView
|
||||
@ -60,16 +66,19 @@ import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkConstructor
|
||||
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
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class CipherManagerTest {
|
||||
@ -88,6 +97,12 @@ class CipherManagerTest {
|
||||
private val reviewPromptManager: ReviewPromptManager = mockk {
|
||||
every { registerAddCipherAction() } just runs
|
||||
}
|
||||
private val mutableSyncCipherDeleteFlow = bufferedMutableSharedFlow<SyncCipherDeleteData>()
|
||||
private val mutableSyncCipherUpsertFlow = bufferedMutableSharedFlow<SyncCipherUpsertData>()
|
||||
private val pushManager: PushManager = mockk {
|
||||
every { syncCipherDeleteFlow } returns mutableSyncCipherDeleteFlow
|
||||
every { syncCipherUpsertFlow } returns mutableSyncCipherUpsertFlow
|
||||
}
|
||||
|
||||
private val cipherManager: CipherManager = CipherManagerImpl(
|
||||
ciphersService = ciphersService,
|
||||
@ -97,6 +112,8 @@ class CipherManagerTest {
|
||||
fileManager = fileManager,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
pushManager = pushManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
@ -2353,6 +2370,348 @@ class CipherManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncCipherDeleteFlow should delete cipher from disk`() {
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
|
||||
coEvery { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } just runs
|
||||
|
||||
mutableSyncCipherDeleteFlow.tryEmit(
|
||||
SyncCipherDeleteData(userId = userId, cipherId = cipherId),
|
||||
)
|
||||
|
||||
coVerify { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow create with local cipher with no common collections should do nothing`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
ciphersService.getCipher(cipherId = any())
|
||||
vaultDiskSource.saveCipher(userId = any(), cipher = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow create with local cipher, and with common collections, should make a request and save cipher to disk`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val collection = createMockCollection(number = number)
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
coEvery {
|
||||
vaultDiskSource.getCollections(userId = userId)
|
||||
} returns MutableStateFlow(listOf(collection))
|
||||
coEvery {
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
} returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = listOf("mockId-1"),
|
||||
organizationId = "mock-id",
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update with no local cipher, but with common collections, should make a request save cipher to disk`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val number = 1
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
val collection = createMockCollection(number = number)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.getCipher(any(), any()) } returns originalCipher
|
||||
coEvery {
|
||||
vaultDiskSource.getCollections(userId = userId)
|
||||
} returns MutableStateFlow(listOf(collection))
|
||||
|
||||
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = listOf("mockId-1"),
|
||||
organizationId = "mock-id",
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update with no local cipher should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.getCipher(userId = userId, cipherId = cipherId) } returns null
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
ciphersService.getCipher(cipherId = any())
|
||||
vaultDiskSource.saveCipher(userId = any(), cipher = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update with more recent local cipher should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
ciphersService.getCipher(cipherId = any())
|
||||
vaultDiskSource.saveCipher(userId = any(), cipher = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update failure with 404 code should make a request for a cipher and then delete it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns createMockCipher(number = number)
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asFailure()
|
||||
coEvery { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } just runs
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow create failure with 404 code should make a request for a cipher and do nothing`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-1"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asFailure()
|
||||
coEvery { vaultDiskSource.getCipher(userId = userId, cipherId = cipherId) } returns null
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultDiskSource.deleteCipher(userId = any(), cipherId = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow valid create success should make a request for a cipher and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns null
|
||||
coEvery {
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
} returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow valid update success should make a request for a cipher and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = mockk<SyncResponseJson.Cipher> {
|
||||
every { revisionDate } returns ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES)
|
||||
}
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri(
|
||||
url: String,
|
||||
queryParams: Map<String, String> = emptyMap(),
|
||||
|
||||
@ -24,7 +24,6 @@ import com.bitwarden.network.model.createMockDomains
|
||||
import com.bitwarden.network.model.createMockFolder
|
||||
import com.bitwarden.network.model.createMockOrganizationKeys
|
||||
import com.bitwarden.network.model.createMockSend
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendView
|
||||
@ -44,8 +43,6 @@ import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@ -111,7 +108,6 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.HttpException
|
||||
import java.net.UnknownHostException
|
||||
import java.security.GeneralSecurityException
|
||||
import java.security.MessageDigest
|
||||
@ -135,7 +131,6 @@ class VaultRepositoryTest {
|
||||
private val settingsDiskSource = mockk<SettingsDiskSource> {
|
||||
every { getLastSyncTime(userId = any()) } returns clock.instant()
|
||||
}
|
||||
private val ciphersService: CiphersService = mockk()
|
||||
private val folderService: FolderService = mockk()
|
||||
private val mutableGetCiphersFlow: MutableStateFlow<List<SyncResponseJson.Cipher>> =
|
||||
MutableStateFlow(listOf(createMockCipher(1)))
|
||||
@ -174,14 +169,10 @@ class VaultRepositoryTest {
|
||||
every { databaseSchemeChangeFlow } returns mutableDatabaseSchemeChangeFlow
|
||||
}
|
||||
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableSyncCipherDeleteFlow = bufferedMutableSharedFlow<SyncCipherDeleteData>()
|
||||
private val mutableSyncCipherUpsertFlow = bufferedMutableSharedFlow<SyncCipherUpsertData>()
|
||||
private val mutableSyncFolderDeleteFlow = bufferedMutableSharedFlow<SyncFolderDeleteData>()
|
||||
private val mutableSyncFolderUpsertFlow = bufferedMutableSharedFlow<SyncFolderUpsertData>()
|
||||
private val pushManager: PushManager = mockk {
|
||||
every { fullSyncFlow } returns mutableFullSyncFlow
|
||||
every { syncCipherDeleteFlow } returns mutableSyncCipherDeleteFlow
|
||||
every { syncCipherUpsertFlow } returns mutableSyncCipherUpsertFlow
|
||||
every { syncFolderDeleteFlow } returns mutableSyncFolderDeleteFlow
|
||||
every { syncFolderUpsertFlow } returns mutableSyncFolderUpsertFlow
|
||||
}
|
||||
@ -189,7 +180,6 @@ class VaultRepositoryTest {
|
||||
private val credentialExchangeImportManager: CredentialExchangeImportManager = mockk()
|
||||
|
||||
private val vaultRepository = VaultRepositoryImpl(
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
@ -2543,348 +2533,6 @@ class VaultRepositoryTest {
|
||||
coVerify { vaultSyncManager.sync(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncCipherDeleteFlow should delete cipher from disk`() {
|
||||
val userId = "mockId-1"
|
||||
val cipherId = "mockId-1"
|
||||
|
||||
coEvery { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } just runs
|
||||
|
||||
mutableSyncCipherDeleteFlow.tryEmit(
|
||||
SyncCipherDeleteData(userId = userId, cipherId = cipherId),
|
||||
)
|
||||
|
||||
coVerify { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow create with local cipher with no common collections should do nothing`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
ciphersService.getCipher(cipherId = any())
|
||||
vaultDiskSource.saveCipher(userId = any(), cipher = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow create with local cipher, and with common collections, should make a request and save cipher to disk`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val collection = createMockCollection(number = number)
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
coEvery {
|
||||
vaultDiskSource.getCollections(userId = userId)
|
||||
} returns MutableStateFlow(listOf(collection))
|
||||
coEvery {
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
} returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = listOf("mockId-1"),
|
||||
organizationId = "mock-id",
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update with no local cipher, but with common collections, should make a request save cipher to disk`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val number = 1
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
val collection = createMockCollection(number = number)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.getCipher(any(), any()) } returns originalCipher
|
||||
coEvery {
|
||||
vaultDiskSource.getCollections(userId = userId)
|
||||
} returns MutableStateFlow(listOf(collection))
|
||||
|
||||
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = listOf("mockId-1"),
|
||||
organizationId = "mock-id",
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update with no local cipher should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.getCipher(userId = userId, cipherId = cipherId) } returns null
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
ciphersService.getCipher(cipherId = any())
|
||||
vaultDiskSource.saveCipher(userId = any(), cipher = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update with more recent local cipher should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = createMockCipher(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
ciphersService.getCipher(cipherId = any())
|
||||
vaultDiskSource.saveCipher(userId = any(), cipher = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow update failure with 404 code should make a request for a cipher and then delete it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns createMockCipher(number = number)
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asFailure()
|
||||
coEvery { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) } just runs
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow create failure with 404 code should make a request for a cipher and do nothing`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-1"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { ciphersService.getCipher(cipherId = cipherId) } returns response.asFailure()
|
||||
coEvery { vaultDiskSource.getCipher(userId = userId, cipherId = cipherId) } returns null
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultDiskSource.deleteCipher(userId = any(), cipherId = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow valid create success should make a request for a cipher and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns null
|
||||
coEvery {
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
} returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = false,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId = cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncCipherUpsertFlow valid update success should make a request for a cipher and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val cipherId = "mockId-$number"
|
||||
val originalCipher = mockk<SyncResponseJson.Cipher> {
|
||||
every { revisionDate } returns ZonedDateTime.now(clock).minus(5, ChronoUnit.MINUTES)
|
||||
}
|
||||
val updatedCipher = mockk<SyncResponseJson.Cipher>()
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
} returns originalCipher
|
||||
coEvery { ciphersService.getCipher(cipherId) } returns updatedCipher.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
} just runs
|
||||
|
||||
mutableSyncCipherUpsertFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
cipherId = cipherId,
|
||||
revisionDate = ZonedDateTime.now(clock),
|
||||
isUpdate = true,
|
||||
collectionIds = null,
|
||||
organizationId = null,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultDiskSource.getCipher(userId = userId, cipherId = cipherId)
|
||||
ciphersService.getCipher(cipherId)
|
||||
vaultDiskSource.saveCipher(userId = userId, cipher = updatedCipher)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncFolderDeleteFlow should delete folder from disk and update ciphers`() {
|
||||
val userId = "mockId-1"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user