Add sends database table (#490)

This commit is contained in:
David Perez
2024-01-04 10:20:54 -06:00
committed by GitHub
parent 83007ce1fc
commit 3cf7266134
10 changed files with 377 additions and 228 deletions

View File

@@ -6,13 +6,16 @@ import com.x8bit.bitwarden.data.util.assertJsonEquals
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeCiphersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeCollectionsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeFoldersDao
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeSendsDao
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
@@ -29,6 +32,7 @@ class VaultDiskSourceTest {
private lateinit var ciphersDao: FakeCiphersDao
private lateinit var collectionsDao: FakeCollectionsDao
private lateinit var foldersDao: FakeFoldersDao
private lateinit var sendsDao: FakeSendsDao
private lateinit var vaultDiskSource: VaultDiskSource
@@ -37,10 +41,12 @@ class VaultDiskSourceTest {
ciphersDao = FakeCiphersDao()
collectionsDao = FakeCollectionsDao()
foldersDao = FakeFoldersDao()
sendsDao = FakeSendsDao()
vaultDiskSource = VaultDiskSourceImpl(
ciphersDao = ciphersDao,
collectionsDao = collectionsDao,
foldersDao = foldersDao,
sendsDao = sendsDao,
json = json,
)
}
@@ -87,17 +93,30 @@ class VaultDiskSourceTest {
}
}
@Test
fun `getSends should emit all SendsDao updates`() = runTest {
val sendEntities = listOf(SEND_ENTITY)
val sends = listOf(SEND_1)
vaultDiskSource
.getSends(USER_ID)
.test {
assertEquals(emptyList<SyncResponseJson.Send>(), awaitItem())
sendsDao.insertSends(sendEntities)
assertEquals(sends, awaitItem())
}
}
@Test
fun `replaceVaultData should clear the daos and insert the new vault data`() = runTest {
assertEquals(ciphersDao.storedCiphers, emptyList<CipherEntity>())
assertEquals(collectionsDao.storedCollections, emptyList<CollectionEntity>())
assertEquals(foldersDao.storedFolders, emptyList<FolderEntity>())
assertEquals(sendsDao.storedSends, emptyList<SendEntity>())
vaultDiskSource.replaceVaultData(USER_ID, VAULT_DATA)
assertEquals(1, ciphersDao.storedCiphers.size)
assertEquals(1, foldersDao.storedFolders.size)
// Verify the ciphers dao is updated
val storedCipherEntity = ciphersDao.storedCiphers.first()
// We cannot compare the JSON strings directly because of formatting differences
@@ -110,6 +129,14 @@ class VaultDiskSourceTest {
// Verify the folders dao is updated
assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders)
assertEquals(1, sendsDao.storedSends.size)
// Verify the ciphers dao is updated
val storedSendEntity = sendsDao.storedSends.first()
// We cannot compare the JSON strings directly because of formatting differences
// So we split that off into its own assertion.
assertEquals(SEND_ENTITY.copy(sendJson = ""), storedSendEntity.copy(sendJson = ""))
assertJsonEquals(SEND_ENTITY.sendJson, storedSendEntity.sendJson)
}
@Test
@@ -117,10 +144,12 @@ class VaultDiskSourceTest {
assertFalse(ciphersDao.deleteCiphersCalled)
assertFalse(collectionsDao.deleteCollectionsCalled)
assertFalse(foldersDao.deleteFoldersCalled)
assertFalse(sendsDao.deleteSendsCalled)
vaultDiskSource.deleteVaultData(USER_ID)
assertTrue(ciphersDao.deleteCiphersCalled)
assertTrue(collectionsDao.deleteCollectionsCalled)
assertTrue(foldersDao.deleteFoldersCalled)
assertTrue(sendsDao.deleteSendsCalled)
}
}
@@ -129,6 +158,7 @@ private const val USER_ID: String = "test_user_id"
private val CIPHER_1: SyncResponseJson.Cipher = createMockCipher(1)
private val COLLECTION_1: SyncResponseJson.Collection = createMockCollection(3)
private val FOLDER_1: SyncResponseJson.Folder = createMockFolder(2)
private val SEND_1: SyncResponseJson.Send = createMockSend(1)
private val VAULT_DATA: SyncResponseJson = SyncResponseJson(
folders = listOf(FOLDER_1),
@@ -142,7 +172,7 @@ private val VAULT_DATA: SyncResponseJson = SyncResponseJson(
globalEquivalentDomains = null,
equivalentDomains = null,
),
sends = null,
sends = listOf(SEND_1),
)
private const val CIPHER_JSON = """
@@ -260,3 +290,39 @@ private val FOLDER_ENTITY = FolderEntity(
name = "mockName-2",
revisionDate = ZonedDateTime.parse("2023-10-27T12:00Z"),
)
private const val SEND_JSON = """
{
"accessCount": 1,
"notes": "mockNotes-1",
"revisionDate": "2023-10-27T12:00:00.000Z",
"maxAccessCount": 1,
"hideEmail": false,
"type": 1,
"accessId": "mockAccessId-1",
"password": "mockPassword-1",
"file": {
"fileName": "mockFileName-1",
"size": 1,
"sizeName": "mockSizeName-1",
"id": "mockId-1"
},
"deletionDate": "2023-10-27T12:00:00.000Z",
"name": "mockName-1",
"disabled": false,
"id": "mockId-1",
"text": {
"hidden": false,
"text": "mockText-1"
},
"key": "mockKey-1",
"expirationDate": "2023-10-27T12:00:00.000Z"
}
"""
private val SEND_ENTITY = SendEntity(
id = "mockId-1",
userId = USER_ID,
sendType = "1",
sendJson = SEND_JSON,
)

View File

@@ -0,0 +1,42 @@
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class FakeSendsDao : SendsDao {
val storedSends = mutableListOf<SendEntity>()
var deleteSendsCalled: Boolean = false
private val sendsFlow = bufferedMutableSharedFlow<List<SendEntity>>(replay = 1)
init {
sendsFlow.tryEmit(emptyList())
}
override suspend fun deleteAllSends(userId: String): Int {
deleteSendsCalled = true
val count = storedSends.count { it.userId == userId }
storedSends.removeAll { it.userId == userId }
sendsFlow.tryEmit(storedSends.toList())
return count
}
override fun getAllSends(userId: String): Flow<List<SendEntity>> =
sendsFlow.map { ciphers -> ciphers.filter { it.userId == userId } }
override suspend fun insertSends(sends: List<SendEntity>) {
storedSends.addAll(sends)
sendsFlow.tryEmit(storedSends.toList())
}
override suspend fun replaceAllSends(userId: String, sends: List<SendEntity>): Boolean {
val removed = storedSends.removeAll { it.userId == userId }
storedSends.addAll(sends)
sendsFlow.tryEmit(storedSends.toList())
return removed || sends.isNotEmpty()
}
}

View File

@@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollect
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
@@ -50,6 +51,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
import io.mockk.awaits
import io.mockk.coEvery
import io.mockk.coVerify
@@ -234,6 +236,55 @@ class VaultRepositoryTest {
}
}
@Test
fun `sendDataStateFlow should emit decrypted list of sends when decryptSendsList succeeds`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockSendList = listOf(createMockSend(number = 1))
val mockEncryptedSendList = mockSendList.toEncryptedSdkSendList()
val mockSendViewList = listOf(createMockSendView(number = 1))
val mutableSendsStateFlow =
bufferedMutableSharedFlow<List<SyncResponseJson.Send>>(replay = 1)
every {
vaultDiskSource.getSends(userId = MOCK_USER_STATE.activeUserId)
} returns mutableSendsStateFlow
coEvery {
vaultSdkSource.decryptSendList(mockEncryptedSendList)
} returns mockSendViewList.asSuccess()
vaultRepository
.sendDataStateFlow
.test {
assertEquals(DataState.Loading, awaitItem())
mutableSendsStateFlow.tryEmit(mockSendList)
assertEquals(DataState.Loaded(SendData(mockSendViewList)), awaitItem())
}
}
@Test
fun `sendDataStateFlow should emit an error when decryptSendsList fails`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val throwable = Throwable("Fail")
val mockSendList = listOf(createMockSend(number = 1))
val mockEncryptedSendList = mockSendList.toEncryptedSdkSendList()
val mutableSendsStateFlow =
bufferedMutableSharedFlow<List<SyncResponseJson.Send>>(replay = 1)
every {
vaultDiskSource.getSends(userId = MOCK_USER_STATE.activeUserId)
} returns mutableSendsStateFlow
coEvery {
vaultSdkSource.decryptSendList(mockEncryptedSendList)
} returns throwable.asFailure()
vaultRepository
.sendDataStateFlow
.test {
assertEquals(DataState.Loading, awaitItem())
mutableSendsStateFlow.tryEmit(mockSendList)
assertEquals(DataState.Error<SendData>(throwable), awaitItem())
}
}
@Test
fun `deleteVaultData should call deleteVaultData on VaultDiskSource`() {
val userId = "userId-1234"
@@ -248,7 +299,7 @@ class VaultRepositoryTest {
@Suppress("MaxLineLength")
@Test
fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and sendDataStateFlows`() =
fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockSyncResponse = createMockSyncResponse(number = 1)
@@ -266,10 +317,6 @@ class VaultRepositoryTest {
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.userState = MOCK_USER_STATE
vaultRepository.sync()
@@ -303,14 +350,6 @@ class VaultRepositoryTest {
userId = "mockId-1",
organizations = listOf(createMockOrganization(number = 1)),
)
assertEquals(
DataState.Loaded(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
vaultRepository.sendDataStateFlow.value,
)
coVerify {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
@@ -325,121 +364,30 @@ class VaultRepositoryTest {
}
@Test
fun `sync with data should update sendDataStateFlow to Pending before service sync`() =
runTest {
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.userState = MOCK_USER_STATE
fun `sync with syncService Failure should update DataStateFlow with an Error`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockException = IllegalStateException("sad")
coEvery { syncService.sync() } returns mockException.asFailure()
vaultRepository.sendDataStateFlow.test {
assertEquals(
DataState.Loading,
awaitItem(),
)
vaultRepository.sync()
assertEquals(
DataState.Loaded(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
awaitItem(),
)
vaultRepository.sync()
assertEquals(
DataState.Pending(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
awaitItem(),
)
assertEquals(
DataState.Loaded(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
awaitItem(),
)
}
}
vaultRepository.sync()
@Test
fun `sync with decryptSendList Failure should update sendDataStateFlows with Error`() =
runTest {
val mockException = IllegalStateException()
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns mockException.asFailure()
fakeAuthDiskSource.userState = MOCK_USER_STATE
vaultRepository.sync()
assertEquals(
DataState.Error<SendData>(error = mockException),
vaultRepository.sendDataStateFlow.value,
)
}
@Test
fun `sync with syncService Failure should update DataStateFlow with an Error`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockException = IllegalStateException("sad")
coEvery { syncService.sync() } returns mockException.asFailure()
vaultRepository.sync()
assertEquals(
DataState.Error<List<CipherView>>(mockException),
vaultRepository.ciphersStateFlow.value,
)
assertEquals(
DataState.Error<List<CollectionView>>(mockException),
vaultRepository.collectionsStateFlow.value,
)
assertEquals(
DataState.Error<List<FolderView>>(mockException),
vaultRepository.foldersStateFlow.value,
)
assertEquals(
DataState.Error<SendData>(mockException),
vaultRepository.sendDataStateFlow.value,
)
}
assertEquals(
DataState.Error<List<CipherView>>(mockException),
vaultRepository.ciphersStateFlow.value,
)
assertEquals(
DataState.Error<List<CollectionView>>(mockException),
vaultRepository.collectionsStateFlow.value,
)
assertEquals(
DataState.Error<List<FolderView>>(mockException),
vaultRepository.foldersStateFlow.value,
)
assertEquals(
DataState.Error<SendData>(mockException),
vaultRepository.sendDataStateFlow.value,
)
}
@Test
fun `sync with syncService Failure should update vaultDataStateFlow with an Error`() = runTest {
@@ -497,66 +445,37 @@ class VaultRepositoryTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `sync with NoNetwork data should update sendDataStateFlow to NoNetwork with data`() =
fun `sync with NoNetwork data should update sendDataStateFlow to Pending and NoNetwork with data`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns UnknownHostException().asFailure()
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
coEvery {
syncService.sync()
} returnsMany listOf(
mockSyncResponse.asSuccess(),
UnknownHostException().asFailure(),
)
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns listOf(createMockSendView(number = 1)).asSuccess()
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1)))
} returns listOf(createMockSendView(1)).asSuccess()
vaultRepository.sendDataStateFlow.test {
assertEquals(
DataState.Loading,
awaitItem(),
)
vaultRepository.sync()
assertEquals(
DataState.Loaded(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
awaitItem(),
)
vaultRepository.sync()
assertEquals(
DataState.Pending(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
awaitItem(),
)
assertEquals(
DataState.NoNetwork(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
),
awaitItem(),
)
}
vaultRepository
.sendDataStateFlow
.test {
assertEquals(DataState.Loading, awaitItem())
sendsFlow.tryEmit(listOf(createMockSend(1)))
assertEquals(
DataState.Loaded(data = SendData(listOf(createMockSendView(1)))),
awaitItem(),
)
vaultRepository.sync()
assertEquals(
DataState.Pending(data = SendData(listOf(createMockSendView(1)))),
awaitItem(),
)
assertEquals(
DataState.NoNetwork(data = SendData(listOf(createMockSendView(1)))),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@@ -1536,47 +1455,28 @@ class VaultRepositoryTest {
@Test
fun `clearUnlockedData should update the sendDataStateFlow to Loading`() = runTest {
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = mockSyncResponse,
)
} just runs
fakeAuthDiskSource.userState = MOCK_USER_STATE
coEvery {
vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1)))
} returns listOf(createMockSendView(number = 1)).asSuccess()
fakeAuthDiskSource.userState = MOCK_USER_STATE
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
setupVaultDiskSourceFlows(sendsFlow = sendsFlow)
vaultRepository.sendDataStateFlow.test {
assertEquals(
DataState.Loading,
awaitItem(),
)
vaultRepository.sync()
assertEquals(DataState.Loading, awaitItem())
sendsFlow.tryEmit(listOf(createMockSend(number = 1)))
assertEquals(
DataState.Loaded(
data = SendData(
sendViewList = listOf(createMockSendView(number = 1)),
),
data = SendData(sendViewList = listOf(createMockSendView(number = 1))),
),
awaitItem(),
)
vaultRepository.clearUnlockedData()
assertEquals(
DataState.Loading,
awaitItem(),
)
assertEquals(DataState.Loading, awaitItem())
}
}
@@ -1857,12 +1757,14 @@ class VaultRepositoryTest {
ciphersFlow: Flow<List<SyncResponseJson.Cipher>> = bufferedMutableSharedFlow(),
collectionsFlow: Flow<List<SyncResponseJson.Collection>> = bufferedMutableSharedFlow(),
foldersFlow: Flow<List<SyncResponseJson.Folder>> = bufferedMutableSharedFlow(),
sendsFlow: Flow<List<SyncResponseJson.Send>> = bufferedMutableSharedFlow(),
) {
coEvery { vaultDiskSource.getCiphers(MOCK_USER_STATE.activeUserId) } returns ciphersFlow
coEvery {
vaultDiskSource.getCollections(MOCK_USER_STATE.activeUserId)
} returns collectionsFlow
coEvery { vaultDiskSource.getFolders(MOCK_USER_STATE.activeUserId) } returns foldersFlow
coEvery { vaultDiskSource.getSends(MOCK_USER_STATE.activeUserId) } returns sendsFlow
}
/**