From 273763b2190ea93dcb6d5af5964c41181cc1d1af Mon Sep 17 00:00:00 2001 From: joshua-livefront <139182194+joshua-livefront@users.noreply.github.com> Date: Fri, 5 Jan 2024 12:56:19 -0500 Subject: [PATCH] BIT-1335: Adding plus addressed email generation (#501) --- .../datasource/sdk/GeneratorSdkSource.kt | 7 + .../datasource/sdk/GeneratorSdkSourceImpl.kt | 6 + .../repository/GeneratorRepository.kt | 8 + .../repository/GeneratorRepositoryImpl.kt | 14 ++ .../GeneratedPlusAddressedUsernameResult.kt | 18 ++ .../feature/generator/GeneratorViewModel.kt | 95 ++++++++-- .../datasource/sdk/GeneratorSdkSourceTest.kt | 23 +++ .../repository/GeneratorRepositoryTest.kt | 43 +++++ .../util/FakeGeneratorRepository.kt | 12 ++ .../feature/generator/GeneratorScreenTest.kt | 28 +++ .../generator/GeneratorViewModelTest.kt | 166 +++++++++++++++--- 11 files changed, 374 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedPlusAddressedUsernameResult.kt 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 bfdf988c2f..01daba6aa7 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 @@ -19,6 +19,13 @@ interface GeneratorSdkSource { */ suspend fun generatePassphrase(request: PassphraseGeneratorRequest): Result + /** + * Generates a plus addressed email returning a [String] wrapped in a [Result]. + */ + suspend fun generatePlusAddressedEmail( + request: UsernameGeneratorRequest.Subaddress, + ): 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 d9017a099f..6ceb517311 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 @@ -26,6 +26,12 @@ class GeneratorSdkSourceImpl( clientGenerator.passphrase(request) } + override suspend fun generatePlusAddressedEmail( + request: UsernameGeneratorRequest.Subaddress, + ): 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 71925a124b..a29fd99370 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 @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState 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.GeneratedPlusAddressedUsernameResult import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions import kotlinx.coroutines.flow.StateFlow @@ -38,6 +39,13 @@ interface GeneratorRepository { passphraseGeneratorRequest: PassphraseGeneratorRequest, ): GeneratedPassphraseResult + /** + * Attempt to generate a forwarded service username. + */ + suspend fun generatePlusAddressedEmail( + plusAddressedEmailGeneratorRequest: UsernameGeneratorRequest.Subaddress, + ): GeneratedPlusAddressedUsernameResult + /** * 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 ec73a78118..6c5cad70bd 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 @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSourc 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.GeneratedPlusAddressedUsernameResult import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import kotlinx.coroutines.CoroutineScope @@ -126,6 +127,19 @@ class GeneratorRepositoryImpl( onFailure = { GeneratedPassphraseResult.InvalidRequest }, ) + override suspend fun generatePlusAddressedEmail( + plusAddressedEmailGeneratorRequest: UsernameGeneratorRequest.Subaddress, + ): GeneratedPlusAddressedUsernameResult = + generatorSdkSource.generatePlusAddressedEmail(plusAddressedEmailGeneratorRequest) + .fold( + onSuccess = { generatedEmail -> + GeneratedPlusAddressedUsernameResult.Success(generatedEmail) + }, + onFailure = { + GeneratedPlusAddressedUsernameResult.InvalidRequest + }, + ) + override suspend fun generateForwardedServiceUsername( forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded, ): GeneratedForwardedServiceUsernameResult = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedPlusAddressedUsernameResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedPlusAddressedUsernameResult.kt new file mode 100644 index 0000000000..c2cfe5f720 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedPlusAddressedUsernameResult.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.tools.generator.repository.model + +/** + * Represents the outcome of a generator operation. + */ +sealed class GeneratedPlusAddressedUsernameResult { + /** + * Operation succeeded with a value. + */ + data class Success( + val generatedEmailAddress: String, + ) : GeneratedPlusAddressedUsernameResult() + + /** + * There was an error during the operation. + */ + data object InvalidRequest : GeneratedPlusAddressedUsernameResult() +} 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 361e8a2515..e7867709b6 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 @@ -5,13 +5,17 @@ package com.x8bit.bitwarden.ui.tools.feature.generator import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.core.AppendType import com.bitwarden.core.PassphraseGeneratorRequest import com.bitwarden.core.PasswordGeneratorRequest +import com.bitwarden.core.UsernameGeneratorRequest import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository 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.GeneratedPlusAddressedUsernameResult 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 @@ -56,8 +60,16 @@ private const val KEY_STATE = "state" class GeneratorViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val generatorRepository: GeneratorRepository, + private val authRepository: AuthRepository, ) : BaseViewModel( - initialState = savedStateHandle[KEY_STATE] ?: INITIAL_STATE, + initialState = savedStateHandle[KEY_STATE] ?: GeneratorState( + generatedText = PLACEHOLDER_GENERATED_TEXT, + selectedType = Passcode( + selectedType = Password(), + ), + currentEmailAddress = + requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email), + ), ) { //region Initialization and Overrides @@ -111,8 +123,12 @@ class GeneratorViewModel @Inject constructor( handleUpdateGeneratedPassphraseResult(action) } - is GeneratorAction.Internal.UpdateGeneratedUsernameResult -> { - handleUpdateGeneratedUsernameResult(action) + is GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult -> { + handleUpdatePlusAddressedGeneratedUsernameResult(action) + } + + is GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult -> { + handleUpdateForwadedServiceGeneratedUsernameResult(action) } is GeneratorAction.MainType.Username.UsernameTypeOptionSelect -> { @@ -205,10 +221,21 @@ class GeneratorViewModel @Inject constructor( } private fun loadUsernameOptions(selectedType: Username) { - mutableStateFlow.update { - it.copy(selectedType = selectedType) + val updatedSelectedType = when (selectedType.selectedType) { + is PlusAddressedEmail -> Username( + selectedType = PlusAddressedEmail( + // For convenience the default is an empty email value. We can supply the + // dynamic value here before updating the state. + email = state.currentEmailAddress, + ), + ) + + else -> selectedType + } + + mutableStateFlow.update { + it.copy(selectedType = updatedSelectedType) } - // TODO: Generate different username types. Plus addressed email: BIT-655 } private fun savePasswordOptionsToDisk(password: Password) { @@ -336,8 +363,24 @@ class GeneratorViewModel @Inject constructor( } } - private fun handleUpdateGeneratedUsernameResult( - action: GeneratorAction.Internal.UpdateGeneratedUsernameResult, + private fun handleUpdatePlusAddressedGeneratedUsernameResult( + action: GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult, + ) { + when (val result = action.result) { + is GeneratedPlusAddressedUsernameResult.Success -> { + mutableStateFlow.update { + it.copy(generatedText = result.generatedEmailAddress) + } + } + + GeneratedPlusAddressedUsernameResult.InvalidRequest -> { + sendEvent(GeneratorEvent.ShowSnackbar(R.string.an_error_has_occurred.asText())) + } + } + } + + private fun handleUpdateForwadedServiceGeneratedUsernameResult( + action: GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult, ) { when (val result = action.result) { is GeneratedForwardedServiceUsernameResult.Success -> { @@ -885,7 +928,9 @@ class GeneratorViewModel @Inject constructor( } is PlusAddressedEmail -> { - // TODO: Implement plus addressed email generation (BIT-1335) + if (isManualRegeneration) { + generatePlusAddressedEmail(selectedType) + } } is RandomWord -> { @@ -899,7 +944,17 @@ class GeneratorViewModel @Inject constructor( private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) { val request = alias.selectedServiceType?.toUsernameGeneratorRequest() ?: return val result = generatorRepository.generateForwardedServiceUsername(request) - sendAction(GeneratorAction.Internal.UpdateGeneratedUsernameResult(result)) + sendAction(GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult(result)) + } + + private suspend fun generatePlusAddressedEmail(plusAddressedEmail: PlusAddressedEmail) { + val result = generatorRepository.generatePlusAddressedEmail( + UsernameGeneratorRequest.Subaddress( + type = AppendType.Random, + email = plusAddressedEmail.email, + ), + ) + sendAction(GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult(result)) } private inline fun updateGeneratorMainTypePasscode( @@ -1119,13 +1174,6 @@ class GeneratorViewModel @Inject constructor( companion object { private const val PLACEHOLDER_GENERATED_TEXT = "Placeholder" - - private val INITIAL_STATE: GeneratorState = GeneratorState( - generatedText = PLACEHOLDER_GENERATED_TEXT, - selectedType = Passcode( - selectedType = Password(), - ), - ) } } @@ -1135,11 +1183,13 @@ class GeneratorViewModel @Inject constructor( * * @param generatedText The text that is generated based on the selected options. * @param selectedType The currently selected main type for generating text. + * @param currentEmailAddress The email address for the current user. */ @Parcelize data class GeneratorState( val generatedText: String, val selectedType: MainType, + val currentEmailAddress: String, ) : Parcelable { /** @@ -1334,7 +1384,7 @@ data class GeneratorState( */ @Parcelize data class PlusAddressedEmail( - val email: String = "PLACEHOLDER", + val email: String = "", ) : UsernameType(), Parcelable { override val displayStringResId: Int get() = UsernameTypeOption.PLUS_ADDRESSED_EMAIL.labelRes @@ -1873,7 +1923,14 @@ sealed class GeneratorAction { /** * Indicates a generated text update is received. */ - data class UpdateGeneratedUsernameResult( + data class UpdateGeneratedPlusAddessedUsernameResult( + val result: GeneratedPlusAddressedUsernameResult, + ) : Internal() + + /** + * Indicates a generated text update is received. + */ + data class UpdateGeneratedForwardedServiceUsernameResult( val result: GeneratedForwardedServiceUsernameResult, ) : Internal() } 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 64fd06ad55..c2e293936c 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 @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.sdk +import com.bitwarden.core.AppendType import com.bitwarden.core.ForwarderServiceType import com.bitwarden.core.PassphraseGeneratorRequest import com.bitwarden.core.PasswordGeneratorRequest @@ -70,6 +71,28 @@ class GeneratorSdkSourceTest { } } + @Suppress("MaxLineLength") + @Test + fun `generatePlusAddressedEmail should call SDK and return a Result with the generated email`() = + runBlocking { + val request = UsernameGeneratorRequest.Subaddress( + type = AppendType.Random, + email = "user@example.com", + ) + val expectedResult = "user+generated@example.com" + + coEvery { + clientGenerators.username(request) + } returns expectedResult + + val result = generatorSdkSource.generatePlusAddressedEmail(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 164d61129c..bcd3e9778d 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.tools.generator.repository import app.cash.turbine.test +import com.bitwarden.core.AppendType import com.bitwarden.core.ForwarderServiceType import com.bitwarden.core.PassphraseGeneratorRequest import com.bitwarden.core.PasswordGeneratorRequest @@ -26,6 +27,7 @@ import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSourc 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.GeneratedPlusAddressedUsernameResult import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import io.mockk.coEvery @@ -260,6 +262,47 @@ class GeneratorRepositoryTest { coVerify { generatorSdkSource.generatePassphrase(request) } } + @Suppress("MaxLineLength") + @Test + fun `generatePlusAddressedEmail should return Success with generated email when SDK call is successful`() = runTest { + val userId = "testUserId" + val request = UsernameGeneratorRequest.Subaddress( + type = AppendType.Random, + email = "user@example.com", + ) + val generatedEmail = "user+generated@example.com" + + coEvery { authDiskSource.userState?.activeUserId } returns userId + coEvery { generatorSdkSource.generatePlusAddressedEmail(request) } returns + Result.success(generatedEmail) + + val result = repository.generatePlusAddressedEmail(request) + + assertEquals( + generatedEmail, + (result as GeneratedPlusAddressedUsernameResult.Success).generatedEmailAddress, + ) + coVerify { generatorSdkSource.generatePlusAddressedEmail(request) } + } + + @Suppress("MaxLineLength") + @Test + fun `generatePlusAddressedEmail should return InvalidRequest on SDK failure`() = runTest { + val request = UsernameGeneratorRequest.Subaddress( + type = AppendType.Random, + email = "user@example.com", + ) + val exception = RuntimeException("An error occurred") + coEvery { + generatorSdkSource.generatePlusAddressedEmail(request) + } returns Result.failure(exception) + + val result = repository.generatePlusAddressedEmail(request) + + assertTrue(result is GeneratedPlusAddressedUsernameResult.InvalidRequest) + coVerify { generatorSdkSource.generatePlusAddressedEmail(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 eb62ffdc5c..34e0aed70b 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 @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository 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.GeneratedPlusAddressedUsernameResult import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerationOptions import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -30,6 +31,11 @@ class FakeGeneratorRepository : GeneratorRepository { private val mutablePasswordHistoryStateFlow = MutableStateFlow>>(LocalDataState.Loading) + private var generatePlusAddressedEmailResult: GeneratedPlusAddressedUsernameResult = + GeneratedPlusAddressedUsernameResult.Success( + generatedEmailAddress = "email+abcd1234@address.com", + ) + private var generateForwardedServiceResult: GeneratedForwardedServiceUsernameResult = GeneratedForwardedServiceUsernameResult.Success( generatedEmailAddress = "updatedUsername", @@ -51,6 +57,12 @@ class FakeGeneratorRepository : GeneratorRepository { return generatePassphraseResult } + override suspend fun generatePlusAddressedEmail( + plusAddressedEmailGeneratorRequest: UsernameGeneratorRequest.Subaddress, + ): GeneratedPlusAddressedUsernameResult { + return generatePlusAddressedEmailResult + } + override suspend fun generateForwardedServiceUsername( forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded, ): GeneratedForwardedServiceUsernameResult { 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 51736d6f6e..1a8f681082 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 @@ -51,6 +51,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(), ), + currentEmailAddress = "currentEmail", ), ) @@ -172,6 +173,7 @@ class GeneratorScreenTest : BaseComposeTest() { email = "email", ), ), + currentEmailAddress = "currentEmail", ), ) @@ -390,6 +392,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(), ), + currentEmailAddress = "currentEmail", ), ) @@ -424,6 +427,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(), ), + currentEmailAddress = "currentEmail", ), ) @@ -458,6 +462,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(minNumbers = initialMinNumbers), ), + currentEmailAddress = "currentEmail", ), ) @@ -486,6 +491,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(minNumbers = initialMinNumbers), ), + currentEmailAddress = "currentEmail", ), ) @@ -514,6 +520,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(), ), + currentEmailAddress = "currentEmail", ), ) @@ -548,6 +555,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(), ), + currentEmailAddress = "currentEmail", ), ) @@ -582,6 +590,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(minSpecial = initialSpecialChars), ), + currentEmailAddress = "currentEmail", ), ) @@ -610,6 +619,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Password(minSpecial = initialSpecialChars), ), + currentEmailAddress = "currentEmail", ), ) @@ -663,6 +673,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(numWords = initialNumWords), ), + currentEmailAddress = "currentEmail", ), ) @@ -698,6 +709,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(numWords = initialNumWords), ), + currentEmailAddress = "currentEmail", ), ) @@ -726,6 +738,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(numWords = initialNumWords), ), + currentEmailAddress = "currentEmail", ), ) @@ -755,6 +768,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(), ), + currentEmailAddress = "currentEmail", ), ) @@ -789,6 +803,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(), ), + currentEmailAddress = "currentEmail", ), ) @@ -821,6 +836,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(), ), + currentEmailAddress = "currentEmail", ), ) @@ -852,6 +868,7 @@ class GeneratorScreenTest : BaseComposeTest() { .PasscodeType .Passphrase(), ), + currentEmailAddress = "currentEmail", ), ) @@ -884,6 +901,7 @@ class GeneratorScreenTest : BaseComposeTest() { selectedServiceType = null, ), ), + currentEmailAddress = "currentEmail", ), ) @@ -943,6 +961,7 @@ class GeneratorScreenTest : BaseComposeTest() { .AddyIo(), ), ), + currentEmailAddress = "currentEmail", ), ) @@ -985,6 +1004,7 @@ class GeneratorScreenTest : BaseComposeTest() { .AddyIo(), ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1031,6 +1051,7 @@ class GeneratorScreenTest : BaseComposeTest() { .DuckDuckGo(), ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1071,6 +1092,7 @@ class GeneratorScreenTest : BaseComposeTest() { .FastMail(), ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1111,6 +1133,7 @@ class GeneratorScreenTest : BaseComposeTest() { .FirefoxRelay(), ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1157,6 +1180,7 @@ class GeneratorScreenTest : BaseComposeTest() { .SimpleLogin(), ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1191,6 +1215,7 @@ class GeneratorScreenTest : BaseComposeTest() { email = "", ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1226,6 +1251,7 @@ class GeneratorScreenTest : BaseComposeTest() { domainName = "", ), ), + currentEmailAddress = "currentEmail", ), ) @@ -1259,6 +1285,7 @@ class GeneratorScreenTest : BaseComposeTest() { selectedType = GeneratorState.MainType.Username( GeneratorState.MainType.Username.UsernameType.RandomWord(), ), + currentEmailAddress = "currentEmail", ), ) @@ -1284,6 +1311,7 @@ class GeneratorScreenTest : BaseComposeTest() { selectedType = GeneratorState.MainType.Username( GeneratorState.MainType.Username.UsernameType.RandomWord(), ), + currentEmailAddress = "currentEmail", ), ) 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 b1ec349707..5574be61ff 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 @@ -3,6 +3,9 @@ package com.x8bit.bitwarden.ui.tools.feature.generator import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.UserState +import com.x8bit.bitwarden.data.platform.repository.model.Environment 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 @@ -10,6 +13,9 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.PasscodeGenerat 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 +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach @@ -54,6 +60,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { private val initialRandomWordState = createRandomWordState() private val randomWordSavedStateHandle = createSavedStateHandleWithState(initialRandomWordState) + private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE) + private val authRepository = mockk() { + every { userStateFlow } returns mutableUserStateFlow + } + private val fakeGeneratorRepository = FakeGeneratorRepository().apply { setMockGeneratePasswordResult( GeneratedPasswordResult.Success("defaultPassword"), @@ -61,11 +72,15 @@ class GeneratorViewModelTest : BaseViewModelTest() { } @Test - fun `initial state should be correct`() = runTest { - val viewModel = createViewModel() - viewModel.stateFlow.test { - assertEquals(initialPasscodeState, awaitItem()) - } + fun `initial state should be correct when there is no saved state`() { + val viewModel = createViewModel(state = null) + assertEquals(initialPasscodeState, viewModel.stateFlow.value) + } + + @Test + fun `initial state should be correct when there is a saved state`() { + val viewModel = createViewModel(state = initialPasscodeState) + assertEquals(initialPasscodeState, viewModel.stateFlow.value) } @Suppress("MaxLineLength") @@ -190,18 +205,34 @@ class GeneratorViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `RegenerateClick for username state should do nothing`() = runTest { - val viewModel = GeneratorViewModel(usernameSavedStateHandle, fakeGeneratorRepository) + fun `RegenerateClick for plus addressed email state should update the plus addressed email correctly`() = + runTest { + val viewModel = GeneratorViewModel( + usernameSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) - fakeGeneratorRepository.setMockGeneratePasswordResult( - GeneratedPasswordResult.Success("DifferentUsername"), - ) + fakeGeneratorRepository.setMockGeneratePasswordResult( + GeneratedPasswordResult.Success("DifferentUsername"), + ) - viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick) + viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick) - assertEquals(initialUsernameState, viewModel.stateFlow.value) - } + val expectedState = + initialPasscodeState.copy( + generatedText = "email+abcd1234@address.com", + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail( + email = "currentEmail", + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } @Test fun `CopyClick should emit CopyTextToClipboard event`() = runTest { @@ -246,7 +277,13 @@ class GeneratorViewModelTest : BaseViewModelTest() { viewModel.actionChannel.trySend(action) val expectedState = - initialPasscodeState.copy(selectedType = GeneratorState.MainType.Username()) + initialPasscodeState.copy( + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail( + email = "currentEmail", + ), + ), + ) assertEquals(expectedState, viewModel.stateFlow.value) } @@ -320,7 +357,9 @@ class GeneratorViewModelTest : BaseViewModelTest() { .MainType .Username .UsernameType - .PlusAddressedEmail(), + .PlusAddressedEmail( + email = "currentEmail", + ), ), ) @@ -412,7 +451,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { fakeGeneratorRepository.setMockGeneratePasswordResult( GeneratedPasswordResult.Success("defaultPassword"), ) - viewModel = GeneratorViewModel(initialPasscodeSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + initialPasscodeSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Suppress("MaxLineLength") @@ -785,7 +828,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { fakeGeneratorRepository.setMockGeneratePasswordResult( GeneratedPasswordResult.Success("defaultPassphrase"), ) - viewModel = GeneratorViewModel(passphraseSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + passphraseSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -923,7 +970,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { viewModel = - GeneratorViewModel(forwardedEmailAliasSavedStateHandle, fakeGeneratorRepository) + GeneratorViewModel( + forwardedEmailAliasSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -980,7 +1031,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(addyIoSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + addyIoSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -1070,7 +1125,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(duckDuckGoSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + duckDuckGoSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -1120,7 +1179,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(fastMailSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + fastMailSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -1170,7 +1233,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(firefoxRelaySavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + firefoxRelaySavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -1221,7 +1288,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(simpleLoginSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + simpleLoginSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Test @@ -1272,7 +1343,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(usernameSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + usernameSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Suppress("MaxLineLength") @@ -1280,6 +1355,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { fun `EmailTextChange should update email correctly`() = runTest { val newEmail = "test@example.com" + val newGeneratedEmail = "email+abcd1234@address.com" viewModel.actionChannel.trySend( GeneratorAction .MainType @@ -1314,7 +1390,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(catchAllEmailSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + catchAllEmailSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Suppress("MaxLineLength") @@ -1356,7 +1436,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - viewModel = GeneratorViewModel(randomWordSavedStateHandle, fakeGeneratorRepository) + viewModel = GeneratorViewModel( + randomWordSavedStateHandle, + fakeGeneratorRepository, + authRepository, + ) } @Suppress("MaxLineLength") @@ -1437,6 +1521,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { avoidAmbiguousChars = avoidAmbiguousChars, ), ), + currentEmailAddress = "currentEmail", ) private fun createPassphraseState( @@ -1456,6 +1541,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { includeNumber = includeNumber, ), ), + currentEmailAddress = "currentEmail", ) private fun createForwardedEmailAliasState( @@ -1470,6 +1556,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { obfuscatedText = obfuscatedText, ), ), + currentEmailAddress = "currentEmail", ) private fun createAddyIoState( @@ -1488,6 +1575,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { .AddyIo(), ), ), + currentEmailAddress = "currentEmail", ) private fun createDuckDuckGoState( @@ -1506,6 +1594,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { .DuckDuckGo(), ), ), + currentEmailAddress = "currentEmail", ) private fun createFastMailState( @@ -1524,6 +1613,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { .FastMail(), ), ), + currentEmailAddress = "currentEmail", ) private fun createFirefoxRelayState( @@ -1542,6 +1632,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { .FirefoxRelay(), ), ), + currentEmailAddress = "currentEmail", ) private fun createSimpleLoginState( @@ -1560,11 +1651,12 @@ class GeneratorViewModelTest : BaseViewModelTest() { .SimpleLogin(), ), ), + currentEmailAddress = "currentEmail", ) private fun createPlusAddressedEmailState( generatedText: String = "defaultPlusAddressedEmail", - email: String = "defaultEmail", + email: String = "currentEmail", ): GeneratorState = GeneratorState( generatedText = generatedText, @@ -1573,6 +1665,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { email = email, ), ), + currentEmailAddress = "currentEmail", ) private fun createCatchAllEmailState( @@ -1586,6 +1679,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { domainName = domain, ), ), + currentEmailAddress = "currentEmail", ) private fun createRandomWordState( @@ -1601,6 +1695,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { includeNumber = includeNumber, ), ), + currentEmailAddress = "currentEmail", ) private fun createSavedStateHandleWithState(state: GeneratorState) = @@ -1613,7 +1708,24 @@ class GeneratorViewModelTest : BaseViewModelTest() { ): GeneratorViewModel = GeneratorViewModel( savedStateHandle = SavedStateHandle().apply { set("state", state) }, generatorRepository = fakeGeneratorRepository, + authRepository = authRepository, ) //endregion Helper Functions } + +private val DEFAULT_USER_STATE = UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "Active User", + email = "currentEmail", + environment = Environment.Us, + avatarColorHex = "#aa00aa", + isPremium = true, + isVaultUnlocked = true, + organizations = emptyList(), + ), + ), +)