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 01daba6aa7..79fa58b8f2 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 @@ -26,6 +26,13 @@ interface GeneratorSdkSource { request: UsernameGeneratorRequest.Subaddress, ): Result + /** + * Generates a catch all email returning a [String] wrapped in a [Result]. + */ + suspend fun generateCatchAllEmail( + request: UsernameGeneratorRequest.Catchall, + ): 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 6ceb517311..a65fd2bc58 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 @@ -32,6 +32,12 @@ class GeneratorSdkSourceImpl( clientGenerator.username(request) } + override suspend fun generateCatchAllEmail( + request: UsernameGeneratorRequest.Catchall, + ): 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 a29fd99370..01ef2f9077 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 @@ -5,6 +5,7 @@ import com.bitwarden.core.PasswordGeneratorRequest import com.bitwarden.core.PasswordHistoryView import com.bitwarden.core.UsernameGeneratorRequest import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState +import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult 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 @@ -40,12 +41,19 @@ interface GeneratorRepository { ): GeneratedPassphraseResult /** - * Attempt to generate a forwarded service username. + * Attempt to generate a plus addressed email username. */ suspend fun generatePlusAddressedEmail( plusAddressedEmailGeneratorRequest: UsernameGeneratorRequest.Subaddress, ): GeneratedPlusAddressedUsernameResult + /** + * Attempt to generate a catch-all email username. + */ + suspend fun generateCatchAllEmail( + catchAllEmailGeneratorRequest: UsernameGeneratorRequest.Catchall, + ): GeneratedCatchAllUsernameResult + /** * 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 6c5cad70bd..0b577abac5 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 @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryD import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.toPasswordHistory import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.toPasswordHistoryEntity import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource +import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult 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 @@ -140,6 +141,19 @@ class GeneratorRepositoryImpl( }, ) + override suspend fun generateCatchAllEmail( + catchAllEmailGeneratorRequest: UsernameGeneratorRequest.Catchall, + ): GeneratedCatchAllUsernameResult = + generatorSdkSource.generateCatchAllEmail(catchAllEmailGeneratorRequest) + .fold( + onSuccess = { generatedEmail -> + GeneratedCatchAllUsernameResult.Success(generatedEmail) + }, + onFailure = { + GeneratedCatchAllUsernameResult.InvalidRequest + }, + ) + override suspend fun generateForwardedServiceUsername( forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded, ): GeneratedForwardedServiceUsernameResult = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedCatchAllUsernameResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedCatchAllUsernameResult.kt new file mode 100644 index 0000000000..778734ba2d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/tools/generator/repository/model/GeneratedCatchAllUsernameResult.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.tools.generator.repository.model + +/** + * Represents the outcome of a generator operation. + */ +sealed class GeneratedCatchAllUsernameResult { + + /** + * Operation succeeded with a value. + */ + data class Success( + val generatedEmailAddress: String, + ) : GeneratedCatchAllUsernameResult() + + /** + * There was an error during the operation. + */ + data object InvalidRequest : GeneratedCatchAllUsernameResult() +} 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 83f54769b5..fe52a4464b 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 @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository +import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult 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 @@ -129,6 +130,10 @@ class GeneratorViewModel @Inject constructor( handleUpdatePlusAddressedGeneratedUsernameResult(action) } + is GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult -> { + handleUpdateCatchAllGeneratedUsernameResult(action) + } + is GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult -> { handleUpdateForwadedServiceGeneratedUsernameResult(action) } @@ -381,6 +386,22 @@ class GeneratorViewModel @Inject constructor( } } + private fun handleUpdateCatchAllGeneratedUsernameResult( + action: GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult, + ) { + when (val result = action.result) { + is GeneratedCatchAllUsernameResult.Success -> { + mutableStateFlow.update { + it.copy(generatedText = result.generatedEmailAddress) + } + } + + GeneratedCatchAllUsernameResult.InvalidRequest -> { + sendEvent(GeneratorEvent.ShowSnackbar(R.string.an_error_has_occurred.asText())) + } + } + } + private fun handleUpdateForwadedServiceGeneratedUsernameResult( action: GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult, ) { @@ -926,7 +947,9 @@ class GeneratorViewModel @Inject constructor( } is CatchAllEmail -> { - // TODO: Implement catch all email generation (BIT-1334) + if (isManualRegeneration) { + generateCatchAllEmail(selectedType) + } } is PlusAddressedEmail -> { @@ -959,6 +982,16 @@ class GeneratorViewModel @Inject constructor( sendAction(GeneratorAction.Internal.UpdateGeneratedPlusAddessedUsernameResult(result)) } + private suspend fun generateCatchAllEmail(catchAllEmail: CatchAllEmail) { + val result = generatorRepository.generateCatchAllEmail( + UsernameGeneratorRequest.Catchall( + type = AppendType.Random, + domain = catchAllEmail.domainName, + ), + ) + sendAction(GeneratorAction.Internal.UpdateGeneratedCatchAllUsernameResult(result)) + } + private inline fun updateGeneratorMainTypePasscode( crossinline block: (Passcode) -> Passcode, ) { @@ -1929,6 +1962,13 @@ sealed class GeneratorAction { val result: GeneratedPlusAddressedUsernameResult, ) : Internal() + /** + * Indicates a generated text update is received. + */ + data class UpdateGeneratedCatchAllUsernameResult( + val result: GeneratedCatchAllUsernameResult, + ) : 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 c2e293936c..c4b77e8d1b 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 @@ -93,6 +93,28 @@ class GeneratorSdkSourceTest { } } + @Suppress("MaxLineLength") + @Test + fun `generateCatchAllEmail should call SDK and return a Result with the generated email`() = + runBlocking { + val request = UsernameGeneratorRequest.Catchall( + type = AppendType.Random, + domain = "domain", + ) + val expectedResult = "user@domain" + + coEvery { + clientGenerators.username(request) + } returns expectedResult + + val result = generatorSdkSource.generateCatchAllEmail(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 bcd3e9778d..7eb2011dde 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 @@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryD import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.toPasswordHistoryEntity import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource +import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult 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 @@ -303,6 +304,46 @@ class GeneratorRepositoryTest { coVerify { generatorSdkSource.generatePlusAddressedEmail(request) } } + @Suppress("MaxLineLength") + @Test + fun `generateCatchAllEmail should return Success with generated email when SDK call is successful`() = runTest { + val userId = "testUserId" + val request = UsernameGeneratorRequest.Catchall( + type = AppendType.Random, + domain = "domain", + ) + val generatedEmail = "user@domain" + + coEvery { generatorSdkSource.generateCatchAllEmail(request) } returns + Result.success(generatedEmail) + + val result = repository.generateCatchAllEmail(request) + + assertEquals( + generatedEmail, + (result as GeneratedCatchAllUsernameResult.Success).generatedEmailAddress, + ) + coVerify { generatorSdkSource.generateCatchAllEmail(request) } + } + + @Suppress("MaxLineLength") + @Test + fun `generateCatchAllEmail should return InvalidRequest on SDK failure`() = runTest { + val request = UsernameGeneratorRequest.Catchall( + type = AppendType.Random, + domain = "user@domain", + ) + val exception = RuntimeException("An error occurred") + coEvery { + generatorSdkSource.generateCatchAllEmail(request) + } returns Result.failure(exception) + + val result = repository.generateCatchAllEmail(request) + + assertTrue(result is GeneratedCatchAllUsernameResult.InvalidRequest) + coVerify { generatorSdkSource.generateCatchAllEmail(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 34e0aed70b..3cf9162649 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 @@ -6,6 +6,7 @@ import com.bitwarden.core.PasswordHistoryView import com.bitwarden.core.UsernameGeneratorRequest import com.x8bit.bitwarden.data.platform.repository.model.LocalDataState import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository +import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult 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 @@ -36,6 +37,11 @@ class FakeGeneratorRepository : GeneratorRepository { generatedEmailAddress = "email+abcd1234@address.com", ) + private var generateCatchAllEmailResult: GeneratedCatchAllUsernameResult = + GeneratedCatchAllUsernameResult.Success( + generatedEmailAddress = "user@domain", + ) + private var generateForwardedServiceResult: GeneratedForwardedServiceUsernameResult = GeneratedForwardedServiceUsernameResult.Success( generatedEmailAddress = "updatedUsername", @@ -63,6 +69,12 @@ class FakeGeneratorRepository : GeneratorRepository { return generatePlusAddressedEmailResult } + override suspend fun generateCatchAllEmail( + catchAllEmailGeneratorRequest: UsernameGeneratorRequest.Catchall, + ): GeneratedCatchAllUsernameResult { + return generateCatchAllEmailResult + } + override suspend fun generateForwardedServiceUsername( forwardedServiceGeneratorRequest: UsernameGeneratorRequest.Forwarded, ): GeneratedForwardedServiceUsernameResult { @@ -122,4 +134,11 @@ class FakeGeneratorRepository : GeneratorRepository { fun setMockGenerateForwardedServiceResult(result: GeneratedForwardedServiceUsernameResult) { generateForwardedServiceResult = result } + + /** + * Sets the mock result for the generateCatchAll function. + */ + fun setMockCatchAllResult(result: GeneratedCatchAllUsernameResult) { + generateCatchAllEmailResult = 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 3a16198cf8..79c50cb5b2 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 @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.repository.model.Environment +import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult 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 @@ -235,6 +236,30 @@ class GeneratorViewModelTest : BaseViewModelTest() { assertEquals(expectedState, viewModel.stateFlow.value) } + @Test + fun `RegenerateClick for catch all email state should update the catch all email correctly`() = + runTest { + val viewModel = createViewModel(catchAllEmailSavedStateHandle) + + fakeGeneratorRepository.setMockCatchAllResult( + GeneratedCatchAllUsernameResult.Success("DifferentUsername"), + ) + + viewModel.actionChannel.trySend(GeneratorAction.RegenerateClick) + + val expectedState = + initialCatchAllEmailState.copy( + generatedText = "DifferentUsername", + selectedType = GeneratorState.MainType.Username( + GeneratorState.MainType.Username.UsernameType.CatchAllEmail( + domainName = "defaultDomain", + ), + ), + ) + + assertEquals(expectedState, viewModel.stateFlow.value) + } + @Test fun `CopyClick should call setText on ClipboardManager`() { val viewModel = createViewModel()