From 0f087b7d1540ff8fb14924640d2b5ed6c75a3b31 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:41:21 -0500 Subject: [PATCH] Add comprehensive tests for AuthenticatorRepositoryImpl (#6424) Co-authored-by: Claude Sonnet 4.5 --- .../disk/util/FakeAuthenticatorDiskSource.kt | 6 +- .../repository/AuthenticatorRepositoryTest.kt | 258 +++++++++++++++++- 2 files changed, 251 insertions(+), 13 deletions(-) diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/datasource/disk/util/FakeAuthenticatorDiskSource.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/datasource/disk/util/FakeAuthenticatorDiskSource.kt index 5663444d82..f8339c8de4 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/datasource/disk/util/FakeAuthenticatorDiskSource.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/datasource/disk/util/FakeAuthenticatorDiskSource.kt @@ -2,11 +2,12 @@ package com.bitwarden.authenticator.data.authenticator.datasource.disk.util import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSource import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity +import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onSubscription class FakeAuthenticatorDiskSource : AuthenticatorDiskSource { - private val mutableItemFlow = MutableSharedFlow>() + private val mutableItemFlow = bufferedMutableSharedFlow>() private val storedItems = mutableListOf() override suspend fun saveItem(vararg authenticatorItem: AuthenticatorItemEntity) { @@ -15,6 +16,7 @@ class FakeAuthenticatorDiskSource : AuthenticatorDiskSource { } override fun getItems(): Flow> = mutableItemFlow + .onSubscription { emit(storedItems) } override suspend fun deleteItem(itemId: String) { storedItems.removeIf { it.id == itemId } diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt index b34c853f31..8520501083 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/authenticator/repository/AuthenticatorRepositoryTest.kt @@ -1,5 +1,6 @@ package com.bitwarden.authenticator.data.authenticator.repository +import android.net.Uri import app.cash.turbine.test import com.bitwarden.authenticator.data.authenticator.datasource.disk.util.FakeAuthenticatorDiskSource import com.bitwarden.authenticator.data.authenticator.datasource.entity.createMockAuthenticatorItemEntity @@ -7,20 +8,33 @@ import com.bitwarden.data.manager.file.FileManager import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem +import com.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult +import com.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult +import com.bitwarden.authenticator.data.authenticator.repository.model.ExportDataResult import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState +import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult import com.bitwarden.authenticator.data.authenticator.repository.util.toAuthenticatorItems import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager +import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult +import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat import com.bitwarden.authenticator.data.platform.repository.SettingsRepository +import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState import com.bitwarden.authenticatorbridge.model.SharedAccountData import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.core.data.util.mockBuilder +import com.bitwarden.ui.platform.model.FileData +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkConstructor import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkConstructor import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow @@ -39,7 +53,7 @@ class AuthenticatorRepositoryTest { private val mockAuthenticatorBridgeManager: AuthenticatorBridgeManager = mockk { every { accountSyncStateFlow } returns mutableAccountSyncStateFlow } - private val mockTotpCodeManager = mockk() + private val mockTotpCodeManager = mockk(relaxed = true) private val mockFileManager = mockk() private val mockImportManager = mockk() private val mockDispatcherManager = FakeDispatcherManager() @@ -59,22 +73,26 @@ class AuthenticatorRepositoryTest { @BeforeEach fun setup() { + mockkStatic(Uri::class) mockkStatic(List::toAuthenticatorItems) + + // Configure Uri.Builder for export tests that call toOtpAuthUriString() + val mockBuiltUri = mockk(relaxed = true) + every { mockBuiltUri.toString() } returns "otpauth://totp/mockIssuer:mockAccountName" + + mockkConstructor(Uri.Builder::class) + mockBuilder { it.scheme(any()) } + mockBuilder { it.authority(any()) } + mockBuilder { it.appendPath(any()) } + mockBuilder { it.appendQueryParameter(any(), any()) } + every { anyConstructed().build() } returns mockBuiltUri } @AfterEach fun teardown() { + unmockkStatic(Uri::class) unmockkStatic(List::toAuthenticatorItems) - } - - @Test - fun `ciphersStateFlow initial state should be loading`() = runTest { - authenticatorRepository.ciphersStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - } + unmockkConstructor(Uri.Builder::class) } @Test @@ -193,4 +211,222 @@ class AuthenticatorRepositoryTest { expectNoEvents() } } + + @Test + fun `getItemStateFlow with valid itemId should emit item when found`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + fakeAuthenticatorDiskSource.saveItem(mockItem) + + authenticatorRepository.getItemStateFlow(mockItem.id).test { + assertEquals(DataState.Loaded(mockItem), awaitItem()) + } + } + + @Test + fun `getItemStateFlow with invalid itemId should emit null`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + fakeAuthenticatorDiskSource.saveItem(mockItem) + + authenticatorRepository.getItemStateFlow("invalid-id").test { + assertEquals(DataState.Loaded(null), awaitItem()) + } + } + + @Test + fun `getItemStateFlow should emit Loaded with null for non-existent item`() = runTest { + authenticatorRepository.getItemStateFlow("any-id").test { + assertEquals(DataState.Loaded(null), awaitItem()) + } + } + + @Test + fun `emitTotpCodeResult should emit to totpCodeFlow`() = runTest { + val expectedResult = TotpCodeResult.TotpCodeScan("test-code") + + authenticatorRepository.totpCodeFlow.test { + authenticatorRepository.emitTotpCodeResult(expectedResult) + assertEquals(expectedResult, awaitItem()) + } + } + + @Test + fun `createItem with valid item should return Success`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + + val result = authenticatorRepository.createItem(mockItem) + + assertEquals(CreateItemResult.Success, result) + } + + @Test + fun `addItems with multiple items should return Success`() = runTest { + val mockItem1 = createMockAuthenticatorItemEntity(1) + val mockItem2 = createMockAuthenticatorItemEntity(2) + + val result = authenticatorRepository.addItems(mockItem1, mockItem2) + + assertEquals(CreateItemResult.Success, result) + } + + @Test + fun `hardDeleteItem with valid id should return Success`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + + val result = authenticatorRepository.hardDeleteItem(mockItem.id) + + assertEquals(DeleteItemResult.Success, result) + } + + @Test + fun `exportVaultData with JSON format should write to fileUri`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + fakeAuthenticatorDiskSource.saveItem(mockItem) + val mockUri = mockk() + + coEvery { + mockFileManager.stringToUri(fileUri = mockUri, dataString = any()) + } returns true + + val result = authenticatorRepository.exportVaultData( + format = ExportVaultFormat.JSON, + fileUri = mockUri, + ) + + assertEquals(ExportDataResult.Success, result) + coVerify { mockFileManager.stringToUri(fileUri = mockUri, dataString = any()) } + } + + @Test + fun `exportVaultData with JSON format failure should return Error`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + fakeAuthenticatorDiskSource.saveItem(mockItem) + val mockUri = mockk() + + coEvery { + mockFileManager.stringToUri(fileUri = mockUri, dataString = any()) + } returns false + + val result = authenticatorRepository.exportVaultData( + format = ExportVaultFormat.JSON, + fileUri = mockUri, + ) + + assertEquals(ExportDataResult.Error, result) + } + + @Test + fun `exportVaultData with CSV format should write to fileUri`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + fakeAuthenticatorDiskSource.saveItem(mockItem) + val mockUri = mockk() + + coEvery { + mockFileManager.stringToUri(fileUri = mockUri, dataString = any()) + } returns true + + val result = authenticatorRepository.exportVaultData( + format = ExportVaultFormat.CSV, + fileUri = mockUri, + ) + + assertEquals(ExportDataResult.Success, result) + coVerify { mockFileManager.stringToUri(fileUri = mockUri, dataString = any()) } + } + + @Test + fun `exportVaultData with CSV format failure should return Error`() = runTest { + val mockItem = createMockAuthenticatorItemEntity(1) + fakeAuthenticatorDiskSource.saveItem(mockItem) + val mockUri = mockk() + + coEvery { + mockFileManager.stringToUri(fileUri = mockUri, dataString = any()) + } returns false + + val result = authenticatorRepository.exportVaultData( + format = ExportVaultFormat.CSV, + fileUri = mockUri, + ) + + assertEquals(ExportDataResult.Error, result) + } + + @Test + fun `importVaultData with valid data should return Success`() = runTest { + val mockUri = mockk() + val mockFileData = FileData( + fileName = "test.json", + uri = mockUri, + sizeBytes = 100L, + ) + val testByteArray = byteArrayOf(1, 2, 3) + + coEvery { + mockFileManager.uriToByteArray(mockUri) + } returns Result.success(testByteArray) + + coEvery { + mockImportManager.import( + importFileFormat = ImportFileFormat.BITWARDEN_JSON, + byteArray = testByteArray, + ) + } returns ImportDataResult.Success + + val result = authenticatorRepository.importVaultData( + format = ImportFileFormat.BITWARDEN_JSON, + fileData = mockFileData, + ) + + assertEquals(ImportDataResult.Success, result) + } + + @Test + fun `importVaultData with FileManager failure should return Error`() = runTest { + val mockUri = mockk() + val mockFileData = FileData( + fileName = "test.json", + uri = mockUri, + sizeBytes = 100L, + ) + + coEvery { + mockFileManager.uriToByteArray(mockUri) + } returns Result.failure(RuntimeException("File read error")) + + val result = authenticatorRepository.importVaultData( + format = ImportFileFormat.BITWARDEN_JSON, + fileData = mockFileData, + ) + + assertEquals(ImportDataResult.Error(), result) + } + + @Test + fun `importVaultData with ImportManager failure should return Error`() = runTest { + val mockUri = mockk() + val mockFileData = FileData( + fileName = "test.json", + uri = mockUri, + sizeBytes = 100L, + ) + val testByteArray = byteArrayOf(1, 2, 3) + + coEvery { + mockFileManager.uriToByteArray(mockUri) + } returns Result.success(testByteArray) + + coEvery { + mockImportManager.import( + importFileFormat = ImportFileFormat.BITWARDEN_JSON, + byteArray = testByteArray, + ) + } returns ImportDataResult.Error() + + val result = authenticatorRepository.importVaultData( + format = ImportFileFormat.BITWARDEN_JSON, + fileData = mockFileData, + ) + + assertEquals(ImportDataResult.Error(), result) + } }