[PM-14936] Add AnonAddy self-hosted server URL support (#4708)

This commit is contained in:
Patrick Honkonen
2025-02-18 08:57:47 -05:00
committed by GitHub
parent 133d8548e8
commit bb2e98eac6
12 changed files with 454 additions and 45 deletions

View File

@@ -21,6 +21,7 @@ import kotlinx.serialization.Serializable
* @property fastMailApiKey The API key for FastMail.
* @property anonAddyApiAccessToken The API access token for AnonAddy.
* @property anonAddyDomainName The domain name associated with AnonAddy.
* @property anonAddySelfHostServerUrl The self-hosted server URL for AnonAddy.
* @property forwardEmailApiAccessToken The API access token for Forward Email.
* @property forwardEmailDomainName The domain name associated with Forward Email.
* @property emailWebsite The website associated with the email service.
@@ -63,6 +64,9 @@ data class UsernameGenerationOptions(
@SerialName("anonAddyDomainName")
val anonAddyDomainName: String? = null,
@SerialName("anonAddyBaseUrl")
val anonAddySelfHostServerUrl: String? = null,
@SerialName("forwardEmailApiAccessToken")
val forwardEmailApiAccessToken: String? = null,

View File

@@ -215,3 +215,10 @@ fun String.prefixHttpsIfNecessaryOrNull(): String? =
"http://" in this || "https://" in this -> this
else -> "https://$this"
}
/**
* If the given [String] is a valid URI, "https://" will be appended if it is not already present.
* Otherwise the original [String] will be returned.
*/
fun String.prefixHttpsIfNecessary(): String =
prefixHttpsIfNecessaryOrNull() ?: this

View File

@@ -528,6 +528,8 @@ private fun CoachMarkScope<ExploreGeneratorCoachMark>.ScrollContent(
plusAddressedEmailHandlers = plusAddressedEmailHandlers,
catchAllEmailHandlers = catchAllEmailHandlers,
randomWordHandlers = randomWordHandlers,
shouldShowSelfHostServerUrlField =
state.shouldShowAnonAddySelfHostServerUrlField,
)
}
}
@@ -1101,6 +1103,7 @@ private fun UsernameTypeItems(
plusAddressedEmailHandlers: PlusAddressedEmailHandlers,
catchAllEmailHandlers: CatchAllEmailHandlers,
randomWordHandlers: RandomWordHandlers,
shouldShowSelfHostServerUrlField: Boolean,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@@ -1125,6 +1128,7 @@ private fun UsernameTypeItems(
ForwardedEmailAliasTypeContent(
usernameTypeState = selectedType,
forwardedEmailAliasHandlers = forwardedEmailAliasHandlers,
shouldShowSelfHostServerUrlField = shouldShowSelfHostServerUrlField,
)
}
@@ -1185,6 +1189,7 @@ private fun UsernameOptionsItem(
private fun ForwardedEmailAliasTypeContent(
usernameTypeState: GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias,
forwardedEmailAliasHandlers: ForwardedEmailAliasHandlers,
shouldShowSelfHostServerUrlField: Boolean,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@@ -1226,6 +1231,21 @@ private fun ForwardedEmailAliasTypeContent(
.standardHorizontalMargin()
.fillMaxWidth(),
)
if (shouldShowSelfHostServerUrlField) {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.self_host_server_url),
value = usernameTypeState.selectedServiceType.selfHostServerUrl,
onValueChange = forwardedEmailAliasHandlers.onAddyIoSelfHostServerUrlChange,
textFieldTestTag = "AnonAddySelfHostUrlEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.standardHorizontalMargin()
.fillMaxWidth(),
)
}
}
is ServiceType.DuckDuckGo -> {

View File

@@ -12,11 +12,13 @@ import com.bitwarden.generators.UsernameGeneratorRequest
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.util.getActivePolicies
import com.x8bit.bitwarden.data.platform.manager.util.getActivePoliciesFlow
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
@@ -80,6 +82,7 @@ class GeneratorViewModel @Inject constructor(
private val policyManager: PolicyManager,
private val reviewPromptManager: ReviewPromptManager,
private val firstTimeActionManager: FirstTimeActionManager,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
initialState = savedStateHandle[KEY_STATE] ?: run {
val generatorMode = GeneratorArgs(savedStateHandle).type
@@ -109,6 +112,9 @@ class GeneratorViewModel @Inject constructor(
.any(),
website = (generatorMode as? GeneratorMode.Modal.Username)?.website,
shouldShowCoachMarkTour = false,
shouldShowAnonAddySelfHostServerUrlField = featureFlagManager.getFeatureFlag(
FlagKey.AnonAddySelfHostAlias,
),
)
},
) {
@@ -135,6 +141,17 @@ class GeneratorViewModel @Inject constructor(
}
.onEach(::sendAction)
.launchIn(viewModelScope)
featureFlagManager
.getFeatureFlagFlow(FlagKey.AnonAddySelfHostAlias)
.map { shouldShowAnonAddySelfHostServerUrlField ->
GeneratorAction.Internal.ShouldShowAnonAddySelfHostValueChangeReceive(
shouldShowAnonAddySelfHostServerUrlField =
shouldShowAnonAddySelfHostServerUrlField,
)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: GeneratorAction) {
@@ -268,6 +285,10 @@ class GeneratorViewModel @Inject constructor(
is GeneratorAction.Internal.ShouldShowGeneratorCoachMarkValueChangeReceive -> {
handleShouldShowCoachMarkValueChange(action)
}
is GeneratorAction.Internal.ShouldShowAnonAddySelfHostValueChangeReceive -> {
handleShouldShowAnonAddySelfHostValueChange(action)
}
}
}
@@ -547,6 +568,8 @@ class GeneratorViewModel @Inject constructor(
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.ANON_ADDY,
anonAddyApiAccessToken = forwardedEmailAlias.selectedServiceType.apiAccessToken,
anonAddyDomainName = forwardedEmailAlias.selectedServiceType.domainName,
anonAddySelfHostServerUrl =
forwardedEmailAlias.selectedServiceType.selfHostServerUrl,
)
is DuckDuckGo -> options.copy(
@@ -624,6 +647,7 @@ class GeneratorViewModel @Inject constructor(
fastMailApiKey = "",
anonAddyApiAccessToken = "",
anonAddyDomainName = "",
anonAddySelfHostServerUrl = "",
forwardEmailApiAccessToken = "",
forwardEmailDomainName = "",
emailWebsite = "",
@@ -1079,6 +1103,7 @@ class GeneratorViewModel @Inject constructor(
selectedServiceType = AddyIo(
apiAccessToken = options.anonAddyApiAccessToken.orEmpty(),
domainName = options.anonAddyDomainName.orEmpty(),
selfHostServerUrl = options.anonAddySelfHostServerUrl.orEmpty(),
),
)
}
@@ -1155,6 +1180,17 @@ class GeneratorViewModel @Inject constructor(
-> {
handleAddyIoDomainNameTextChange(action)
}
is GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.AddyIo
.SelfHostServerUrlChange,
-> {
handleAddyIoSelfHostServerUrlChange(action)
}
}
}
@@ -1188,6 +1224,31 @@ class GeneratorViewModel @Inject constructor(
}
}
private fun handleShouldShowAnonAddySelfHostValueChange(
action: GeneratorAction.Internal.ShouldShowAnonAddySelfHostValueChangeReceive,
) {
mutableStateFlow.update {
it.copy(
shouldShowAnonAddySelfHostServerUrlField =
action.shouldShowAnonAddySelfHostServerUrlField,
)
}
}
private fun handleAddyIoSelfHostServerUrlChange(
action: GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.AddyIo
.SelfHostServerUrlChange,
) {
updateAddyIoServiceType { addyIoServiceType ->
addyIoServiceType.copy(selfHostServerUrl = action.url)
}
}
//endregion Addy.Io Service Specific Handlers
//region DuckDuckGo Service Specific Handlers
@@ -1472,10 +1533,16 @@ class GeneratorViewModel @Inject constructor(
}
private suspend fun generateForwardedEmailAlias(alias: ForwardedEmailAlias) {
val request = alias.selectedServiceType?.toUsernameGeneratorRequest(state.website) ?: run {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
return
}
val request = alias
.selectedServiceType
?.toUsernameGeneratorRequest(
website = state.website,
allowAddyIoSelfHostUrl = state.shouldShowAnonAddySelfHostServerUrlField,
)
?: run {
mutableStateFlow.update { it.copy(generatedText = NO_GENERATED_TEXT) }
return
}
val result = generatorRepository.generateForwardedServiceUsername(request)
sendAction(GeneratorAction.Internal.UpdateGeneratedForwardedServiceUsernameResult(result))
}
@@ -1772,6 +1839,7 @@ data class GeneratorState(
val website: String? = null,
var passcodePolicyOverride: PasscodePolicyOverride? = null,
private val shouldShowCoachMarkTour: Boolean,
val shouldShowAnonAddySelfHostServerUrlField: Boolean,
) : Parcelable {
/**
@@ -2097,10 +2165,15 @@ data class GeneratorState(
data class AddyIo(
val apiAccessToken: String = "",
val domainName: String = "",
val baseUrl: String = "https://app.addy.io",
val selfHostServerUrl: String = "",
) : ServiceType(), Parcelable {
override val displayStringResId: Int
get() = ServiceTypeOption.ADDY_IO.labelRes
@Suppress("UndocumentedPublicClass")
companion object {
const val DEFAULT_ADDY_IO_URL = "https://app.addy.io"
}
}
/**
@@ -2434,6 +2507,13 @@ sealed class GeneratorAction {
* @property domain The new domain text.
*/
data class DomainTextChange(val domain: String) : AddyIo()
/**
* Fired when the self host server url input text is changed.
*
* @property url The new self host server url text.
*/
data class SelfHostServerUrlChange(val url: String) : AddyIo()
}
/**
@@ -2617,6 +2697,13 @@ sealed class GeneratorAction {
data class ShouldShowGeneratorCoachMarkValueChangeReceive(
val shouldShowCoachMarkTour: Boolean,
) : Internal()
/**
* The value for the shouldShowAnonAddySelfHostServerUrlField feature flag has changed.
*/
data class ShouldShowAnonAddySelfHostValueChangeReceive(
val shouldShowAnonAddySelfHostServerUrlField: Boolean,
) : Internal()
}
}

View File

@@ -17,6 +17,7 @@ data class ForwardedEmailAliasHandlers(
val onServiceChange: (ForwardedEmailAlias.ServiceTypeOption) -> Unit,
val onAddyIoAccessTokenTextChange: (String) -> Unit,
val onAddyIoDomainNameTextChange: (String) -> Unit,
val onAddyIoSelfHostServerUrlChange: (String) -> Unit,
val onDuckDuckGoApiKeyTextChange: (String) -> Unit,
val onFastMailApiKeyTextChange: (String) -> Unit,
val onFirefoxRelayAccessTokenTextChange: (String) -> Unit,
@@ -30,6 +31,7 @@ data class ForwardedEmailAliasHandlers(
* Creates an instance of [ForwardedEmailAliasHandlers] by binding actions to the provided
* [GeneratorViewModel].
*/
@Suppress("LongMethod")
fun create(
viewModel: GeneratorViewModel,
): ForwardedEmailAliasHandlers = ForwardedEmailAliasHandlers(
@@ -52,6 +54,11 @@ data class ForwardedEmailAliasHandlers(
ForwardedEmailAliasAction.AddyIo.DomainTextChange(domain = newDomainName),
)
},
onAddyIoSelfHostServerUrlChange = { newServerUrl ->
viewModel.trySendAction(
ForwardedEmailAliasAction.AddyIo.SelfHostServerUrlChange(url = newServerUrl),
)
},
onDuckDuckGoApiKeyTextChange = { newApiKey ->
viewModel.trySendAction(
ForwardedEmailAliasAction.DuckDuckGo.ApiKeyTextChange(apiKey = newApiKey),

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.tools.feature.generator.util
import com.x8bit.bitwarden.data.tools.generator.repository.model.UsernameGenerationOptions
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.AddyIo
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType.DuckDuckGo
@@ -38,6 +39,9 @@ fun UsernameGenerationOptions.ForwardedEmailServiceType?.toServiceType(
AddyIo(
apiAccessToken = options.anonAddyApiAccessToken.orEmpty(),
domainName = options.anonAddyDomainName.orEmpty(),
selfHostServerUrl = options.anonAddySelfHostServerUrl
?.prefixHttpsIfNecessary()
.orEmpty(),
)
}

View File

@@ -3,22 +3,31 @@ package com.x8bit.bitwarden.ui.tools.feature.generator.util
import com.bitwarden.generators.ForwarderServiceType
import com.bitwarden.generators.UsernameGeneratorRequest
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessary
import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias.ServiceType
/**
* Converts a [ServiceType] to a [UsernameGeneratorRequest.Forwarded].
*/
@Suppress("LongMethod")
fun ServiceType.toUsernameGeneratorRequest(website: String?): UsernameGeneratorRequest.Forwarded? {
fun ServiceType.toUsernameGeneratorRequest(
website: String?,
allowAddyIoSelfHostUrl: Boolean,
): UsernameGeneratorRequest.Forwarded? {
return when (this) {
is ServiceType.AddyIo -> {
val accessToken = this.apiAccessToken.orNullIfBlank() ?: return null
val domain = this.domainName.orNullIfBlank() ?: return null
val baseUrl = if (allowAddyIoSelfHostUrl && selfHostServerUrl.isNotBlank()) {
selfHostServerUrl.prefixHttpsIfNecessary()
} else {
ServiceType.AddyIo.DEFAULT_ADDY_IO_URL
}
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.AddyIo(
apiToken = accessToken,
domain = domain,
baseUrl = this.baseUrl,
baseUrl = baseUrl,
),
website = website,
)

View File

@@ -1209,4 +1209,5 @@ Do you want to switch to this account?</string>
<string name="passkey_operation_failed_because_the_request_is_invalid">Passkey operation failed because the request is invalid.</string>
<string name="passkey_operation_failed_because_user_verification_attempts_exceeded">Passkey operation failed because user verification attempts exceeded.</string>
<string name="passkey_operation_failed_because_no_item_was_selected">Passkey operation failed because no item was selected.</string>
<string name="self_host_server_url">Self-host server URL</string>
</resources>

View File

@@ -5,6 +5,7 @@ import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.SemanticsMatcher.Companion.expectValue
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertTextEquals
@@ -1124,6 +1125,84 @@ class GeneratorScreenTest : BaseComposeTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `in Username_ForwardedEmailAlias_AddyIo state, updating self host server url text input should send SelfHostServerUrlChange action`() {
updateState(
DEFAULT_STATE.copy(
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.AddyIo(),
),
),
),
)
val newServerUrl = "https://addyio.local"
composeTestRule
.onNodeWithText("Self-host server URL")
.performScrollTo()
.performTextInput(newServerUrl)
verify {
viewModel.trySendAction(
GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.AddyIo
.SelfHostServerUrlChange(
url = newServerUrl,
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in Username_ForwardedEmailAlias_AddyIo state, self host server url field should show based on state`() {
updateState(
DEFAULT_STATE.copy(
shouldShowAnonAddySelfHostServerUrlField = true,
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = GeneratorState
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.ServiceType
.AddyIo(),
),
),
),
)
composeTestRule
.onNodeWithText("Self-host server URL")
.performScrollTo()
.assertIsDisplayed()
// Simulate Disabling the feature flag
updateState(
DEFAULT_STATE.copy(
shouldShowAnonAddySelfHostServerUrlField = false,
),
)
composeTestRule
.onNodeWithText("Self-host server URL")
.assertIsNotDisplayed()
}
//endregion Addy.Io Service Type Tests
//region DuckDuckGo Service Type Tests
@@ -1773,4 +1852,5 @@ private val DEFAULT_STATE = GeneratorState(
selectedType = GeneratorState.MainType.Password(),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = false,
shouldShowAnonAddySelfHostServerUrlField = true,
)

View File

@@ -8,12 +8,14 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratedCatchAllUsernameResult
@@ -119,6 +121,13 @@ class GeneratorViewModelTest : BaseViewModelTest() {
every { markCoachMarkTourCompleted(CoachMarkTourType.GENERATOR) } just runs
every { shouldShowGeneratorCoachMarkFlow } returns mutableShouldShowGeneratorCoachMarkFlow
}
private val mutableAnonAddySelfHostAliasFlow = MutableStateFlow(true)
private val featureFlagManager = mockk<FeatureFlagManager> {
every { getFeatureFlag(FlagKey.AnonAddySelfHostAlias) } returns true
every {
getFeatureFlagFlow(FlagKey.AnonAddySelfHostAlias)
} returns mutableAnonAddySelfHostAliasFlow
}
@Test
fun `initial state should be correct when there is no saved state`() {
@@ -148,9 +157,9 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
generatorMode = GeneratorMode.Modal.Username(website = ""),
currentEmailAddress = "currentEmail",
isUnderPolicy = false,
website = "",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
val viewModel = createViewModel(
@@ -184,9 +193,8 @@ class GeneratorViewModelTest : BaseViewModelTest() {
selectedType = GeneratorState.MainType.Passphrase(),
generatorMode = GeneratorMode.Modal.Password,
currentEmailAddress = "currentEmail",
isUnderPolicy = false,
website = null,
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
val viewModel = createViewModel(
@@ -1696,9 +1704,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = ServiceType
.AddyIo(
apiAccessToken = newAccessToken,
),
.AddyIo(apiAccessToken = newAccessToken),
),
),
)
@@ -1726,13 +1732,47 @@ class GeneratorViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(action)
val expectedState = defaultAddyIoState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = ServiceType
.AddyIo(domainName = newDomainName),
),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `ServerUrlTextChange should update the server text correctly`() = runTest {
val newSelfHostServerUrl = "https://selfhost.alias"
val action = GeneratorAction
.MainType
.Username
.UsernameType
.ForwardedEmailAlias
.AddyIo
.SelfHostServerUrlChange(
url = newSelfHostServerUrl,
)
fakeGeneratorRepository.setMockGenerateForwardedServiceResult(
GeneratedForwardedServiceUsernameResult.Success(
generatedEmailAddress = "defaultAddyIo",
),
)
viewModel.trySendAction(action)
val expectedState = defaultAddyIoState.copy(
generatedText = "-",
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.ForwardedEmailAlias(
selectedServiceType = ServiceType
.AddyIo(
domainName = newDomainName,
selfHostServerUrl = newSelfHostServerUrl,
),
),
),
@@ -2295,6 +2335,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createPassphraseState(
@@ -2314,6 +2355,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createUsernameModeState(
@@ -2322,14 +2364,15 @@ class GeneratorViewModelTest : BaseViewModelTest() {
): GeneratorState =
GeneratorState(
generatedText = generatedText,
generatorMode = GeneratorMode.Modal.Username(website = null),
selectedType = GeneratorState.MainType.Username(
GeneratorState.MainType.Username.UsernameType.PlusAddressedEmail(
email = email,
),
),
generatorMode = GeneratorMode.Modal.Username(website = null),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createForwardedEmailAliasState(
@@ -2346,6 +2389,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createAddyIoState(
@@ -2361,6 +2405,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createDuckDuckGoState(
@@ -2376,6 +2421,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createFastMailState(
@@ -2391,6 +2437,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createFirefoxRelayState(
@@ -2406,6 +2453,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createForwardEmailState(
@@ -2421,6 +2469,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createSimpleLoginState(
@@ -2436,6 +2485,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createPlusAddressedEmailState(
@@ -2451,6 +2501,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createCatchAllEmailState(
@@ -2466,6 +2517,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createRandomWordState(
@@ -2483,6 +2535,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
),
currentEmailAddress = "currentEmail",
shouldShowCoachMarkTour = true,
shouldShowAnonAddySelfHostServerUrlField = true,
)
private fun createSavedStateHandleWithState(state: GeneratorState) =
@@ -2500,6 +2553,7 @@ class GeneratorViewModelTest : BaseViewModelTest() {
policyManager = policyManager,
reviewPromptManager = reviewPromptManager,
firstTimeActionManager = firstTimeActionManager,
featureFlagManager = featureFlagManager,
)
private fun createViewModel(

View File

@@ -22,13 +22,17 @@ class ForwardedEmailServiceTypeExtensionsTest {
fastMailApiKey = "api_key_fast_mail",
anonAddyApiAccessToken = "access_token_anon_addy",
anonAddyDomainName = "anonaddy.com",
anonAddySelfHostServerUrl = "https://anonaddy.local",
forwardEmailApiAccessToken = "access_token_forward_email",
forwardEmailDomainName = "forwardemail.net",
emailWebsite = "email.example.com",
)
UsernameGenerationOptions.ForwardedEmailServiceType.entries
.forEach {
val expected = createMockForwardedEmailAliasServiceType(it)
val expected = createMockForwardedEmailAliasServiceType(
serviceTypeOption = it,
useEmptyValues = false,
)
assertEquals(
expected,
it.toServiceType(options),
@@ -36,39 +40,90 @@ class ForwardedEmailServiceTypeExtensionsTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `toServiceType should map to correct service type with empty values`() {
val options = UsernameGenerationOptions(
type = UsernameGenerationOptions.UsernameType.RANDOM_WORD,
serviceType = UsernameGenerationOptions.ForwardedEmailServiceType.NONE,
capitalizeRandomWordUsername = true,
includeNumberRandomWordUsername = false,
plusAddressedEmail = null,
catchAllEmailDomain = null,
firefoxRelayApiAccessToken = null,
simpleLoginApiKey = null,
duckDuckGoApiKey = null,
fastMailApiKey = null,
anonAddyApiAccessToken = null,
anonAddyDomainName = null,
anonAddySelfHostServerUrl = null,
forwardEmailApiAccessToken = null,
forwardEmailDomainName = null,
emailWebsite = null,
)
UsernameGenerationOptions.ForwardedEmailServiceType.entries
.forEach {
assertEquals(
createMockForwardedEmailAliasServiceType(
serviceTypeOption = it,
useEmptyValues = true,
),
it.toServiceType(options),
)
}
}
private fun createMockForwardedEmailAliasServiceType(
serviceTypeOption: UsernameGenerationOptions.ForwardedEmailServiceType,
useEmptyValues: Boolean = false,
): ServiceType? = when (serviceTypeOption) {
UsernameGenerationOptions.ForwardedEmailServiceType.NONE -> null
UsernameGenerationOptions.ForwardedEmailServiceType.ANON_ADDY -> {
ServiceType.AddyIo(
apiAccessToken = "access_token_anon_addy",
domainName = "anonaddy.com",
apiAccessToken = "access_token_anon_addy".takeUnless { useEmptyValues }.orEmpty(),
domainName = "anonaddy.com".takeUnless { useEmptyValues }.orEmpty(),
selfHostServerUrl = "https://anonaddy.local"
.takeUnless { useEmptyValues }
.orEmpty(),
)
}
UsernameGenerationOptions.ForwardedEmailServiceType.FIREFOX_RELAY -> {
ServiceType.FirefoxRelay(apiAccessToken = "access_token_firefox_relay")
ServiceType.FirefoxRelay(
apiAccessToken = "access_token_firefox_relay"
.takeUnless { useEmptyValues }
.orEmpty(),
)
}
UsernameGenerationOptions.ForwardedEmailServiceType.SIMPLE_LOGIN -> {
ServiceType.SimpleLogin(apiKey = "api_key_simple_login")
ServiceType.SimpleLogin(
apiKey = "api_key_simple_login"
.takeUnless { useEmptyValues }
.orEmpty(),
)
}
UsernameGenerationOptions.ForwardedEmailServiceType.DUCK_DUCK_GO -> {
ServiceType.DuckDuckGo(apiKey = "api_key_duck_duck_go")
ServiceType.DuckDuckGo(
apiKey = "api_key_duck_duck_go"
.takeUnless { useEmptyValues }
.orEmpty(),
)
}
UsernameGenerationOptions.ForwardedEmailServiceType.FASTMAIL -> {
ServiceType.FastMail(apiKey = "api_key_fast_mail")
ServiceType.FastMail(
apiKey = "api_key_fast_mail"
.takeUnless { useEmptyValues }
.orEmpty(),
)
}
UsernameGenerationOptions.ForwardedEmailServiceType.FORWARD_EMAIL -> {
ServiceType.ForwardEmail(
apiKey = "access_token_forward_email",
domainName = "forwardemail.net",
apiKey = "access_token_forward_email".takeUnless { useEmptyValues }.orEmpty(),
domainName = "forwardemail.net".takeUnless { useEmptyValues }.orEmpty(),
)
}
}

View File

@@ -12,11 +12,13 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for AddyIo returns null when apiAccessToken is blank`() {
val addyIoServiceType = ServiceType.AddyIo(
apiAccessToken = "",
domainName = "test.com",
baseUrl = "http://test.com",
selfHostServerUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
val request = addyIoServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@@ -25,23 +27,24 @@ internal class ServiceTypeExtensionsTest {
fun `toUsernameGeneratorRequest for AddyIo returns null when domainName is blank`() {
val addyIoServiceType = ServiceType.AddyIo(
apiAccessToken = "testToken",
domainName = "",
baseUrl = "http://test.com",
selfHostServerUrl = "http://test.com",
)
val request = addyIoServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
val request = addyIoServiceType.toUsernameGeneratorRequest(website = null)
assertNull(request)
}
@Test
fun `toUsernameGeneratorRequest for AddyIo returns correct request`() {
fun `toUsernameGeneratorRequest for AddyIo with selfHostServerUrl returns correct request`() {
val addyIoServiceType = ServiceType.AddyIo(
apiAccessToken = "testToken",
domainName = "test.com",
baseUrl = "http://test.com",
selfHostServerUrl = "http://test.com",
)
val website = "bitwarden.com"
val request = addyIoServiceType.toUsernameGeneratorRequest(website)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@@ -52,6 +55,51 @@ internal class ServiceTypeExtensionsTest {
),
website = website,
),
addyIoServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
),
)
// Verify the correct request is returned when allowAddyIoSelfHostUrl is false
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.AddyIo(
apiToken = "testToken",
domain = "test.com",
baseUrl = ServiceType.AddyIo.DEFAULT_ADDY_IO_URL,
),
website = website,
),
addyIoServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = false,
),
)
}
@Suppress("MaxLineLength")
@Test
fun `toUsernameGeneratorRequest for AddyIo without selfHostServerUrl returns correct request`() {
val addyIoServiceType = ServiceType.AddyIo(
apiAccessToken = "testToken",
domainName = "test.com",
)
val website = "bitwarden.com"
val request = addyIoServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
)
assertEquals(
UsernameGeneratorRequest.Forwarded(
service = ForwarderServiceType.AddyIo(
apiToken = "testToken",
domain = "test.com",
baseUrl = ServiceType.AddyIo.DEFAULT_ADDY_IO_URL,
),
website = website,
),
request,
)
}
@@ -59,7 +107,10 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for DuckDuckGo returns null when apiKey is blank`() {
val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "")
val request = duckDuckGoServiceType.toUsernameGeneratorRequest(website = null)
val request = duckDuckGoServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
assertNull(request)
}
@@ -68,7 +119,10 @@ internal class ServiceTypeExtensionsTest {
fun `toUsernameGeneratorRequest for DuckDuckGo returns correct request`() {
val duckDuckGoServiceType = ServiceType.DuckDuckGo(apiKey = "testKey")
val website = "bitwarden.com"
val request = duckDuckGoServiceType.toUsernameGeneratorRequest(website)
val request = duckDuckGoServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@@ -82,7 +136,10 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for FirefoxRelay returns null when apiAccessToken is blank`() {
val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "")
val request = firefoxRelayServiceType.toUsernameGeneratorRequest(website = null)
val request = firefoxRelayServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
assertNull(request)
}
@@ -91,7 +148,10 @@ internal class ServiceTypeExtensionsTest {
fun `toUsernameGeneratorRequest for FirefoxRelay returns correct request`() {
val firefoxRelayServiceType = ServiceType.FirefoxRelay(apiAccessToken = "testToken")
val website = "bitwarden.com"
val request = firefoxRelayServiceType.toUsernameGeneratorRequest(website)
val request = firefoxRelayServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@@ -105,7 +165,10 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for FastMail returns null when apiKey is blank`() {
val fastMailServiceType = ServiceType.FastMail(apiKey = "")
val request = fastMailServiceType.toUsernameGeneratorRequest(website = null)
val request = fastMailServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
assertNull(request)
}
@@ -116,7 +179,10 @@ internal class ServiceTypeExtensionsTest {
apiKey = "testKey",
)
val website = "bitwarden.com"
val request = fastMailServiceType.toUsernameGeneratorRequest(website)
val request = fastMailServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@@ -133,7 +199,10 @@ internal class ServiceTypeExtensionsTest {
apiKey = "",
domainName = "domainName",
)
val request = forwardMailServiceType.toUsernameGeneratorRequest(website = null)
val request = forwardMailServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
assertNull(request)
}
@@ -144,7 +213,10 @@ internal class ServiceTypeExtensionsTest {
apiKey = "apiKey",
domainName = "",
)
val request = forwardMailServiceType.toUsernameGeneratorRequest(website = null)
val request = forwardMailServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
assertNull(request)
}
@@ -156,7 +228,10 @@ internal class ServiceTypeExtensionsTest {
domainName = "domainName",
)
val website = "bitwarden.com"
val request = forwardEmailServiceType.toUsernameGeneratorRequest(website)
val request = forwardEmailServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
)
assertEquals(
UsernameGeneratorRequest.Forwarded(
@@ -173,7 +248,10 @@ internal class ServiceTypeExtensionsTest {
@Test
fun `toUsernameGeneratorRequest for SimpleLogin returns null when apiKey is blank`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "")
val request = simpleLoginServiceType.toUsernameGeneratorRequest(website = null)
val request = simpleLoginServiceType.toUsernameGeneratorRequest(
website = null,
allowAddyIoSelfHostUrl = true,
)
assertNull(request)
}
@@ -182,7 +260,10 @@ internal class ServiceTypeExtensionsTest {
fun `toUsernameGeneratorRequest for SimpleLogin returns correct request`() {
val simpleLoginServiceType = ServiceType.SimpleLogin(apiKey = "testKey")
val website = "bitwarden.com"
val request = simpleLoginServiceType.toUsernameGeneratorRequest(website)
val request = simpleLoginServiceType.toUsernameGeneratorRequest(
website = website,
allowAddyIoSelfHostUrl = true,
)
assertEquals(
UsernameGeneratorRequest.Forwarded(