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

View File

@ -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<TotpCodeManager>()
private val mockTotpCodeManager = mockk<TotpCodeManager>(relaxed = true)
private val mockFileManager = mockk<FileManager>()
private val mockImportManager = mockk<ImportManager>()
private val mockDispatcherManager = FakeDispatcherManager()
@ -59,22 +73,26 @@ class AuthenticatorRepositoryTest {
@BeforeEach
fun setup() {
mockkStatic(Uri::class)
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
fun teardown() {
unmockkStatic(Uri::class)
unmockkStatic(List<SharedAccountData.Account>::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<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)
}
}