Add comprehensive tests for AuthenticatorRepositoryImpl (#6424)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Honkonen 2026-01-30 10:41:21 -05:00 committed by GitHub
parent 99a6dd7647
commit 0f087b7d15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 251 additions and 13 deletions

View File

@ -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.AuthenticatorDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity 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.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.onSubscription
class FakeAuthenticatorDiskSource : AuthenticatorDiskSource { class FakeAuthenticatorDiskSource : AuthenticatorDiskSource {
private val mutableItemFlow = MutableSharedFlow<List<AuthenticatorItemEntity>>() private val mutableItemFlow = bufferedMutableSharedFlow<List<AuthenticatorItemEntity>>()
private val storedItems = mutableListOf<AuthenticatorItemEntity>() private val storedItems = mutableListOf<AuthenticatorItemEntity>()
override suspend fun saveItem(vararg authenticatorItem: AuthenticatorItemEntity) { override suspend fun saveItem(vararg authenticatorItem: AuthenticatorItemEntity) {
@ -15,6 +16,7 @@ class FakeAuthenticatorDiskSource : AuthenticatorDiskSource {
} }
override fun getItems(): Flow<List<AuthenticatorItemEntity>> = mutableItemFlow override fun getItems(): Flow<List<AuthenticatorItemEntity>> = mutableItemFlow
.onSubscription { emit(storedItems) }
override suspend fun deleteItem(itemId: String) { override suspend fun deleteItem(itemId: String) {
storedItems.removeIf { it.id == itemId } storedItems.removeIf { it.id == itemId }

View File

@ -1,5 +1,6 @@
package com.bitwarden.authenticator.data.authenticator.repository package com.bitwarden.authenticator.data.authenticator.repository
import android.net.Uri
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.authenticator.data.authenticator.datasource.disk.util.FakeAuthenticatorDiskSource import com.bitwarden.authenticator.data.authenticator.datasource.disk.util.FakeAuthenticatorDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.entity.createMockAuthenticatorItemEntity 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.TotpCodeManager
import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem 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.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.SharedVerificationCodesState
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
import com.bitwarden.authenticator.data.authenticator.repository.util.toAuthenticatorItems 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.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.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.AuthenticatorBridgeManager
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
import com.bitwarden.authenticatorbridge.model.SharedAccountData import com.bitwarden.authenticatorbridge.model.SharedAccountData
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.core.data.repository.model.DataState 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.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.unmockkConstructor
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -39,7 +53,7 @@ class AuthenticatorRepositoryTest {
private val mockAuthenticatorBridgeManager: AuthenticatorBridgeManager = mockk { private val mockAuthenticatorBridgeManager: AuthenticatorBridgeManager = mockk {
every { accountSyncStateFlow } returns mutableAccountSyncStateFlow every { accountSyncStateFlow } returns mutableAccountSyncStateFlow
} }
private val mockTotpCodeManager = mockk<TotpCodeManager>() private val mockTotpCodeManager = mockk<TotpCodeManager>(relaxed = true)
private val mockFileManager = mockk<FileManager>() private val mockFileManager = mockk<FileManager>()
private val mockImportManager = mockk<ImportManager>() private val mockImportManager = mockk<ImportManager>()
private val mockDispatcherManager = FakeDispatcherManager() private val mockDispatcherManager = FakeDispatcherManager()
@ -59,22 +73,26 @@ class AuthenticatorRepositoryTest {
@BeforeEach @BeforeEach
fun setup() { fun setup() {
mockkStatic(Uri::class)
mockkStatic(List<SharedAccountData.Account>::toAuthenticatorItems) mockkStatic(List<SharedAccountData.Account>::toAuthenticatorItems)
// Configure Uri.Builder for export tests that call toOtpAuthUriString()
val mockBuiltUri = mockk<Uri>(relaxed = true)
every { mockBuiltUri.toString() } returns "otpauth://totp/mockIssuer:mockAccountName"
mockkConstructor(Uri.Builder::class)
mockBuilder<Uri.Builder> { it.scheme(any()) }
mockBuilder<Uri.Builder> { it.authority(any()) }
mockBuilder<Uri.Builder> { it.appendPath(any()) }
mockBuilder<Uri.Builder> { it.appendQueryParameter(any(), any()) }
every { anyConstructed<Uri.Builder>().build() } returns mockBuiltUri
} }
@AfterEach @AfterEach
fun teardown() { fun teardown() {
unmockkStatic(Uri::class)
unmockkStatic(List<SharedAccountData.Account>::toAuthenticatorItems) unmockkStatic(List<SharedAccountData.Account>::toAuthenticatorItems)
} unmockkConstructor(Uri.Builder::class)
@Test
fun `ciphersStateFlow initial state should be loading`() = runTest {
authenticatorRepository.ciphersStateFlow.test {
assertEquals(
DataState.Loading,
awaitItem(),
)
}
} }
@Test @Test
@ -193,4 +211,222 @@ class AuthenticatorRepositoryTest {
expectNoEvents() 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<Uri>()
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<Uri>()
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<Uri>()
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<Uri>()
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<Uri>()
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<Uri>()
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<Uri>()
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)
}
} }