diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt index 5d3118696be..f1ae2f8f66f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt @@ -7,7 +7,6 @@ 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.file.FileManager -import com.bitwarden.data.manager.model.DownloadResult import com.bitwarden.network.model.ArchiveCipherResponseJson import com.bitwarden.network.model.AttachmentJsonResponse import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest @@ -18,6 +17,7 @@ import com.bitwarden.network.model.UnarchiveCipherResponseJson import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import com.bitwarden.network.service.CiphersService +import com.bitwarden.network.service.DownloadService import com.bitwarden.vault.AttachmentView import com.bitwarden.vault.CipherView import com.bitwarden.vault.EncryptionContext @@ -57,6 +57,7 @@ import java.time.Clock */ @Suppress("TooManyFunctions", "LongParameterList", "LargeClass") class CipherManagerImpl( + private val downloadService: DownloadService, private val fileManager: FileManager, private val authDiskSource: AuthDiskSource, private val settingsDiskSource: SettingsDiskSource, @@ -622,14 +623,13 @@ class CipherManagerImpl( val url = attachmentData.url ?: return IllegalStateException("Attachment does not have a url").asFailure() - val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) { - is DownloadResult.Failure -> { - return IllegalStateException("Download failed", result.error).asFailure() + val encryptedFile = downloadService + .getDataStream(url = url) + .flatMap { fileManager.streamFileToCache(stream = it.byteStream()) } + .getOrElse { + return IllegalStateException("Download failed", it).asFailure() } - is DownloadResult.Success -> result.file - } - val decryptedFile = File(encryptedFile.path + "_decrypted") return vaultSdkSource .decryptFile( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt index 21452a4a680..87a48ff5d27 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -7,6 +7,7 @@ import com.bitwarden.cxf.parser.CredentialExchangePayloadParser import com.bitwarden.data.manager.appstate.AppStateManager import com.bitwarden.data.manager.file.FileManager import com.bitwarden.network.service.CiphersService +import com.bitwarden.network.service.DownloadService import com.bitwarden.network.service.FolderService import com.bitwarden.network.service.SendsService import com.bitwarden.network.service.SyncService @@ -98,6 +99,7 @@ object VaultManagerModule { @Provides @Singleton fun provideCipherManager( + downloadService: DownloadService, ciphersService: CiphersService, settingsDiskSource: SettingsDiskSource, vaultDiskSource: VaultDiskSource, @@ -109,6 +111,7 @@ object VaultManagerModule { dispatcherManager: DispatcherManager, pushManager: PushManager, ): CipherManager = CipherManagerImpl( + downloadService = downloadService, fileManager = fileManager, settingsDiskSource = settingsDiskSource, authDiskSource = authDiskSource, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt index efcf65f06a4..a8d8f035920 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt @@ -7,7 +7,6 @@ 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.manager.file.FileManager -import com.bitwarden.data.manager.model.DownloadResult import com.bitwarden.network.exception.CookieRedirectException import com.bitwarden.network.model.ArchiveCipherResponseJson import com.bitwarden.network.model.AttachmentJsonRequest @@ -27,6 +26,7 @@ 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.network.service.DownloadService import com.bitwarden.vault.Attachment import com.bitwarden.vault.AttachmentView import com.bitwarden.vault.Cipher @@ -77,12 +77,14 @@ import io.mockk.unmockkConstructor import io.mockk.unmockkStatic import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody import org.junit.jupiter.api.AfterEach 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 java.io.File +import java.io.InputStream import java.time.Clock import java.time.Instant import java.time.ZoneOffset @@ -112,8 +114,10 @@ class CipherManagerTest { every { syncCipherDeleteFlow } returns mutableSyncCipherDeleteFlow every { syncCipherUpsertFlow } returns mutableSyncCipherUpsertFlow } + private val downloadService: DownloadService = mockk() private val cipherManager: CipherManager = CipherManagerImpl( + downloadService = downloadService, ciphersService = ciphersService, settingsDiskSource = fakeSettingsDiskSource, vaultDiskSource = vaultDiskSource, @@ -1303,9 +1307,16 @@ class CipherManagerTest { coEvery { ciphersService.getCipherAttachment(cipherId = "mockId-1", attachmentId = "mockId-1") } returns attachment.asSuccess() + val mockInputStream = mockk() + val mockResponseBody = mockk { + every { byteStream() } returns mockInputStream + } + coEvery { + downloadService.getDataStream(url = "mockUrl-1") + } returns mockResponseBody.asSuccess() coEvery { - fileManager.downloadFileToCache(url = "mockUrl-1") - } returns DownloadResult.Success(file = encryptedFile) + fileManager.streamFileToCache(stream = mockInputStream) + } returns encryptedFile.asSuccess() coEvery { vaultSdkSource.decryptFile( userId = userId, @@ -2311,7 +2322,7 @@ class CipherManagerTest { } @Test - fun `downloadAttachment with failed download should return Failure`() = runTest { + fun `downloadAttachment with failed data stream request should return Failure`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val attachmentId = "mockId-1" @@ -2343,8 +2354,8 @@ class CipherManagerTest { ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any()) } returns response.asSuccess() coEvery { - fileManager.downloadFileToCache(url = any()) - } returns DownloadResult.Failure(error = Throwable("Fail!")) + downloadService.getDataStream(url = any()) + } returns Throwable("Fail!").asFailure() assertEquals( DownloadAttachmentResult.Failure(IllegalStateException()), @@ -2363,7 +2374,75 @@ class CipherManagerTest { cipherId = requireNotNull(cipherView.id), attachmentId = attachmentId, ) - fileManager.downloadFileToCache("https://bitwarden.com") + downloadService.getDataStream(url = "https://bitwarden.com") + } + coVerify(exactly = 0) { + fileManager.streamFileToCache(stream = any()) + } + } + + @Test + fun `downloadAttachment with failed stream to cache should return Failure`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + + val attachmentId = "mockId-1" + val attachment = mockk { + every { id } returns attachmentId + } + val mockCipher = mockk { + every { key } returns "key" + every { attachments } returns listOf(attachment) + every { id } returns "mockId-1" + } + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } + + val cipherView = createMockCipherView(number = 1) + coEvery { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + } returns mockEncryptionContext.asSuccess() + + val response = mockk { + every { url } returns "https://bitwarden.com" + } + coEvery { + ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any()) + } returns response.asSuccess() + val mockInputStream = mockk() + val mockResponseBody = mockk { + every { byteStream() } returns mockInputStream + } + coEvery { + downloadService.getDataStream(url = any()) + } returns mockResponseBody.asSuccess() + coEvery { + fileManager.streamFileToCache(stream = mockInputStream) + } returns Throwable("Fail!").asFailure() + + assertEquals( + DownloadAttachmentResult.Failure(IllegalStateException()), + cipherManager.downloadAttachment( + cipherView = cipherView, + attachmentId = attachmentId, + ), + ) + + coVerify(exactly = 1) { + vaultSdkSource.encryptCipher( + userId = MOCK_USER_STATE.activeUserId, + cipherView = cipherView, + ) + ciphersService.getCipherAttachment( + cipherId = requireNotNull(cipherView.id), + attachmentId = attachmentId, + ) + downloadService.getDataStream(url = "https://bitwarden.com") + fileManager.streamFileToCache(stream = mockInputStream) } } @@ -2405,9 +2484,16 @@ class CipherManagerTest { every { path } returns "path/to/encrypted/file" } coEvery { fileManager.delete(file) } just runs + val mockInputStream = mockk() + val mockResponseBody = mockk { + every { byteStream() } returns mockInputStream + } coEvery { - fileManager.downloadFileToCache(url = any()) - } returns DownloadResult.Success(file) + downloadService.getDataStream(url = any()) + } returns mockResponseBody.asSuccess() + coEvery { + fileManager.streamFileToCache(stream = mockInputStream) + } returns file.asSuccess() val error = Throwable("Fail") coEvery { vaultSdkSource.decryptFile( @@ -2436,7 +2522,8 @@ class CipherManagerTest { cipherId = requireNotNull(cipherView.id), attachmentId = attachmentId, ) - fileManager.downloadFileToCache("https://bitwarden.com") + downloadService.getDataStream(url = "https://bitwarden.com") + fileManager.streamFileToCache(stream = mockInputStream) vaultSdkSource.decryptFile( userId = MOCK_USER_STATE.activeUserId, cipher = mockCipher, @@ -2488,9 +2575,16 @@ class CipherManagerTest { every { path } returns "path/to/encrypted/file" } coEvery { fileManager.delete(file) } just runs + val mockInputStream = mockk() + val mockResponseBody = mockk { + every { byteStream() } returns mockInputStream + } + coEvery { + downloadService.getDataStream(url = any()) + } returns mockResponseBody.asSuccess() coEvery { - fileManager.downloadFileToCache(any()) - } returns DownloadResult.Success(file) + fileManager.streamFileToCache(stream = mockInputStream) + } returns file.asSuccess() coEvery { vaultSdkSource.decryptFile( @@ -2521,7 +2615,8 @@ class CipherManagerTest { cipherId = requireNotNull(cipherView.id), attachmentId = attachmentId, ) - fileManager.downloadFileToCache(url = "https://bitwarden.com") + downloadService.getDataStream(url = "https://bitwarden.com") + fileManager.streamFileToCache(stream = mockInputStream) vaultSdkSource.decryptFile( userId = MOCK_USER_STATE.activeUserId, cipher = mockCipher, diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt index a177d396ca2..e9a30d33e9c 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/manager/di/PlatformManagerModule.kt @@ -24,7 +24,6 @@ import com.bitwarden.authenticator.data.platform.manager.lock.AppLockManagerImpl import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository import com.bitwarden.authenticator.data.platform.repository.SettingsRepository import com.bitwarden.core.data.manager.UuidManager -import com.bitwarden.core.data.manager.UuidManagerImpl import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManagerImpl import com.bitwarden.core.data.manager.realtime.RealtimeManager @@ -128,10 +127,6 @@ object PlatformManagerModule { @Singleton fun provideEncodingManager(): BitwardenEncodingManager = BitwardenEncodingManagerImpl() - @Provides - @Singleton - fun provideUuidManager(): UuidManager = UuidManagerImpl() - @Provides @Singleton fun providesFeatureFlagManager( diff --git a/core/src/main/kotlin/com/bitwarden/core/data/manager/di/CoreManagerModule.kt b/core/src/main/kotlin/com/bitwarden/core/data/manager/di/CoreManagerModule.kt index 0e830226509..e1a5908d285 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/manager/di/CoreManagerModule.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/manager/di/CoreManagerModule.kt @@ -1,6 +1,8 @@ package com.bitwarden.core.data.manager.di import com.bitwarden.core.data.manager.BuildInfoManager +import com.bitwarden.core.data.manager.UuidManager +import com.bitwarden.core.data.manager.UuidManagerImpl import com.bitwarden.core.data.manager.encryption.EncryptionManager import com.bitwarden.core.data.manager.encryption.EncryptionManagerImpl import com.bitwarden.core.data.manager.encryption.KeystoreManager @@ -33,4 +35,8 @@ object CoreManagerModule { ): KeystoreManager = KeystoreManagerImpl( buildInfoManager = buildInfoManager, ) + + @Provides + @Singleton + fun provideUuidManager(): UuidManager = UuidManagerImpl() } diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt b/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt index a5dfca3e178..b6d52547413 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt @@ -3,7 +3,9 @@ package com.bitwarden.data.manager.di import android.app.Application import android.content.Context import com.bitwarden.core.data.manager.BuildInfoManager +import com.bitwarden.core.data.manager.UuidManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManager +import com.bitwarden.data.datasource.disk.ConfigDiskSource import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource import com.bitwarden.data.manager.BitwardenPackageManager import com.bitwarden.data.manager.BitwardenPackageManagerImpl @@ -17,8 +19,6 @@ import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager import com.bitwarden.data.manager.flightrecorder.FlightRecorderManagerImpl import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriterImpl -import com.bitwarden.data.repository.ServerConfigRepository -import com.bitwarden.network.service.DownloadService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -50,11 +50,11 @@ object DataManagerModule { @Singleton fun provideFileManager( @ApplicationContext context: Context, - downloadService: DownloadService, + uuidManager: UuidManager, dispatcherManager: DispatcherManager, ): FileManager = FileManagerImpl( context = context, - downloadService = downloadService, + uuidManager = uuidManager, dispatcherManager = dispatcherManager, ) @@ -81,13 +81,13 @@ object DataManagerModule { fileManager: FileManager, dispatcherManager: DispatcherManager, buildInfoManager: BuildInfoManager, - serverConfigRepository: ServerConfigRepository, + configDiskSource: ConfigDiskSource, ): FlightRecorderWriter = FlightRecorderWriterImpl( clock = clock, fileManager = fileManager, dispatcherManager = dispatcherManager, buildInfoManager = buildInfoManager, - serverConfigRepository = serverConfigRepository, + configDiskSource = configDiskSource, ) @Provides diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManager.kt b/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManager.kt index 0b3e5cecd47..71834426f73 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManager.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManager.kt @@ -2,9 +2,9 @@ package com.bitwarden.data.manager.file import android.net.Uri import com.bitwarden.annotation.OmitFromCoverage -import com.bitwarden.data.manager.model.DownloadResult import com.bitwarden.data.manager.model.ZipFileResult import java.io.File +import java.io.InputStream /** * Manages reading files. @@ -28,10 +28,10 @@ interface FileManager { suspend fun delete(vararg files: File) /** - * Downloads a file temporarily to cache from [url]. A successful [DownloadResult] will contain + * Opens the provided [stream] and writes it to cache. A successful [Result] will contain * the final file path. */ - suspend fun downloadFileToCache(url: String): DownloadResult + suspend fun streamFileToCache(stream: InputStream): Result /** * Writes an existing [file] to a [fileUri]. `true` will be returned if the file was diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManagerImpl.kt b/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManagerImpl.kt index 1673fe32206..bedb11129c4 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManagerImpl.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/file/FileManagerImpl.kt @@ -5,11 +5,12 @@ package com.bitwarden.data.manager.file import android.content.Context import android.net.Uri import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.core.data.manager.UuidManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManager +import com.bitwarden.core.data.util.asFailure +import com.bitwarden.core.data.util.asSuccess import com.bitwarden.core.data.util.sdkAgnosticTransferTo -import com.bitwarden.data.manager.model.DownloadResult import com.bitwarden.data.manager.model.ZipFileResult -import com.bitwarden.network.service.DownloadService import kotlinx.coroutines.withContext import java.io.BufferedInputStream import java.io.BufferedOutputStream @@ -18,7 +19,7 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException -import java.util.UUID +import java.io.InputStream import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -32,7 +33,7 @@ private const val BUFFER_SIZE: Int = 1024 */ internal class FileManagerImpl( private val context: Context, - private val downloadService: DownloadService, + private val uuidManager: UuidManager, private val dispatcherManager: DispatcherManager, ) : FileManager { @@ -48,20 +49,10 @@ internal class FileManagerImpl( } } - @Suppress("NestedBlockDepth") - override suspend fun downloadFileToCache(url: String): DownloadResult { - val response = downloadService - .getDataStream(url) - .fold( - onSuccess = { it }, - onFailure = { return DownloadResult.Failure(error = it) }, - ) - + override suspend fun streamFileToCache(stream: InputStream): Result { // Create a temporary file in cache to write to - val file = File(context.cacheDir, UUID.randomUUID().toString()) - - withContext(dispatcherManager.io) { - val stream = response.byteStream() + val file = File(context.cacheDir, uuidManager.generateUuid()) + return withContext(dispatcherManager.io) { stream.use { val buffer = ByteArray(BUFFER_SIZE) var progress = 0 @@ -76,13 +67,12 @@ internal class FileManagerImpl( } fos.flush() } catch (e: RuntimeException) { - return@withContext DownloadResult.Failure(error = e) + return@withContext e.asFailure() } } } + file.asSuccess() } - - return DownloadResult.Success(file) } @Suppress("NestedBlockDepth") diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt index 7746f664e81..9f2967e82f7 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt @@ -8,9 +8,9 @@ import com.bitwarden.core.data.manager.BuildInfoManager import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.manager.util.deviceData import com.bitwarden.core.data.util.toFormattedPattern +import com.bitwarden.data.datasource.disk.ConfigDiskSource import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet import com.bitwarden.data.manager.file.FileManager -import com.bitwarden.data.repository.ServerConfigRepository import com.bitwarden.network.util.redactHostnamesInMessage import kotlinx.coroutines.withContext import timber.log.Timber @@ -34,12 +34,15 @@ internal class FlightRecorderWriterImpl( private val fileManager: FileManager, private val dispatcherManager: DispatcherManager, private val buildInfoManager: BuildInfoManager, - private val serverConfigRepository: ServerConfigRepository, + private val configDiskSource: ConfigDiskSource, ) : FlightRecorderWriter { private val configuredHosts: Set get() { - val environment = serverConfigRepository.serverConfigStateFlow.value - ?.serverData?.environment ?: return emptySet() + val environment = configDiskSource + .serverConfig + ?.serverData + ?.environment + ?: return emptySet() return listOfNotNull( environment.vaultUrl, environment.apiUrl, @@ -75,7 +78,7 @@ internal class FlightRecorderWriterImpl( logFile.createNewFile() val ciInfo = buildInfoManager.ciBuildInfo?.takeIf { it.isNotBlank() } - val serverData = serverConfigRepository.serverConfigStateFlow.value?.serverData + val serverData = configDiskSource.serverConfig?.serverData val serverInfo = StringBuilder() .append(serverData?.server?.name ?: "Bitwarden Cloud") .apply { diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/model/DownloadResult.kt b/data/src/main/kotlin/com/bitwarden/data/manager/model/DownloadResult.kt deleted file mode 100644 index 8036e8cf10c..00000000000 --- a/data/src/main/kotlin/com/bitwarden/data/manager/model/DownloadResult.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.bitwarden.data.manager.model - -import java.io.File - -/** - * Represents a result from downloading a raw file. - */ -sealed class DownloadResult { - /** - * The download was a success, and was saved to [file]. - */ - data class Success(val file: File) : DownloadResult() - - /** - * The download failed. - */ - data class Failure( - val error: Throwable, - ) : DownloadResult() -} diff --git a/data/src/test/kotlin/com/bitwarden/data/manager/file/FileManagerTest.kt b/data/src/test/kotlin/com/bitwarden/data/manager/file/FileManagerTest.kt index e0ffb48b29b..84d148dc78b 100644 --- a/data/src/test/kotlin/com/bitwarden/data/manager/file/FileManagerTest.kt +++ b/data/src/test/kotlin/com/bitwarden/data/manager/file/FileManagerTest.kt @@ -3,23 +3,27 @@ package com.bitwarden.data.manager.file import android.content.ContentResolver import android.content.Context import android.net.Uri +import com.bitwarden.core.data.manager.UuidManager import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.util.asSuccess -import com.bitwarden.network.service.DownloadService import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertInstanceOf +import java.io.File import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.nio.file.Files /** * Test class for [FileManagerImpl]. @@ -28,18 +32,25 @@ class FileManagerTest { private val fakeDispatcherManager = FakeDispatcherManager() private val mockContentResolver = mockk() - private val downloadService = mockk() + private val cacheDirectory: File = Files.createTempDirectory("cache").toFile() private val mockContext = mockk { every { contentResolver } returns mockContentResolver + every { cacheDir } returns cacheDirectory } + private val uuidManager: UuidManager = mockk() private val mockUri = mockk() private val fileManager = FileManagerImpl( context = mockContext, + uuidManager = uuidManager, dispatcherManager = fakeDispatcherManager, - downloadService = downloadService, ) + @AfterEach + fun tearDown() { + cacheDirectory.deleteRecursively() + } + //region stringToUri Tests @Test @@ -258,6 +269,62 @@ class FileManagerTest { //endregion + //region streamFileToCache Tests + + @Test + fun `streamFileToCache with valid stream should return Success with the cached file`() = + runTest { + val testData = "Test content".toByteArray() + val mockInputStream = createMockInputStream(testData) + every { uuidManager.generateUuid() } returns "mockUuid" + + val result = fileManager.streamFileToCache(stream = mockInputStream) + + val file = result.getOrThrow() + assertEquals(File(cacheDirectory, "mockUuid"), file) + assertArrayEquals(testData, file.readBytes()) + verify(exactly = 1) { mockInputStream.close() } + } + + @Test + fun `streamFileToCache with empty stream should return Success with an empty file`() = runTest { + val mockInputStream = createMockInputStream(testData = ByteArray(0)) + every { uuidManager.generateUuid() } returns "mockUuid" + + val result = fileManager.streamFileToCache(stream = mockInputStream) + + val file = result.getOrThrow() + assertEquals(0, file.length()) + } + + @Test + fun `streamFileToCache with large stream should write the stream completely`() = runTest { + val testData = "L".repeat(5000).toByteArray() + val mockInputStream = createMockInputStream(testData) + every { uuidManager.generateUuid() } returns "mockUuid" + + val result = fileManager.streamFileToCache(stream = mockInputStream) + + assertArrayEquals(testData, result.getOrThrow().readBytes()) + } + + @Test + fun `streamFileToCache with read failure should return Failure`() = runTest { + val error = RuntimeException("Read failed") + val mockInputStream = mockk { + every { read(any()) } throws error + every { close() } just runs + } + every { uuidManager.generateUuid() } returns "mockUuid" + + val result = fileManager.streamFileToCache(stream = mockInputStream) + + assertEquals(error, result.exceptionOrNull()) + verify(exactly = 1) { mockInputStream.close() } + } + + //endregion + //region Helper Methods /**