mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 02:06:52 -05:00
BIT-279: Adding password history data layer (#387)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.FakePasswordHistoryDao
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class PasswordHistoryDiskSourceTest {
|
||||
|
||||
private val fakePasswordHistoryDao = FakePasswordHistoryDao()
|
||||
private val diskSource = PasswordHistoryDiskSourceImpl(fakePasswordHistoryDao)
|
||||
private val testUserId = "testUserId"
|
||||
|
||||
@Test
|
||||
fun `insertPassword calls dao insertPasswordHistory`() = runTest {
|
||||
val passwordHistoryEntity = PasswordHistoryEntity(
|
||||
id = 0,
|
||||
userId = testUserId,
|
||||
encryptedPassword = "encrypted",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
|
||||
diskSource.insertPasswordHistory(passwordHistoryEntity)
|
||||
|
||||
assertTrue(fakePasswordHistoryDao.storedPasswordHistories.contains(passwordHistoryEntity))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasswordHistoriesForUser returns flow from dao`() = runTest {
|
||||
val passwordHistoryEntity = PasswordHistoryEntity(
|
||||
id = 0,
|
||||
userId = testUserId,
|
||||
encryptedPassword = "encrypted",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
fakePasswordHistoryDao.insertPasswordHistory(passwordHistoryEntity)
|
||||
|
||||
val result = diskSource
|
||||
.getPasswordHistoriesForUser(testUserId)
|
||||
.first()
|
||||
|
||||
assertEquals(listOf(passwordHistoryEntity), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearPasswordHistoriesForUser calls dao clearPasswordHistoriesForUser`() = runTest {
|
||||
fakePasswordHistoryDao.storedPasswordHistories.add(
|
||||
PasswordHistoryEntity(
|
||||
id = 1,
|
||||
userId = testUserId,
|
||||
encryptedPassword = "encrypted",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
),
|
||||
)
|
||||
|
||||
diskSource.clearPasswordHistories(testUserId)
|
||||
|
||||
assertTrue(fakePasswordHistoryDao.storedPasswordHistories.none { it.userId == testUserId })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao
|
||||
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class FakePasswordHistoryDao : PasswordHistoryDao {
|
||||
val storedPasswordHistories = mutableListOf<PasswordHistoryEntity>()
|
||||
|
||||
private val passwordHistoriesFlow = MutableSharedFlow<List<PasswordHistoryEntity>>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
init {
|
||||
passwordHistoriesFlow.tryEmit(emptyList())
|
||||
}
|
||||
|
||||
override suspend fun insertPasswordHistory(passwordHistory: PasswordHistoryEntity) {
|
||||
storedPasswordHistories.add(passwordHistory)
|
||||
passwordHistoriesFlow.tryEmit(storedPasswordHistories.toList())
|
||||
}
|
||||
|
||||
override fun getPasswordHistoriesForUserAsFlow(
|
||||
userId: String,
|
||||
): Flow<List<PasswordHistoryEntity>> {
|
||||
return passwordHistoriesFlow
|
||||
.map { histories -> histories.filter { it.userId == userId } }
|
||||
}
|
||||
|
||||
override suspend fun clearPasswordHistoriesForUser(userId: String) {
|
||||
storedPasswordHistories.removeAll { it.userId == userId }
|
||||
passwordHistoriesFlow.tryEmit(storedPasswordHistories.toList())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity
|
||||
|
||||
import com.bitwarden.core.PasswordHistory
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import java.time.Instant
|
||||
|
||||
class PasswordHistoryEntityTest {
|
||||
|
||||
@Test
|
||||
fun `toPasswordHistoryEntity should return the correct value`() {
|
||||
val passwordHistory = PasswordHistory(
|
||||
password = "testPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
val expectedEntity = PasswordHistoryEntity(
|
||||
id = 0,
|
||||
userId = "testId",
|
||||
encryptedPassword = "testPassword",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
val entity = passwordHistory.toPasswordHistoryEntity("testId")
|
||||
|
||||
assertEquals(expectedEntity, entity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toPasswordHistory should return the correct value`() {
|
||||
val entity = PasswordHistoryEntity(
|
||||
id = 1,
|
||||
userId = "testId",
|
||||
encryptedPassword = "testPassword",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
)
|
||||
val passwordHistory = entity.toPasswordHistory()
|
||||
val expectedPasswordHistory = PasswordHistory(
|
||||
password = "testPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
|
||||
assertEquals(expectedPasswordHistory, passwordHistory)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.PassphraseGeneratorRequest
|
||||
import com.bitwarden.core.PasswordGeneratorRequest
|
||||
import com.bitwarden.core.PasswordHistory
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
@@ -11,34 +14,45 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.toPasswordHistoryEntity
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import io.mockk.Runs
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class GeneratorRepositoryTest {
|
||||
|
||||
private val generatorSdkSource: GeneratorSdkSource = mockk()
|
||||
private val generatorDiskSource: GeneratorDiskSource = mockk()
|
||||
private val authDiskSource: AuthDiskSource = mockk()
|
||||
private val passwordHistoryDiskSource: PasswordHistoryDiskSource = mockk()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||
|
||||
private val repository = GeneratorRepositoryImpl(
|
||||
generatorSdkSource = generatorSdkSource,
|
||||
generatorDiskSource = generatorDiskSource,
|
||||
authDiskSource = authDiskSource,
|
||||
passwordHistoryDiskSource = passwordHistoryDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
@@ -213,13 +227,105 @@ class GeneratorRepositoryTest {
|
||||
|
||||
coEvery {
|
||||
generatorDiskSource.storePasscodeGenerationOptions(userId, optionsToSave)
|
||||
} just Runs
|
||||
} just runs
|
||||
|
||||
repository.savePasscodeGenerationOptions(optionsToSave)
|
||||
|
||||
coVerify { generatorDiskSource.storePasscodeGenerationOptions(userId, optionsToSave) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storePasswordHistory should call encrypt and insert functions`() = runTest {
|
||||
val testUserId = "testUserId"
|
||||
val passwordHistoryView = PasswordHistoryView(
|
||||
password = "decryptedPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
val encryptedPasswordHistory = PasswordHistory(
|
||||
password = "encryptedPassword",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
)
|
||||
val expectedPasswordHistoryEntity = encryptedPasswordHistory
|
||||
.toPasswordHistoryEntity(testUserId)
|
||||
|
||||
coEvery { authDiskSource.userState?.activeUserId } returns testUserId
|
||||
|
||||
coEvery { vaultSdkSource.encryptPasswordHistory(passwordHistoryView) } returns
|
||||
Result.success(encryptedPasswordHistory)
|
||||
|
||||
coEvery {
|
||||
passwordHistoryDiskSource.insertPasswordHistory(expectedPasswordHistoryEntity)
|
||||
} just runs
|
||||
|
||||
repository.storePasswordHistory(passwordHistoryView)
|
||||
|
||||
coVerify { vaultSdkSource.encryptPasswordHistory(passwordHistoryView) }
|
||||
coVerify { passwordHistoryDiskSource.insertPasswordHistory(expectedPasswordHistoryEntity) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwordHistoryStateFlow should emit correct states based on password history updates`() =
|
||||
runTest {
|
||||
val encryptedPasswordHistoryEntities = listOf(
|
||||
PasswordHistoryEntity(
|
||||
userId = USER_STATE.activeUserId,
|
||||
encryptedPassword = "encryptedPassword1",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-01T00:00:00Z").toEpochMilli(),
|
||||
),
|
||||
PasswordHistoryEntity(
|
||||
userId = USER_STATE.activeUserId,
|
||||
encryptedPassword = "encryptedPassword2",
|
||||
generatedDateTimeMs = Instant.parse("2021-01-02T00:00:00Z").toEpochMilli(),
|
||||
),
|
||||
)
|
||||
|
||||
val decryptedPasswordHistoryList = listOf(
|
||||
PasswordHistoryView(
|
||||
password = "password1",
|
||||
lastUsedDate = Instant.parse("2021-01-01T00:00:00Z"),
|
||||
),
|
||||
PasswordHistoryView(
|
||||
password = "password2",
|
||||
lastUsedDate = Instant.parse("2021-01-02T00:00:00Z"),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery { authDiskSource.userStateFlow } returns flowOf(USER_STATE)
|
||||
|
||||
coEvery {
|
||||
passwordHistoryDiskSource.getPasswordHistoriesForUser(USER_STATE.activeUserId)
|
||||
} returns flowOf(encryptedPasswordHistoryEntities)
|
||||
|
||||
coEvery {
|
||||
vaultSdkSource.decryptPasswordHistoryList(any())
|
||||
} returns Result.success(decryptedPasswordHistoryList)
|
||||
|
||||
val historyFlow = repository.passwordHistoryStateFlow
|
||||
|
||||
historyFlow.test {
|
||||
assertEquals(LocalDataState.Loading, awaitItem())
|
||||
assertEquals(LocalDataState.Loaded(decryptedPasswordHistoryList), awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
||||
coVerify {
|
||||
passwordHistoryDiskSource.getPasswordHistoriesForUser(USER_STATE.activeUserId)
|
||||
}
|
||||
|
||||
coVerify { vaultSdkSource.decryptPasswordHistoryList(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearPasswordHistory should call clearAllPasswords function`() = runTest {
|
||||
val testUserId = "testUserId"
|
||||
coEvery { authDiskSource.userState?.activeUserId } returns testUserId
|
||||
coEvery { passwordHistoryDiskSource.clearPasswordHistories(testUserId) } just runs
|
||||
|
||||
repository.clearPasswordHistory()
|
||||
|
||||
coVerify { passwordHistoryDiskSource.clearPasswordHistories(testUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `savePasscodeGenerationOptions should not store options when there is no active user`() =
|
||||
runTest {
|
||||
|
||||
@@ -2,10 +2,14 @@ package com.x8bit.bitwarden.data.tools.generator.repository.util
|
||||
|
||||
import com.bitwarden.core.PassphraseGeneratorRequest
|
||||
import com.bitwarden.core.PasswordGeneratorRequest
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPassphraseResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
|
||||
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A fake implementation of [GeneratorRepository] for testing purposes.
|
||||
@@ -21,6 +25,12 @@ class FakeGeneratorRepository : GeneratorRepository {
|
||||
)
|
||||
private var passcodeGenerationOptions: PasscodeGenerationOptions? = null
|
||||
|
||||
private val mutablePasswordHistoryStateFlow =
|
||||
MutableStateFlow<LocalDataState<List<PasswordHistoryView>>>(LocalDataState.Loading)
|
||||
|
||||
override val passwordHistoryStateFlow: StateFlow<LocalDataState<List<PasswordHistoryView>>>
|
||||
get() = mutablePasswordHistoryStateFlow
|
||||
|
||||
override suspend fun generatePassword(
|
||||
passwordGeneratorRequest: PasswordGeneratorRequest,
|
||||
): GeneratedPasswordResult {
|
||||
@@ -41,6 +51,16 @@ class FakeGeneratorRepository : GeneratorRepository {
|
||||
passcodeGenerationOptions = options
|
||||
}
|
||||
|
||||
override suspend fun storePasswordHistory(passwordHistoryView: PasswordHistoryView) {
|
||||
val currentList = mutablePasswordHistoryStateFlow.value.data.orEmpty()
|
||||
val updatedList = currentList + passwordHistoryView
|
||||
mutablePasswordHistoryStateFlow.value = LocalDataState.Loaded(updatedList)
|
||||
}
|
||||
|
||||
override suspend fun clearPasswordHistory() {
|
||||
mutablePasswordHistoryStateFlow.value = LocalDataState.Loaded(emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mock result for the generatePassword function.
|
||||
*/
|
||||
|
||||
@@ -9,10 +9,13 @@ import com.bitwarden.core.Folder
|
||||
import com.bitwarden.core.FolderView
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.PasswordHistory
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.bitwarden.core.Send
|
||||
import com.bitwarden.core.SendView
|
||||
import com.bitwarden.sdk.BitwardenException
|
||||
import com.bitwarden.sdk.ClientCrypto
|
||||
import com.bitwarden.sdk.ClientPasswordHistory
|
||||
import com.bitwarden.sdk.ClientVault
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
@@ -27,9 +30,11 @@ import org.junit.jupiter.api.Test
|
||||
class VaultSdkSourceTest {
|
||||
private val clientVault = mockk<ClientVault>()
|
||||
private val clientCrypto = mockk<ClientCrypto>()
|
||||
private val clientPasswordHistory = mockk<ClientPasswordHistory>()
|
||||
private val vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
clientVault = clientVault,
|
||||
clientCrypto = clientCrypto,
|
||||
clientPasswordHistory = clientPasswordHistory,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -404,4 +409,50 @@ class VaultSdkSourceTest {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encryptPasswordHistory should call SDK and return a Result with correct data`() =
|
||||
runBlocking {
|
||||
val mockPasswordHistoryView = mockk<PasswordHistoryView>()
|
||||
val expectedResult = mockk<PasswordHistory>()
|
||||
coEvery {
|
||||
clientPasswordHistory.encrypt(
|
||||
passwordHistory = mockPasswordHistoryView,
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = vaultSdkSource.encryptPasswordHistory(
|
||||
passwordHistory = mockPasswordHistoryView,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult.asSuccess(), result)
|
||||
coVerify {
|
||||
clientPasswordHistory.encrypt(
|
||||
passwordHistory = mockPasswordHistoryView,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decryptPasswordHistoryList should call SDK and return a Result with correct data`() =
|
||||
runBlocking {
|
||||
val mockPasswordHistoryList = mockk<List<PasswordHistory>>()
|
||||
val expectedResult = mockk<List<PasswordHistoryView>>()
|
||||
coEvery {
|
||||
clientPasswordHistory.decryptList(
|
||||
list = mockPasswordHistoryList,
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = vaultSdkSource.decryptPasswordHistoryList(
|
||||
passwordHistoryList = mockPasswordHistoryList,
|
||||
)
|
||||
|
||||
assertEquals(expectedResult.asSuccess(), result)
|
||||
coVerify {
|
||||
clientPasswordHistory.decryptList(
|
||||
list = mockPasswordHistoryList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user