diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSource.kt index 79fa58b8f2..d2b7e530ea 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSource.kt @@ -33,6 +33,13 @@ interface GeneratorSdkSource { request: UsernameGeneratorRequest.Catchall, ): Result + /** + * Generates a random word username returning a [String] wrapped in a [Result]. + */ + suspend fun generateRandomWord( + request: UsernameGeneratorRequest.Word, + ): Result + /** * Generates a forwarded service email returning a [String] wrapped in a [Result]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt index a65fd2bc58..b5dd754ef6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceImpl.kt @@ -38,6 +38,12 @@ class GeneratorSdkSourceImpl( clientGenerator.username(request) } + override suspend fun generateRandomWord( + request: UsernameGeneratorRequest.Word, + ): Result = runCatching { + clientGenerator.username(request) + } + override suspend fun generateForwardedServiceEmail( request: UsernameGeneratorRequest.Forwarded, ): Result = runCatching { diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepository.kt index 01ef2f9077..0c097f7d1d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepository.kt @@ -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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryImpl.kt index 0b577abac5..185a748892 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryImpl.kt @@ -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 = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedRandomWordUsernameResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedRandomWordUsernameResult.kt new file mode 100644 index 0000000000..fb156b7d48 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedRandomWordUsernameResult.kt @@ -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() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt index fe52a4464b..952b890a60 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt @@ -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. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt index c4b77e8d1b..9f3a4df7cd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/datasource/sdk/GeneratorSdkSourceTest.kt @@ -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`() = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt index 7eb2011dde..bdddb9203a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt @@ -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 { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/util/FakeGeneratorRepository.kt b/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/util/FakeGeneratorRepository.kt index 3cf9162649..ca2ba51581 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/util/FakeGeneratorRepository.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/tools/generator/repository/util/FakeGeneratorRepository.kt @@ -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 + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt index 79c50cb5b2..23fbb3f217 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt @@ -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()