From 35e204d9c1763443c14141be84e9bc3f25235e38 Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:32:05 -0500 Subject: [PATCH] BIT-1957: Update Email Alias generator UI to include 'ForwardEmail' (#1082) --- .../feature/generator/GeneratorScreen.kt | 53 +++++++ .../feature/generator/GeneratorViewModel.kt | 140 ++++++++++++++++++ .../generator/util/ServiceTypeExtensions.kt | 9 ++ .../feature/generator/GeneratorScreenTest.kt | 86 +++++++++++ .../generator/GeneratorViewModelTest.kt | 112 ++++++++++++++ .../util/ServiceTypeExtensionsTest.kt | 42 ++++++ 6 files changed, 442 insertions(+) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index a9c71a0a6..7a22d82e8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -1062,6 +1062,31 @@ private fun ColumnScope.ForwardedEmailAliasTypeContent( ) } + is ServiceType.ForwardEmail -> { + BitwardenPasswordField( + label = stringResource(id = R.string.api_key_required_parenthesis), + value = usernameTypeState.selectedServiceType.apiKey, + onValueChange = forwardedEmailAliasHandlers.onForwardEmailApiKeyTextChange, + showPasswordTestTag = "ShowForwardedEmailApiSecretButton", + modifier = Modifier + .padding(horizontal = 16.dp) + .semantics { testTag = "ForwardedEmailApiSecretEntry" } + .fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + BitwardenTextField( + label = stringResource(id = R.string.domain_name_required_parenthesis), + value = usernameTypeState.selectedServiceType.domainName, + onValueChange = forwardedEmailAliasHandlers.onForwardEmailDomainNameTextChange, + modifier = Modifier + .padding(horizontal = 16.dp) + .semantics { testTag = "ForwardedEmailDomainNameEntry" } + .fillMaxWidth(), + ) + } + is ServiceType.SimpleLogin -> { BitwardenPasswordField( label = stringResource(id = R.string.api_key_required_parenthesis), @@ -1429,6 +1454,8 @@ private data class ForwardedEmailAliasHandlers( val onDuckDuckGoApiKeyTextChange: (String) -> Unit, val onFastMailApiKeyTextChange: (String) -> Unit, val onFirefoxRelayAccessTokenTextChange: (String) -> Unit, + val onForwardEmailApiKeyTextChange: (String) -> Unit, + val onForwardEmailDomainNameTextChange: (String) -> Unit, val onSimpleLoginApiKeyTextChange: (String) -> Unit, ) { companion object { @@ -1512,6 +1539,32 @@ private data class ForwardedEmailAliasHandlers( ), ) }, + onForwardEmailApiKeyTextChange = { newApiKey -> + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .ApiKeyTextChange( + apiKey = newApiKey, + ), + ) + }, + onForwardEmailDomainNameTextChange = { newDomainName -> + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .DomainNameTextChange( + domainName = newDomainName, + ), + ) + }, onSimpleLoginApiKeyTextChange = { newApiKey -> viewModel.trySendAction( GeneratorAction 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 2a06af5e0..e4e74ef1e 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 @@ -41,6 +41,7 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Us import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.DuckDuckGo import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.FastMail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.FirefoxRelay +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.ForwardEmail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.SimpleLogin import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.RandomWord @@ -169,6 +170,10 @@ class GeneratorViewModel @Inject constructor( handleSimpleLoginTextInputChange(action) } + is GeneratorAction.MainType.Username.UsernameType.ForwardedEmailAlias.ForwardEmail -> { + handleForwardEmailSpecificAction(action) + } + is GeneratorAction.MainType.Username.UsernameType.PlusAddressedEmail.EmailTextChange -> { handlePlusAddressedEmailTextInputChange(action) } @@ -971,6 +976,15 @@ class GeneratorViewModel @Inject constructor( ) } + ForwardedEmailAlias.ServiceTypeOption.FORWARD_EMAIL -> updateForwardedEmailAliasType { + ForwardedEmailAlias( + selectedServiceType = ForwardEmail( + apiKey = options.forwardEmailApiAccessToken.orEmpty(), + domainName = options.forwardEmailDomainName.orEmpty(), + ), + ) + } + ForwardedEmailAlias.ServiceTypeOption.SIMPLE_LOGIN -> updateForwardedEmailAliasType { ForwardedEmailAlias( selectedServiceType = SimpleLogin( @@ -1102,6 +1116,73 @@ class GeneratorViewModel @Inject constructor( //endregion FirefoxRelay Service Specific Handlers + //region ForwardEmail Email Specific Handlers + + private fun handleForwardEmailSpecificAction( + action: GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail, + ) { + when (action) { + is GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .ApiKeyTextChange, + -> { + handleForwardEmailApiKeyTextChange(action) + } + + is GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .DomainNameTextChange, + -> { + handleForwardEmailDomainNameTextChange(action) + } + } + } + + private fun handleForwardEmailApiKeyTextChange( + action: GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .ApiKeyTextChange, + ) { + updateForwardEmailServiceType { forwardEmailServiceType -> + val newApiKey = action.apiKey + forwardEmailServiceType.copy(apiKey = newApiKey) + } + } + + private fun handleForwardEmailDomainNameTextChange( + action: GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .DomainNameTextChange, + ) { + updateForwardEmailServiceType { forwardEmailServiceType -> + val newDomainName = action.domainName + forwardEmailServiceType.copy(domainName = newDomainName) + } + } + + //endregion ForwardEmail Email Specific Handlers + //region SimpleLogin Service Specific Handlers private fun handleSimpleLoginTextInputChange( @@ -1485,6 +1566,28 @@ class GeneratorViewModel @Inject constructor( } } + private inline fun updateForwardEmailServiceType( + crossinline block: (ForwardEmail) -> ForwardEmail, + ) { + updateGeneratorMainTypeUsername { currentUsernameType -> + if (currentUsernameType.selectedType !is ForwardedEmailAlias) { + return@updateGeneratorMainTypeUsername currentUsernameType + } + + val currentServiceType = currentUsernameType.selectedType.selectedServiceType + if (currentServiceType !is ForwardEmail) { + return@updateGeneratorMainTypeUsername currentUsernameType + } + + currentUsernameType.copy( + selectedType = ForwardedEmailAlias( + selectedServiceType = block(currentServiceType), + obfuscatedText = currentUsernameType.selectedType.obfuscatedText, + ), + ) + } + } + private inline fun updateCatchAllEmailType( crossinline block: (CatchAllEmail) -> CatchAllEmail, ) { @@ -1810,6 +1913,7 @@ data class GeneratorState( DUCK_DUCK_GO(R.string.duck_duck_go), FAST_MAIL(R.string.fastmail), FIREFOX_RELAY(R.string.firefox_relay), + FORWARD_EMAIL(R.string.forward_email), SIMPLE_LOGIN(R.string.simple_login), } @@ -1886,6 +1990,22 @@ data class GeneratorState( get() = ServiceTypeOption.FIREFOX_RELAY.labelRes } + /** + * Represents the ForwardEmail service type, with configurable options for + * api key and domain name. + * + * @property apiKey The api key used for generation. + * @property domainName The domain name used for generation. + */ + @Parcelize + data class ForwardEmail( + val apiKey: String = "", + val domainName: String = "", + ) : ServiceType(), Parcelable { + override val displayStringResId: Int + get() = ServiceTypeOption.FORWARD_EMAIL.labelRes + } + /** * Represents the SimpleLogin service type, with a configurable option for * api key. @@ -2206,6 +2326,26 @@ sealed class GeneratorAction { data class AccessTokenTextChange(val accessToken: String) : FirefoxRelay() } + /** + * Represents actions specifically related to the ForwardEmail service. + */ + sealed class ForwardEmail : ForwardedEmailAlias() { + + /** + * Fired when the api key input text is changed. + * + * @property apiKey The new api key text. + */ + data class ApiKeyTextChange(val apiKey: String) : ForwardEmail() + + /** + * Fires when the domain name input text is changed. + * + * @property domainName The new domain name text. + */ + data class DomainNameTextChange(val domainName: String) : ForwardEmail() + } + /** * Represents actions specifically related to the SimpleLogin service. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensions.kt index 9a86b7c12..9a830f1af 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensions.kt @@ -60,6 +60,15 @@ fun ServiceType.toUsernameGeneratorRequest(): UsernameGeneratorRequest.Forwarded } } + is ServiceType.ForwardEmail -> { + val apiKey = this.apiKey.orNullIfBlank() ?: return null + val domainName = this.domainName.orNullIfBlank() ?: return null + UsernameGeneratorRequest.Forwarded( + service = ForwarderServiceType.ForwardEmail(apiKey, domainName), + website = null, + ) + } + is ServiceType.SimpleLogin -> { this .apiKey diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 13f167dc2..3770f10f5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -1429,6 +1429,92 @@ class GeneratorScreenTest : BaseComposeTest() { //endregion SimpleLogin Service Type Tests + //region ForwardEmail Service Type Tests + + @Suppress("MaxLineLength") + @Test + fun `in Username_ForwardedEmailAlias_ForwardEmail state, updating api key text input should send ApiKeyTextChange action`() { + updateState( + DEFAULT_STATE.copy( + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .ForwardEmail(), + ), + ), + ), + ) + + val newApiKey = "apiKey" + + composeTestRule + .onNodeWithText("API key (required)") + .performScrollTo() + .performTextInput(newApiKey) + + verify { + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .ApiKeyTextChange( + apiKey = newApiKey, + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `in Username_ForwardedEmailAlias_ForwardEmail state, updating domain name text input should send DomainNameChange action`() { + updateState( + DEFAULT_STATE.copy( + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .ForwardEmail(), + ), + ), + ), + ) + + val newDomainName = "domainName" + + composeTestRule + .onNodeWithText("Domain name (required)") + .performScrollTo() + .performTextInput(newDomainName) + + verify { + viewModel.trySendAction( + GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .DomainNameTextChange( + domainName = newDomainName, + ), + ) + } + } + + //endregion ForwardEmail Service Type Tests + //region Username Type Tests @Test 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 ae2ec038b..d1b9b19c8 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 @@ -1588,6 +1588,99 @@ class GeneratorViewModelTest : BaseViewModelTest() { } } + @Nested + inner class ForwardEmailActions { + private val defaultForwardEmailState = createForwardEmailState() + private lateinit var viewModel: GeneratorViewModel + + @BeforeEach + fun setUp() { + viewModel = createViewModel(defaultForwardEmailState) + } + + @Test + fun `ApiKeyTextChange should update api key text correctly`() { + val newApiKey = "newApiKey" + val action = GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .ApiKeyTextChange( + apiKey = newApiKey, + ) + + fakeGeneratorRepository.setMockGenerateForwardedServiceResult( + GeneratedForwardedServiceUsernameResult.Success( + generatedEmailAddress = "defaultForwardEmail", + ), + ) + + viewModel.actionChannel.trySend(action) + + val expectedState = defaultForwardEmailState.copy( + generatedText = "-", + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .ForwardEmail( + apiKey = newApiKey, + ), + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + + @Test + fun `DomainNameTextChange should update domain name text correctly`() { + val newDomainName = "newDomainName" + val action = GeneratorAction + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ForwardEmail + .DomainNameTextChange( + domainName = newDomainName, + ) + + fakeGeneratorRepository.setMockGenerateForwardedServiceResult( + GeneratedForwardedServiceUsernameResult.Success( + generatedEmailAddress = "defaultForwardEmail", + ), + ) + + viewModel.actionChannel.trySend(action) + + val expectedState = defaultForwardEmailState.copy( + generatedText = "-", + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .ForwardEmail( + domainName = newDomainName, + ), + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + } + @Nested inner class SimpleLoginActions { private val defaultSimpleLoginState = createSimpleLoginState() @@ -1943,6 +2036,25 @@ class GeneratorViewModelTest : BaseViewModelTest() { currentEmailAddress = "currentEmail", ) + private fun createForwardEmailState( + generatedText: String = "defaultForwardEmail", + ): GeneratorState = + GeneratorState( + generatedText = generatedText, + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias( + selectedServiceType = GeneratorState + .MainType + .Username + .UsernameType + .ForwardedEmailAlias + .ServiceType + .ForwardEmail(), + ), + ), + currentEmailAddress = "currentEmail", + ) + private fun createSimpleLoginState( generatedText: String = "defaultSimpleLogin", ): GeneratorState = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensionsTest.kt index 24b1c4d47..c7ea81b02 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/util/ServiceTypeExtensionsTest.kt @@ -121,6 +121,48 @@ internal class ServiceTypeExtensionsTest { ) } + @Test + fun `toUsernameGeneratorRequest for ForwardEmail returns null when apiKey is blank`() { + val forwardMailServiceType = ServiceType.ForwardEmail( + apiKey = "", + domainName = "domainName", + ) + val request = forwardMailServiceType.toUsernameGeneratorRequest() + + assertNull(request) + } + + @Test + fun `toUsernameGeneratorRequest for ForwardEmail returns null when domainName is blank`() { + val forwardMailServiceType = ServiceType.ForwardEmail( + apiKey = "apiKey", + domainName = "", + ) + val request = forwardMailServiceType.toUsernameGeneratorRequest() + + assertNull(request) + } + + @Test + fun `toUsernameGeneratorRequest for ForwardEmail returns correct request`() { + val forwardEmailServiceType = ServiceType.ForwardEmail( + apiKey = "apiKey", + domainName = "domainName", + ) + val request = forwardEmailServiceType.toUsernameGeneratorRequest() + + assertEquals( + UsernameGeneratorRequest.Forwarded( + service = ForwarderServiceType.ForwardEmail( + apiToken = "apiKey", + domain = "domainName", + ), + website = null, + ), + request, + ) + } + @Test fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() { val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "")