BIT-279: Adding password history data layer (#387)

This commit is contained in:
joshua-livefront
2023-12-14 12:25:35 -05:00
committed by GitHub
parent 9905b96211
commit 3f67f130fa
20 changed files with 693 additions and 2 deletions

View File

@@ -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 })
}
}

View File

@@ -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())
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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.
*/

View File

@@ -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,
)
}
}
}