BIT-1334: Adding generation for catch all email usernames (#538)

This commit is contained in:
joshua-livefront 2024-01-08 15:52:19 -05:00 committed by Álison Fernandes
parent 36d49a62a6
commit b6e7655938
10 changed files with 203 additions and 2 deletions

View file

@ -26,6 +26,13 @@ interface GeneratorSdkSource {
request: UsernameGeneratorRequest.Subaddress,
): Result<String>
/**
* Generates a catch all email returning a [String] wrapped in a [Result].
*/
suspend fun generateCatchAllEmail(
request: UsernameGeneratorRequest.Catchall,
): Result<String>
/**
* Generates a forwarded service email returning a [String] wrapped in a [Result].
*/

View file

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

View file

@ -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.
*/

View file

@ -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 =

View file

@ -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()
}

View file

@ -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.
*/

View file

@ -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`() =

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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()