From 5531b478d3454bcf18bd68d5397e7541098ce5e1 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:18:53 -0500 Subject: [PATCH] Add comprehensive tests for FileManagerImpl (#6425) Co-authored-by: Claude Sonnet 4.5 --- .../data/manager/file/FileManagerTest.kt | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 data/src/test/kotlin/com/bitwarden/data/manager/file/FileManagerTest.kt 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 new file mode 100644 index 0000000000..e0ffb48b29 --- /dev/null +++ b/data/src/test/kotlin/com/bitwarden/data/manager/file/FileManagerTest.kt @@ -0,0 +1,306 @@ +package com.bitwarden.data.manager.file + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +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 kotlinx.coroutines.test.runTest +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.IOException +import java.io.InputStream +import java.io.OutputStream + +/** + * Test class for [FileManagerImpl]. + */ +class FileManagerTest { + + private val fakeDispatcherManager = FakeDispatcherManager() + private val mockContentResolver = mockk() + private val downloadService = mockk() + private val mockContext = mockk { + every { contentResolver } returns mockContentResolver + } + private val mockUri = mockk() + + private val fileManager = FileManagerImpl( + context = mockContext, + dispatcherManager = fakeDispatcherManager, + downloadService = downloadService, + ) + + //region stringToUri Tests + + @Test + fun `stringToUri with valid data should return true`() = runTest { + val testString = "Test data" + val mockOutputStream = createMockOutputStream() + + every { mockOutputStream.write(testString.toByteArray()) } + every { mockContentResolver.openOutputStream(mockUri) } returns mockOutputStream + + val result = fileManager.stringToUri(mockUri, testString) + + assertTrue(result) + } + + @Test + fun `stringToUri with write failure should return false`() = runTest { + val testString = "Test data" + + every { + mockContentResolver.openOutputStream(mockUri) + } throws IOException("Write failed") + + val result = fileManager.stringToUri(mockUri, testString) + + assertFalse(result) + } + + @Test + fun `stringToUri should convert string to bytes correctly`() = runTest { + val testString = "Hello, World!" + val capturedBytes = mutableListOf() + val mockOutputStream = createMockOutputStream(capturedBytes) + + every { mockContentResolver.openOutputStream(mockUri) } returns mockOutputStream + + fileManager.stringToUri(mockUri, testString) + + assertEquals(testString, String(capturedBytes.toByteArray())) + } + + @Test + fun `stringToUri with large string should write completely`() = runTest { + val testString = "A".repeat(10000) + val capturedBytes = mutableListOf() + val mockOutputStream = createMockOutputStream(capturedBytes) + + every { mockContentResolver.openOutputStream(mockUri) } returns mockOutputStream + + val result = fileManager.stringToUri(mockUri, testString) + + assertTrue(result) + assertEquals(testString.length, capturedBytes.size) + assertEquals(testString, String(capturedBytes.toByteArray())) + } + + @Test + fun `stringToUri with null OutputStream should return false`() = runTest { + every { mockContentResolver.openOutputStream(mockUri) } returns null + + val result = fileManager.stringToUri(mockUri, "Test data") + + assertFalse(result) + } + + //endregion + + //region uriToByteArray Tests + + @Test + fun `uriToByteArray with valid file should return Success`() = runTest { + val testData = "Test content".toByteArray() + val mockInputStream = createMockInputStream(testData) + + every { mockContentResolver.openInputStream(mockUri) } returns mockInputStream + + val result = fileManager.uriToByteArray(mockUri) + + assertArrayEquals(testData, result.getOrThrow()) + } + + @Test + fun `uriToByteArray with null InputStream should return Failure with Stream has crashed`() = + runTest { + every { mockContentResolver.openInputStream(mockUri) } returns null + + val result = fileManager.uriToByteArray(mockUri) + assertTrue(result.isFailure) + val exception = result.exceptionOrNull() + assertInstanceOf(exception) + assertEquals("Stream has crashed", exception.message) + } + + @Test + fun `uriToByteArray with read exception should return Failure`() = runTest { + every { mockContentResolver.openInputStream(mockUri) } throws + RuntimeException("Read failed") + + val result = fileManager.uriToByteArray(mockUri) + + assertTrue(result.isFailure) + val exception = result.exceptionOrNull() + assertInstanceOf(exception) + assertEquals("Read failed", exception.message) + } + + @Test + fun `uriToByteArray should read in 1024-byte buffers`() = runTest { + val testData = "X".repeat(2048).toByteArray() + var readCallCount = 0 + var maxBufferSize = 0 + + val mockInputStream = mockk { + var position = 0 + every { read(any()) } answers { + readCallCount++ + val buffer = firstArg() + maxBufferSize = maxOf(maxBufferSize, buffer.size) + + val remaining = testData.size - position + if (remaining <= 0) return@answers -1 + + val toRead = minOf(remaining, buffer.size) + testData.copyInto( + destination = buffer, + destinationOffset = 0, + startIndex = position, + endIndex = position + toRead, + ) + position += toRead + toRead + } + every { close() } just runs + } + + every { mockContentResolver.openInputStream(mockUri) } returns mockInputStream + + val result = fileManager.uriToByteArray(mockUri) + + assertArrayEquals(testData, result.getOrNull()) + assertEquals(1024, maxBufferSize) + } + + @Test + fun `uriToByteArray with empty file should return empty ByteArray`() = runTest { + val testData = ByteArray(0) + val mockInputStream = createMockInputStream(testData) + + every { mockContentResolver.openInputStream(mockUri) } returns mockInputStream + + val result = fileManager.uriToByteArray(mockUri) + val expected = testData.asSuccess() + + assertArrayEquals(expected.getOrNull(), result.getOrNull()) + } + + @Test + fun `uriToByteArray with small file should read completely`() = runTest { + val testData = "Small file".toByteArray() + val mockInputStream = createMockInputStream(testData) + + every { mockContentResolver.openInputStream(mockUri) } returns mockInputStream + + val result = fileManager.uriToByteArray(mockUri) + + assertArrayEquals(testData, result.getOrThrow()) + } + + @Test + fun `uriToByteArray with large file should read completely`() = runTest { + val testData = "L".repeat(5000).toByteArray() + val mockInputStream = createMockInputStream(testData) + + every { mockContentResolver.openInputStream(mockUri) } returns mockInputStream + + val result = fileManager.uriToByteArray(mockUri) + + assertArrayEquals(testData, result.getOrThrow()) + } + + @Test + fun `uriToByteArray should handle partial reads`() = runTest { + val testData = "Partial read test".toByteArray() + var readCallCount = 0 + + val mockInputStream = mockk { + var position = 0 + every { read(any()) } answers { + readCallCount++ + val buffer = firstArg() + + val remaining = testData.size - position + if (remaining <= 0) return@answers -1 + + // Simulate partial reads by only reading half the buffer size + val toRead = minOf(remaining, buffer.size / 2, 5) + testData.copyInto( + destination = buffer, + destinationOffset = 0, + startIndex = position, + endIndex = position + toRead, + ) + position += toRead + toRead + } + every { close() } just runs + } + + every { mockContentResolver.openInputStream(mockUri) } returns mockInputStream + + val result = fileManager.uriToByteArray(mockUri) + + assertArrayEquals(testData, result.getOrThrow()) + assertTrue(readCallCount > testData.size / 5) // Multiple small reads + } + + //endregion + + //region Helper Methods + + /** + * Creates a mock OutputStream that captures written bytes. + */ + private fun createMockOutputStream( + capturedBytes: MutableList = mutableListOf(), + ): OutputStream = mockk { + every { write(any()) } answers { + capturedBytes.addAll(firstArg().toList()) + } + every { write(any(), any(), any()) } answers { + val buffer = firstArg() + val offset = secondArg() + val length = thirdArg() + capturedBytes.addAll(buffer.slice(offset until offset + length)) + } + every { close() } just runs + every { flush() } just runs + } + + /** + * Creates a mock InputStream that reads from testData. + */ + private fun createMockInputStream(testData: ByteArray): InputStream = mockk { + var position = 0 + every { read(any()) } answers { + val buffer = firstArg() + val remaining = testData.size - position + if (remaining <= 0) return@answers -1 + + val toRead = minOf(remaining, buffer.size) + testData.copyInto( + destination = buffer, + destinationOffset = 0, + startIndex = position, + endIndex = position + toRead, + ) + position += toRead + toRead + } + every { close() } just runs + } + + //endregion +}