From 6acfb1070917e9ff32efce0c5b976c07d4b1f3b8 Mon Sep 17 00:00:00 2001 From: David Perez Date: Sun, 7 Jan 2024 12:50:55 -0600 Subject: [PATCH] Saves changes to database instead of syncing (#517) --- .../vault/datasource/disk/VaultDiskSource.kt | 20 ++++ .../datasource/disk/VaultDiskSourceImpl.kt | 51 +++++++++ .../vault/repository/VaultRepositoryImpl.kt | 40 ++++--- .../datasource/disk/VaultDiskSourceTest.kt | 56 ++++++++++ .../datasource/disk/dao/FakeCiphersDao.kt | 2 + .../datasource/disk/dao/FakeCollectionsDao.kt | 2 + .../datasource/disk/dao/FakeFoldersDao.kt | 2 + .../vault/datasource/disk/dao/FakeSendsDao.kt | 2 + .../vault/repository/VaultRepositoryTest.kt | 104 ++---------------- 9 files changed, 171 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt index 3bcc561dc..04adb32d1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt @@ -8,21 +8,41 @@ import kotlinx.coroutines.flow.Flow */ interface VaultDiskSource { + /** + * Saves a cipher to the data source for the given [userId]. + */ + suspend fun saveCipher(userId: String, cipher: SyncResponseJson.Cipher) + /** * Retrieves all ciphers from the data source for a given [userId]. */ fun getCiphers(userId: String): Flow> + /** + * Saves a collection to the data source for the given [userId]. + */ + suspend fun saveCollection(userId: String, collection: SyncResponseJson.Collection) + /** * Retrieves all collections from the data source for a given [userId]. */ fun getCollections(userId: String): Flow> + /** + * Saves a folder to the data source for the given [userId]. + */ + suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder) + /** * Retrieves all folders from the data source for a given [userId]. */ fun getFolders(userId: String): Flow> + /** + * Saves a send to the data source for the given [userId]. + */ + suspend fun saveSend(userId: String, send: SyncResponseJson.Send) + /** * Retrieves all sends from the data source for a given [userId]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt index 78f5e2412..a32c8cf76 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt @@ -36,6 +36,19 @@ class VaultDiskSourceImpl( private val forceFolderFlow = bufferedMutableSharedFlow>() private val forceSendFlow = bufferedMutableSharedFlow>() + override suspend fun saveCipher(userId: String, cipher: SyncResponseJson.Cipher) { + ciphersDao.insertCiphers( + ciphers = listOf( + CipherEntity( + id = cipher.id, + userId = userId, + cipherType = json.encodeToString(cipher.type), + cipherJson = json.encodeToString(cipher), + ), + ), + ) + } + override fun getCiphers( userId: String, ): Flow> = @@ -50,6 +63,20 @@ class VaultDiskSourceImpl( }, ) + override suspend fun saveCollection(userId: String, collection: SyncResponseJson.Collection) { + collectionsDao.insertCollection( + collection = CollectionEntity( + userId = userId, + id = collection.id, + name = collection.name, + organizationId = collection.organizationId, + shouldHidePasswords = collection.shouldHidePasswords, + externalId = collection.externalId, + isReadOnly = collection.isReadOnly, + ), + ) + } + override fun getCollections( userId: String, ): Flow> = @@ -71,6 +98,17 @@ class VaultDiskSourceImpl( }, ) + override suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder) { + foldersDao.insertFolder( + folder = FolderEntity( + userId = userId, + id = folder.id, + name = folder.name, + revisionDate = folder.revisionDate, + ), + ) + } + override fun getFolders( userId: String, ): Flow> = @@ -89,6 +127,19 @@ class VaultDiskSourceImpl( }, ) + override suspend fun saveSend(userId: String, send: SyncResponseJson.Send) { + sendsDao.insertSends( + sends = listOf( + SendEntity( + userId = userId, + id = send.id, + sendType = json.encodeToString(send.type), + sendJson = json.encodeToString(send), + ), + ), + ) + } + override fun getSends( userId: String, ): Flow> = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index fedb7521d..f15c33af2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -370,10 +370,11 @@ class VaultRepositoryImpl( .onCompletion { willSyncAfterUnlock = false } .first() - override suspend fun createCipher(cipherView: CipherView): CreateCipherResult = - vaultSdkSource + override suspend fun createCipher(cipherView: CipherView): CreateCipherResult { + val userId = requireNotNull(activeUserId) + return vaultSdkSource .encryptCipher( - userId = requireNotNull(activeUserId), + userId = userId, cipherView = cipherView, ) .flatMap { cipher -> @@ -387,18 +388,20 @@ class VaultRepositoryImpl( CreateCipherResult.Error }, onSuccess = { - sync() + vaultDiskSource.saveCipher(userId = userId, cipher = it) CreateCipherResult.Success }, ) + } override suspend fun updateCipher( cipherId: String, cipherView: CipherView, - ): UpdateCipherResult = - vaultSdkSource + ): UpdateCipherResult { + val userId = requireNotNull(activeUserId) + return vaultSdkSource .encryptCipher( - userId = requireNotNull(activeUserId), + userId = userId, cipherView = cipherView, ) .flatMap { cipher -> @@ -416,35 +419,39 @@ class VaultRepositoryImpl( } is UpdateCipherResponseJson.Success -> { - sync() + vaultDiskSource.saveCipher(userId = userId, cipher = response.cipher) UpdateCipherResult.Success } } }, ) + } - override suspend fun createSend(sendView: SendView): CreateSendResult = - vaultSdkSource + override suspend fun createSend(sendView: SendView): CreateSendResult { + val userId = requireNotNull(activeUserId) + return vaultSdkSource .encryptSend( - userId = requireNotNull(activeUserId), + userId = userId, sendView = sendView, ) .flatMap { send -> sendsService.createSend(body = send.toEncryptedNetworkSend()) } .fold( onFailure = { CreateSendResult.Error }, onSuccess = { - sync() + vaultDiskSource.saveSend(userId = userId, send = it) CreateSendResult.Success }, ) + } override suspend fun updateSend( sendId: String, sendView: SendView, - ): UpdateSendResult = - vaultSdkSource + ): UpdateSendResult { + val userId = requireNotNull(activeUserId) + return vaultSdkSource .encryptSend( - userId = requireNotNull(activeUserId), + userId = userId, sendView = sendView, ) .flatMap { send -> @@ -462,12 +469,13 @@ class VaultRepositoryImpl( } is UpdateSendResponseJson.Success -> { - sync() + vaultDiskSource.saveSend(userId = userId, send = response.send) UpdateSendResult.Success } } }, ) + } // TODO: This is temporary. Eventually this needs to be based on the presence of various // user keys but this will likely require SDK updates to support this (BIT-1190). diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index b7f511eb1..3e6ffd9fb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -51,6 +51,23 @@ class VaultDiskSourceTest { ) } + @Test + fun `saveCipher should call insertCiphers`() = runTest { + assertFalse(ciphersDao.insertCiphersCalled) + assertEquals(0, ciphersDao.storedCiphers.size) + + vaultDiskSource.saveCipher(USER_ID, CIPHER_1) + + // Verify the ciphers dao is updated + assertTrue(ciphersDao.insertCiphersCalled) + assertEquals(1, ciphersDao.storedCiphers.size) + val storedCipherEntity = ciphersDao.storedCiphers.first() + // We cannot compare the JSON strings directly because of formatting differences + // So we split that off into its own assertion. + assertEquals(CIPHER_ENTITY.copy(cipherJson = ""), storedCipherEntity.copy(cipherJson = "")) + assertJsonEquals(CIPHER_ENTITY.cipherJson, storedCipherEntity.cipherJson) + } + @Test fun `getCiphers should emit all CiphersDao updates`() = runTest { val cipherEntities = listOf(CIPHER_ENTITY) @@ -65,6 +82,17 @@ class VaultDiskSourceTest { } } + @Test + fun `saveCollection should call insertCollection`() = runTest { + assertFalse(collectionsDao.insertCollectionCalled) + assertEquals(0, collectionsDao.storedCollections.size) + + vaultDiskSource.saveCollection(USER_ID, COLLECTION_1) + + assertTrue(collectionsDao.insertCollectionCalled) + assertEquals(listOf(COLLECTION_ENTITY), collectionsDao.storedCollections) + } + @Test fun `getCollections should emit all CollectionsDao updates`() = runTest { val collectionEntities = listOf(COLLECTION_ENTITY) @@ -79,6 +107,17 @@ class VaultDiskSourceTest { } } + @Test + fun `saveFolder should call insertFolder`() = runTest { + assertFalse(foldersDao.insertFolderCalled) + assertEquals(0, foldersDao.storedFolders.size) + + vaultDiskSource.saveFolder(USER_ID, FOLDER_1) + + assertTrue(foldersDao.insertFolderCalled) + assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders) + } + @Test fun `getFolders should emit all FoldersDao updates`() = runTest { val folderEntities = listOf(FOLDER_ENTITY) @@ -93,6 +132,23 @@ class VaultDiskSourceTest { } } + @Test + fun `saveSend should call insertSend`() = runTest { + assertFalse(sendsDao.insertSendsCalled) + assertEquals(0, sendsDao.storedSends.size) + + vaultDiskSource.saveSend(USER_ID, SEND_1) + + // Verify the sends dao is updated + assertTrue(sendsDao.insertSendsCalled) + assertEquals(1, sendsDao.storedSends.size) + 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 fun `getSends should emit all SendsDao updates`() = runTest { val sendEntities = listOf(SEND_ENTITY) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt index 9dbfa7d23..1c7f7db21 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCiphersDao.kt @@ -10,6 +10,7 @@ class FakeCiphersDao : CiphersDao { val storedCiphers = mutableListOf() var deleteCiphersCalled: Boolean = false + var insertCiphersCalled: Boolean = false private val ciphersFlow = bufferedMutableSharedFlow>(replay = 1) @@ -31,6 +32,7 @@ class FakeCiphersDao : CiphersDao { override suspend fun insertCiphers(ciphers: List) { storedCiphers.addAll(ciphers) ciphersFlow.tryEmit(ciphers.toList()) + insertCiphersCalled = true } override suspend fun replaceAllCiphers(userId: String, ciphers: List): Boolean { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCollectionsDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCollectionsDao.kt index ee3ba3c2b..93e457b8e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCollectionsDao.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeCollectionsDao.kt @@ -11,6 +11,7 @@ class FakeCollectionsDao : CollectionsDao { var deleteCollectionCalled: Boolean = false var deleteCollectionsCalled: Boolean = false + var insertCollectionCalled = false private val collectionsFlow = bufferedMutableSharedFlow>(replay = 1) @@ -43,6 +44,7 @@ class FakeCollectionsDao : CollectionsDao { override suspend fun insertCollection(collection: CollectionEntity) { storedCollections.add(collection) collectionsFlow.tryEmit(storedCollections.toList()) + insertCollectionCalled = true } override suspend fun replaceAllCollections( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeFoldersDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeFoldersDao.kt index a73fd086e..823553189 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeFoldersDao.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeFoldersDao.kt @@ -11,6 +11,7 @@ class FakeFoldersDao : FoldersDao { var deleteFolderCalled: Boolean = false var deleteFoldersCalled: Boolean = false + var insertFolderCalled: Boolean = false private val foldersFlow = bufferedMutableSharedFlow>(replay = 1) @@ -43,6 +44,7 @@ class FakeFoldersDao : FoldersDao { override suspend fun insertFolder(folder: FolderEntity) { storedFolders.add(folder) foldersFlow.tryEmit(storedFolders.toList()) + insertFolderCalled = true } override suspend fun replaceAllFolders(userId: String, folders: List): Boolean { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt index 1d798c46a..43451f1b5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt @@ -10,6 +10,7 @@ class FakeSendsDao : SendsDao { val storedSends = mutableListOf() var deleteSendsCalled: Boolean = false + var insertSendsCalled: Boolean = false private val sendsFlow = bufferedMutableSharedFlow>(replay = 1) @@ -31,6 +32,7 @@ class FakeSendsDao : SendsDao { override suspend fun insertSends(sends: List) { storedSends.addAll(sends) sendsFlow.tryEmit(storedSends.toList()) + insertSendsCalled = true } override suspend fun replaceAllSends(userId: String, sends: List): Boolean { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 7e833a394..a2633f8a1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -411,7 +411,6 @@ class VaultRepositoryTest { @Test fun `sync with syncService Failure should update DataStateFlow with an Error`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" val mockException = IllegalStateException("sad") coEvery { syncService.sync() } returns mockException.asFailure() @@ -438,7 +437,6 @@ class VaultRepositoryTest { @Test fun `sync with syncService Failure should update vaultDataStateFlow with an Error`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" val mockException = IllegalStateException("sad") coEvery { syncService.sync() } returns mockException.asFailure() setupVaultDiskSourceFlows() @@ -455,7 +453,6 @@ class VaultRepositoryTest { @Test fun `sync with NoNetwork should update DataStateFlows to NoNetwork`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" coEvery { syncService.sync() } returns UnknownHostException().asFailure() vaultRepository.sync() @@ -481,7 +478,6 @@ class VaultRepositoryTest { @Test fun `sync with NoNetwork should update vaultDataStateFlow to NoNetwork`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" coEvery { syncService.sync() } returns UnknownHostException().asFailure() setupVaultDiskSourceFlows() @@ -1042,7 +1038,6 @@ class VaultRepositoryTest { privateKey = "mockPrivateKey-1", ) fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" assertEquals( VaultUnlockResult.InvalidStateError, result, @@ -1075,7 +1070,6 @@ class VaultRepositoryTest { privateKey = null, ) fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" assertEquals( VaultUnlockResult.InvalidStateError, result, @@ -1602,7 +1596,6 @@ class VaultRepositoryTest { val folderIdString = "mockId-$folderId" val throwable = Throwable("Fail") fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" coEvery { syncService.sync() } returns throwable.asFailure() setupVaultDiskSourceFlows() @@ -1623,7 +1616,6 @@ class VaultRepositoryTest { val itemId = 1234 val itemIdString = "mockId-$itemId" fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" coEvery { syncService.sync() } returns UnknownHostException().asFailure() setupVaultDiskSourceFlows() @@ -1644,7 +1636,6 @@ class VaultRepositoryTest { val folderId = 1234 val folderIdString = "mockId-$folderId" fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" coEvery { syncService.sync() } returns UnknownHostException().asFailure() setupVaultDiskSourceFlows() @@ -1665,7 +1656,6 @@ class VaultRepositoryTest { val folderIdString = "mockId-$folderId" val throwable = Throwable("Fail") fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" coEvery { syncService.sync() } returns throwable.asFailure() setupVaultDiskSourceFlows() @@ -1741,34 +1731,13 @@ class VaultRepositoryTest { cipherView = mockCipherView, ) } returns createMockSdkCipher(number = 1).asSuccess() + val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.createCipher( body = createMockCipherJsonRequest(number = 1), ) - } returns createMockCipher(number = 1).asSuccess() - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(1)) - coEvery { - vaultDiskSource.replaceVaultData( - userId = userId, - vault = createMockSyncResponse(1), - ) - } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.decryptSendList( - userId = userId, - sendList = listOf(createMockSdkSend(1)), - ) - } returns listOf(createMockSendView(1)).asSuccess() + } returns mockCipher.asSuccess() + coEvery { vaultDiskSource.saveCipher(userId, mockCipher) } just runs val result = vaultRepository.createCipher(cipherView = mockCipherView) @@ -1882,37 +1851,16 @@ class VaultRepositoryTest { cipherView = mockCipherView, ) } returns createMockSdkCipher(number = 1).asSuccess() + val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.updateCipher( cipherId = cipherId, body = createMockCipherJsonRequest(number = 1), ) } returns UpdateCipherResponseJson - .Success(cipher = createMockCipher(number = 1)) + .Success(cipher = mockCipher) .asSuccess() - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(1)) - coEvery { - vaultDiskSource.replaceVaultData( - userId = userId, - vault = createMockSyncResponse(1), - ) - } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.decryptSendList( - userId = userId, - sendList = listOf(createMockSdkSend(1)), - ) - } returns listOf(createMockSendView(1)).asSuccess() + coEvery { vaultDiskSource.saveCipher(userId = userId, cipher = mockCipher) } just runs val result = vaultRepository.updateCipher( cipherId = cipherId, @@ -1965,24 +1913,11 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) } returns createMockSdkSend(number = 1).asSuccess() + val mockSend = createMockSend(number = 1) coEvery { sendsService.createSend(body = createMockSendJsonRequest(number = 1)) - } returns createMockSend(number = 1).asSuccess() - coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(1)) - coEvery { - vaultDiskSource.replaceVaultData( - userId = userId, - vault = createMockSyncResponse(1), - ) - } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() + } returns mockSend.asSuccess() + coEvery { vaultDiskSource.saveSend(userId, mockSend) } just runs val result = vaultRepository.createSend(sendView = mockSendView) @@ -2080,31 +2015,16 @@ class VaultRepositoryTest { coEvery { vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) } returns createMockSdkSend(number = 1).asSuccess() + val mockSend = createMockSend(number = 1) coEvery { sendsService.updateSend( sendId = sendId, body = createMockSendJsonRequest(number = 1), ) } returns UpdateSendResponseJson - .Success(send = createMockSend(number = 1)) + .Success(send = mockSend) .asSuccess() - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(1)) - coEvery { - vaultDiskSource.replaceVaultData( - userId = userId, - vault = createMockSyncResponse(1), - ) - } just runs - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() + coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs val result = vaultRepository.updateSend( sendId = sendId,