BIT-1336: Adding random word username generation (#540)

This commit is contained in:
Joshua Queen
2024-01-08 17:50:17 -05:00
committed by GitHub
parent 586b426f20
commit ecab23fe58
10 changed files with 201 additions and 1 deletions

View File

@@ -33,6 +33,13 @@ interface GeneratorSdkSource {
request: UsernameGeneratorRequest.Catchall,
): Result<String>
/**
* Generates a random word username returning a [String] wrapped in a [Result].
*/
suspend fun generateRandomWord(
request: UsernameGeneratorRequest.Word,
): Result<String>
/**
* Generates a forwarded service email returning a [String] wrapped in a [Result].
*/

View File

@@ -38,6 +38,12 @@ class GeneratorSdkSourceImpl(
clientGenerator.username(request)
}
override suspend fun generateRandomWord(
request: UsernameGeneratorRequest.Word,
): Result<String> = runCatching {
clientGenerator.username(request)
}
override suspend fun generateForwardedServiceEmail(
request: UsernameGeneratorRequest.Forwarded,
): Result<String> = runCatching {

View File

@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
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.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import kotlinx.coroutines.flow.StateFlow
@@ -54,6 +55,13 @@ interface GeneratorRepository {
catchAllEmailGeneratorRequest: UsernameGeneratorRequest.Catchall,
): GeneratedCatchAllUsernameResult
/**
* Attempt to generate a random word username.
*/
suspend fun generateRandomWordUsername(
randomWordGeneratorRequest: UsernameGeneratorRequest.Word,
): GeneratedRandomWordUsernameResult
/**
* Attempt to generate a forwarded service username.
*/

View File

@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package com.x8bit.bitwarden.data.tools.generator.repository
import com.bitwarden.core.PassphraseGeneratorRequest
@@ -18,6 +20,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
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.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.CoroutineScope
@@ -154,6 +157,19 @@ class GeneratorRepositoryImpl(
},
)
override suspend fun generateRandomWordUsername(
randomWordGeneratorRequest: UsernameGeneratorRequest.Word,
): GeneratedRandomWordUsernameResult =
generatorSdkSource.generateRandomWord(randomWordGeneratorRequest)
.fold(
onSuccess = { generatedUsername ->
GeneratedRandomWordUsernameResult.Success(generatedUsername)
},
onFailure = {
GeneratedRandomWordUsernameResult.InvalidRequest
},
)
override suspend fun generateForwardedServiceUsername(
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
): GeneratedForwardedServiceUsernameResult =

View File

@@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.tools.generator.repository.model
/**
* Represents the outcome of a generator operation.
*/
sealed class GeneratedRandomWordUsernameResult {
/**
* Operation succeeded with a value.
*/
data class Success(
val generatedUsername: String,
) : GeneratedRandomWordUsernameResult()
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedRandomWordUsernameResult()
}

View File

@@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
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.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
@@ -134,6 +135,10 @@ class GeneratorViewModel @Inject constructor(
handleUpdateCatchAllGeneratedUsernameResult(action)
}
is GeneratorAction.Internal.UpdateGeneratedRandomWordUsernameResult -> {
handleUpdateRandomWordGeneratedUsernameResult(action)
}
is GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult -> {
handleUpdateForwadedServiceGeneratedUsernameResult(action)
}
@@ -402,6 +407,22 @@ class GeneratorViewModel @Inject constructor(
}
}
private fun handleUpdateRandomWordGeneratedUsernameResult(
action: GeneratorAction.Internal.UpdateGeneratedRandomWordUsernameResult,
) {
when (val result = action.result) {
is GeneratedRandomWordUsernameResult.Success -> {
mutableStateFlow.update {
it.copy(generatedText = result.generatedUsername)
}
}
GeneratedRandomWordUsernameResult.InvalidRequest -> {
sendEvent(GeneratorEvent.ShowSnackbar(R.string.an_error_has_occurred.asText()))
}
}
}
private fun handleUpdateForwadedServiceGeneratedUsernameResult(
action: GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult,
) {
@@ -959,7 +980,9 @@ class GeneratorViewModel @Inject constructor(
}
is RandomWord -> {
// TODO: Implement random word generation (BIT-1336)
if (isManualRegeneration) {
generateRandomWordUsername(selectedType)
}
}
}
}
@@ -992,6 +1015,15 @@ class GeneratorViewModel @Inject constructor(
sendAction(GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult(result))
}
private suspend fun generateRandomWordUsername(randomWord: RandomWord) {
val result = generatorRepository.generateRandomWordUsername(
UsernameGeneratorRequest.Word(
capitalize = randomWord.capitalize,
includeNumber = randomWord.includeNumber,
),
)
sendAction(GeneratorAction.Internal.UpdateGeneratedRandomWordUsernameResult(result))
}
private inline fun updateGeneratorMainTypePasscode(
crossinline block: (Passcode) -> Passcode,
) {
@@ -1969,6 +2001,13 @@ sealed class GeneratorAction {
val result: GeneratedCatchAllUsernameResult,
) : Internal()
/**
* Indicates a generated text update is received.
*/
data class UpdateGeneratedRandomWordUsernameResult(
val result: GeneratedRandomWordUsernameResult,
) : Internal()
/**
* Indicates a generated text update is received.
*/

View File

@@ -115,6 +115,28 @@ class GeneratorSdkSourceTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `generateRandomWordUsername should call SDK and return a Result with the generated email`() =
runBlocking {
val request = UsernameGeneratorRequest.Word(
capitalize = true,
includeNumber = true,
)
val expectedResult = "USER1"
coEvery {
clientGenerators.username(request)
} returns expectedResult
val result = generatorSdkSource.generateRandomWord(request)
assertEquals(Result.success(expectedResult), result)
coVerify {
clientGenerators.username(request)
}
}
@Suppress("MaxLineLength")
@Test
fun `generateForwardedServiceEmail should call SDK and return a Result with the generated email`() =

View File

@@ -29,6 +29,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
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.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import io.mockk.coEvery
@@ -344,6 +345,45 @@ class GeneratorRepositoryTest {
coVerify { generatorSdkSource.generateCatchAllEmail(request) }
}
@Suppress("MaxLineLength")
@Test
fun `generateRandomWord should return Success with generated email when SDK call is successful`() = runTest {
val userId = "testUserId"
val request = UsernameGeneratorRequest.Word(
capitalize = false,
includeNumber = false,
)
val generatedEmail = "user"
coEvery { generatorSdkSource.generateRandomWord(request) } returns
Result.success(generatedEmail)
val result = repository.generateRandomWordUsername(request)
assertEquals(
generatedEmail,
(result as GeneratedRandomWordUsernameResult.Success).generatedUsername,
)
coVerify { generatorSdkSource.generateRandomWord(request) }
}
@Test
fun `generateRandomWord should return InvalidRequest on SDK failure`() = runTest {
val request = UsernameGeneratorRequest.Word(
capitalize = false,
includeNumber = false,
)
val exception = RuntimeException("An error occurred")
coEvery {
generatorSdkSource.generateRandomWord(request)
} returns Result.failure(exception)
val result = repository.generateRandomWordUsername(request)
assertTrue(result is GeneratedRandomWordUsernameResult.InvalidRequest)
coVerify { generatorSdkSource.generateRandomWord(request) }
}
@Test
fun `generateForwardedService should emit Success result and store the generated email`() =
runTest {

View File

@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwar
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.GeneratedPlusAddressedUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedRandomWordUsernameResult
import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -42,6 +43,11 @@ class FakeGeneratorRepository : GeneratorRepository {
generatedEmailAddress = "user@domain",
)
private var generateRandomWordUsernameResult: GeneratedRandomWordUsernameResult =
GeneratedRandomWordUsernameResult.Success(
generatedUsername = "randomWord",
)
private var generateForwardedServiceResult: GeneratedForwardedServiceUsernameResult =
GeneratedForwardedServiceUsernameResult.Success(
generatedEmailAddress = "updatedUsername",
@@ -75,6 +81,12 @@ class FakeGeneratorRepository : GeneratorRepository {
return generateCatchAllEmailResult
}
override suspend fun generateRandomWordUsername(
randomWordGeneratorRequest: UsernameGeneratorRequest.Word,
): GeneratedRandomWordUsernameResult {
return generateRandomWordUsernameResult
}
override suspend fun generateForwardedServiceUsername(
forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded,
): GeneratedForwardedServiceUsernameResult {
@@ -141,4 +153,11 @@ class FakeGeneratorRepository : GeneratorRepository {
fun setMockCatchAllResult(result: GeneratedCatchAllUsernameResult) {
generateCatchAllEmailResult = result
}
/**
* Sets the mock result for the generateRandomWord function.
*/
fun setMockRandomWordResult(result: GeneratedRandomWordUsernameResult) {
generateRandomWordUsernameResult = result
}
}

View File

@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchA
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedForwardedServiceUsernameResult
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.GeneratedRandomWordUsernameResult
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
@@ -260,6 +261,29 @@ class GeneratorViewModelTest : BaseViewModelTest() {
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `RegenerateClick for random word username state should update the random word username correctly`() =
runTest {
val viewModel = createViewModel(randomWordSavedStateHandle)
fakeGeneratorRepository.setMockRandomWordResult(
GeneratedRandomWordUsernameResult.Success("DifferentUsername"),
)
viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick)
val expectedState =
initialCatchAllEmailState.copy(
generatedText = "DifferentUsername",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.RandomWord(),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `CopyClick should call setText on ClipboardManager`() {
val viewModel = createViewModel()