BIT-653: Adding data source for passphrase generation (#273)

This commit is contained in:
joshua-livefront
2023-11-22 18:22:56 -05:00
committed by GitHub
parent ddc7b0b713
commit fa93d4cc6b
14 changed files with 266 additions and 72 deletions

View File

@@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.disk
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasswordGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
@@ -25,9 +25,9 @@ class GeneratorDiskSourceTest {
)
@Test
fun `getPasswordGenerationOptions should return correct options when available`() {
fun `getPasscodeGenerationOptions should return correct options when available`() {
val userId = "user123"
val options = PasswordGenerationOptions(
val options = PasscodeGenerationOptions(
length = 14,
allowAmbiguousChar = false,
hasNumbers = true,
@@ -38,29 +38,33 @@ class GeneratorDiskSourceTest {
minLowercase = null,
allowSpecial = false,
minSpecial = 1,
allowCapitalize = false,
allowIncludeNumber = false,
wordSeparator = "-",
numWords = 3,
)
val key = "bwPreferencesStorage_passwordGenerationOptions_$userId"
fakeSharedPreferences.edit().putString(key, json.encodeToString(options)).apply()
val result = generatorDiskSource.getPasswordGenerationOptions(userId)
val result = generatorDiskSource.getPasscodeGenerationOptions(userId)
assertEquals(options, result)
}
@Test
fun `getPasswordGenerationOptions should return null when options are not available`() {
fun `getPasscodeGenerationOptions should return null when options are not available`() {
val userId = "user123"
val result = generatorDiskSource.getPasswordGenerationOptions(userId)
val result = generatorDiskSource.getPasscodeGenerationOptions(userId)
assertNull(result)
}
@Test
fun `storePasswordGenerationOptions should correctly store options`() {
fun `storePasscodeGenerationOptions should correctly store options`() {
val userId = "user123"
val options = PasswordGenerationOptions(
val options = PasscodeGenerationOptions(
length = 14,
allowAmbiguousChar = false,
hasNumbers = true,
@@ -71,11 +75,15 @@ class GeneratorDiskSourceTest {
minLowercase = null,
allowSpecial = false,
minSpecial = 1,
allowCapitalize = false,
allowIncludeNumber = false,
wordSeparator = "-",
numWords = 3,
)
val key = "bwPreferencesStorage_passwordGenerationOptions_$userId"
generatorDiskSource.storePasswordGenerationOptions(userId, options)
generatorDiskSource.storePasscodeGenerationOptions(userId, options)
val storedValue = fakeSharedPreferences.getString(key, null)
assertNotNull(storedValue)

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.sdk
import com.bitwarden.core.PassphraseGeneratorRequest
import com.bitwarden.core.PasswordGeneratorRequest
import com.bitwarden.sdk.ClientGenerators
import io.mockk.coEvery
@@ -42,4 +43,28 @@ class GeneratorSdkSourceTest {
clientGenerators.password(request)
}
}
@Suppress("MaxLineLength")
@Test
fun `generatePassphrase should call SDK and return a Result with the generated passphrase`() = runBlocking {
val request = PassphraseGeneratorRequest(
numWords = 4.toUByte(),
wordSeparator = "-",
capitalize = true,
includeNumber = true,
)
val expectedResult = "Generated-Passphrase123"
coEvery {
clientGenerators.passphrase(request)
} returns expectedResult
val result = generatorSdkSource.generatePassphrase(request)
assertEquals(Result.success(expectedResult), result)
coVerify {
clientGenerators.passphrase(request)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.tools.generator.repository
import com.bitwarden.core.PassphraseGeneratorRequest
import com.bitwarden.core.PasswordGeneratorRequest
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
@@ -12,8 +13,9 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserD
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
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.PasswordGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import io.mockk.Runs
import io.mockk.clearMocks
import io.mockk.coEvery
@@ -93,9 +95,49 @@ class GeneratorRepositoryTest {
}
@Test
fun `getPasswordGenerationOptions should return options when available`() = runTest {
fun `generatePassphrase should emit Success result with the generated passphrase`() = runTest {
val request = PassphraseGeneratorRequest(
numWords = 5.toUByte(),
capitalize = true,
includeNumber = true,
wordSeparator = '-'.toString(),
)
val expectedResult = "Generated-Passphrase-123!"
coEvery {
generatorSdkSource.generatePassphrase(request)
} returns Result.success(expectedResult)
val result = repository.generatePassphrase(request)
assertEquals(expectedResult, (result as GeneratedPassphraseResult.Success).generatedString)
coVerify { generatorSdkSource.generatePassphrase(request) }
}
@Suppress("MaxLineLength")
@Test
fun `generatePassphrase should emit InvalidRequest result when SDK throws exception`() =
runTest {
val request = PassphraseGeneratorRequest(
numWords = 5.toUByte(),
capitalize = true,
includeNumber = true,
wordSeparator = '-'.toString(),
)
val exception = RuntimeException("An error occurred")
coEvery { generatorSdkSource.generatePassphrase(request) } returns Result.failure(
exception,
)
val result = repository.generatePassphrase(request)
assertTrue(result is GeneratedPassphraseResult.InvalidRequest)
coVerify { generatorSdkSource.generatePassphrase(request) }
}
@Test
fun `getPasscodeGenerationOptions should return options when available`() = runTest {
val userId = "activeUserId"
val expectedOptions = PasswordGenerationOptions(
val expectedOptions = PasscodeGenerationOptions(
length = 14,
allowAmbiguousChar = false,
hasNumbers = true,
@@ -106,47 +148,51 @@ class GeneratorRepositoryTest {
minLowercase = null,
allowSpecial = false,
minSpecial = 1,
allowCapitalize = false,
allowIncludeNumber = false,
wordSeparator = "-",
numWords = 3,
)
coEvery { authDiskSource.userState } returns USER_STATE
coEvery {
generatorDiskSource.getPasswordGenerationOptions(userId)
generatorDiskSource.getPasscodeGenerationOptions(userId)
} returns expectedOptions
val result = repository.getPasswordGenerationOptions()
val result = repository.getPasscodeGenerationOptions()
assertEquals(expectedOptions, result)
coVerify { generatorDiskSource.getPasswordGenerationOptions(userId) }
coVerify { generatorDiskSource.getPasscodeGenerationOptions(userId) }
}
@Test
fun `getPasswordGenerationOptions should return null when there is no active user`() = runTest {
fun `getPasscodeGenerationOptions should return null when there is no active user`() = runTest {
coEvery { authDiskSource.userState } returns null
val result = repository.getPasswordGenerationOptions()
val result = repository.getPasscodeGenerationOptions()
assertNull(result)
coVerify(exactly = 0) { generatorDiskSource.getPasswordGenerationOptions(any()) }
coVerify(exactly = 0) { generatorDiskSource.getPasscodeGenerationOptions(any()) }
}
@Suppress("MaxLineLength")
@Test
fun `getPasswordGenerationOptions should return null when no data is stored for active user`() = runTest {
fun `getPasscodeGenerationOptions should return null when no data is stored for active user`() = runTest {
val userId = "activeUserId"
coEvery { authDiskSource.userState } returns USER_STATE
coEvery { generatorDiskSource.getPasswordGenerationOptions(userId) } returns null
coEvery { generatorDiskSource.getPasscodeGenerationOptions(userId) } returns null
val result = repository.getPasswordGenerationOptions()
val result = repository.getPasscodeGenerationOptions()
assertNull(result)
coVerify { generatorDiskSource.getPasswordGenerationOptions(userId) }
coVerify { generatorDiskSource.getPasscodeGenerationOptions(userId) }
}
@Test
fun `savePasswordGenerationOptions should store options correctly`() = runTest {
fun `savePasscodeGenerationOptions should store options correctly`() = runTest {
val userId = "activeUserId"
val optionsToSave = PasswordGenerationOptions(
val optionsToSave = PasscodeGenerationOptions(
length = 14,
allowAmbiguousChar = false,
hasNumbers = true,
@@ -157,23 +203,27 @@ class GeneratorRepositoryTest {
minLowercase = null,
allowSpecial = false,
minSpecial = 1,
allowCapitalize = false,
allowIncludeNumber = false,
wordSeparator = "-",
numWords = 3,
)
coEvery { authDiskSource.userState } returns USER_STATE
coEvery {
generatorDiskSource.storePasswordGenerationOptions(userId, optionsToSave)
generatorDiskSource.storePasscodeGenerationOptions(userId, optionsToSave)
} just Runs
repository.savePasswordGenerationOptions(optionsToSave)
repository.savePasscodeGenerationOptions(optionsToSave)
coVerify { generatorDiskSource.storePasswordGenerationOptions(userId, optionsToSave) }
coVerify { generatorDiskSource.storePasscodeGenerationOptions(userId, optionsToSave) }
}
@Suppress("MaxLineLength")
@Test
fun `savePasswordGenerationOptions should not store options when there is no active user`() = runTest {
val optionsToSave = PasswordGenerationOptions(
fun `savePasscodeGenerationOptions should not store options when there is no active user`() = runTest {
val optionsToSave = PasscodeGenerationOptions(
length = 14,
allowAmbiguousChar = false,
hasNumbers = true,
@@ -184,13 +234,17 @@ class GeneratorRepositoryTest {
minLowercase = null,
allowSpecial = false,
minSpecial = 1,
allowCapitalize = false,
allowIncludeNumber = false,
wordSeparator = "-",
numWords = 3,
)
coEvery { authDiskSource.userState } returns null
repository.savePasswordGenerationOptions(optionsToSave)
repository.savePasscodeGenerationOptions(optionsToSave)
coVerify(exactly = 0) { generatorDiskSource.storePasswordGenerationOptions(any(), any()) }
coVerify(exactly = 0) { generatorDiskSource.storePasscodeGenerationOptions(any(), any()) }
}
private val USER_STATE = UserStateJson(

View File

@@ -1,9 +1,11 @@
package com.x8bit.bitwarden.data.tools.generator.repository.util
import com.bitwarden.core.PassphraseGeneratorRequest
import com.bitwarden.core.PasswordGeneratorRequest
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.PasswordGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
/**
* A fake implementation of [GeneratorRepository] for testing purposes.
@@ -13,7 +15,11 @@ class FakeGeneratorRepository : GeneratorRepository {
private var generatePasswordResult: GeneratedPasswordResult = GeneratedPasswordResult.Success(
generatedString = "updatedText",
)
private var passwordGenerationOptions: PasswordGenerationOptions? = null
private var generatePassphraseResult: GeneratedPassphraseResult =
GeneratedPassphraseResult.Success(
generatedString = "updatedPassphrase",
)
private var passcodeGenerationOptions: PasscodeGenerationOptions? = null
override suspend fun generatePassword(
passwordGeneratorRequest: PasswordGeneratorRequest,
@@ -21,12 +27,18 @@ class FakeGeneratorRepository : GeneratorRepository {
return generatePasswordResult
}
override fun getPasswordGenerationOptions(): PasswordGenerationOptions? {
return passwordGenerationOptions
override suspend fun generatePassphrase(
passphraseGeneratorRequest: PassphraseGeneratorRequest,
): GeneratedPassphraseResult {
return generatePassphraseResult
}
override fun savePasswordGenerationOptions(options: PasswordGenerationOptions) {
passwordGenerationOptions = options
override fun getPasscodeGenerationOptions(): PasscodeGenerationOptions? {
return passcodeGenerationOptions
}
override fun savePasscodeGenerationOptions(options: PasscodeGenerationOptions) {
passcodeGenerationOptions = options
}
/**
@@ -37,9 +49,9 @@ class FakeGeneratorRepository : GeneratorRepository {
}
/**
* Sets the mock password generation options.
* Sets the mock result for the generatePassphrase function.
*/
fun setMockGeneratePasswordGenerationOptions(options: PasswordGenerationOptions?) {
passwordGenerationOptions = options
fun setMockGeneratePassphraseResult(result: GeneratedPassphraseResult) {
generatePassphraseResult = result
}
}

View File

@@ -4,7 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedPasswordResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasswordGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.tools.generator.repository.util.FakeGeneratorRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
@@ -48,7 +48,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val initialState = viewModel.stateFlow.value
val updatedPasswordOptions = PasswordGenerationOptions(
val updatedPasswordOptions = PasscodeGenerationOptions(
length = 14,
allowAmbiguousChar = false,
hasNumbers = true,
@@ -59,6 +59,10 @@ class GeneratorViewModelTest : BaseViewModelTest() {
minLowercase = null,
allowSpecial = false,
minSpecial = 1,
allowCapitalize = false,
allowIncludeNumber = false,
wordSeparator = "-",
numWords = 3,
)
fakeGeneratorRepository.setMockGeneratePasswordResult(
@@ -72,7 +76,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(
updatedPasswordOptions,
fakeGeneratorRepository.getPasswordGenerationOptions(),
fakeGeneratorRepository.getPasscodeGenerationOptions(),
)
}