Add AutoDiscovery loading

This commit is contained in:
Wolf-Martell Montwé 2023-06-06 17:43:30 +02:00
parent 54d082c919
commit 8df500b52e
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
9 changed files with 198 additions and 24 deletions

View file

@ -43,6 +43,7 @@ val featureAccountSetupModule: Module = module {
viewModel {
AccountAutoConfigViewModel(
validator = get(),
getAutoDiscovery = get(),
)
}
viewModel {

View file

@ -4,7 +4,7 @@ import app.k9mail.autodiscovery.api.AutoDiscoveryResult
interface DomainContract {
interface GetAutoDiscoveryUseCase {
fun interface GetAutoDiscoveryUseCase {
suspend fun execute(emailAddress: String): AutoDiscoveryResult
}
}

View file

@ -57,7 +57,7 @@ internal fun AccountAutoConfigContent(
item(key = "error") {
ErrorItem(
title = stringResource(id = R.string.account_setup_auto_config_loading_error),
message = state.error.toResourceString(resources)
message = state.error.toResourceString(resources),
)
}
} else {

View file

@ -1,8 +1,8 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
import app.k9mail.feature.account.setup.domain.entity.AutoDiscovery
import app.k9mail.feature.account.setup.domain.input.StringInputField
interface AccountAutoConfigContract {
@ -21,7 +21,7 @@ interface AccountAutoConfigContract {
val configStep: ConfigStep = ConfigStep.EMAIL_ADDRESS,
val emailAddress: StringInputField = StringInputField(),
val password: StringInputField = StringInputField(),
val autoDiscovery: AutoDiscovery? = null,
val autoDiscoverySettings: AutoDiscoveryResult.Settings? = null,
val error: Error? = null,
val isLoading: Boolean = false,
)

View file

@ -3,6 +3,7 @@ package app.k9mail.feature.account.setup.ui.autoconfig
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.core.ui.compose.common.DevicePreviews
import app.k9mail.core.ui.compose.common.mvi.observe
import app.k9mail.core.ui.compose.designsystem.template.Scaffold
@ -60,6 +61,7 @@ internal fun AccountAutoConfigScreenK9Preview() {
onBack = {},
viewModel = AccountAutoConfigViewModel(
validator = AccountAutoConfigValidator(),
getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
),
)
}
@ -74,6 +76,7 @@ internal fun AccountAutoConfigScreenThunderbirdPreview() {
onBack = {},
viewModel = AccountAutoConfigViewModel(
validator = AccountAutoConfigValidator(),
getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
),
)
}

View file

@ -1,19 +1,25 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import androidx.lifecycle.viewModelScope
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
import app.k9mail.feature.account.setup.domain.DomainContract
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Effect
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Error
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Event
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.State
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.Validator
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ViewModel
import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
class AccountAutoConfigViewModel(
initialState: State = State(),
private val validator: Validator,
private val getAutoDiscovery: DomainContract.GetAutoDiscoveryUseCase,
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
override fun initState(state: State) {
@ -69,10 +75,50 @@ class AccountAutoConfigViewModel(
updateState {
it.copy(
configStep = if (hasError) ConfigStep.EMAIL_ADDRESS else ConfigStep.PASSWORD,
emailAddress = it.emailAddress.updateFromValidationResult(emailValidationResult),
)
}
if (!hasError) {
loadAutoConfig()
}
}
}
private fun loadAutoConfig() {
viewModelScope.launch {
updateState {
it.copy(
isLoading = true,
)
}
val result = getAutoDiscovery.execute(state.value.emailAddress.value)
when (result) {
AutoDiscoveryResult.NoUsableSettingsFound -> updateAutoDiscoverySettings(null)
is AutoDiscoveryResult.Settings -> updateAutoDiscoverySettings(result)
is AutoDiscoveryResult.NetworkError -> updateError(Error.NetworkError)
is AutoDiscoveryResult.UnexpectedException -> updateError(Error.UnknownError)
}
}
}
private fun updateAutoDiscoverySettings(settings: AutoDiscoveryResult.Settings?) {
updateState {
it.copy(
isLoading = false,
autoDiscoverySettings = settings,
configStep = ConfigStep.PASSWORD, // TODO use oauth if applicable
)
}
}
private fun updateError(error: Error) {
updateState {
it.copy(
isLoading = false,
error = error,
)
}
}

View file

@ -0,0 +1,30 @@
package app.k9mail.feature.account.setup.domain.entity
import app.k9mail.autodiscovery.api.AuthenticationType
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.autodiscovery.api.ConnectionSecurity
import app.k9mail.autodiscovery.api.ImapServerSettings
import app.k9mail.autodiscovery.api.SmtpServerSettings
import app.k9mail.core.common.net.toHostname
import app.k9mail.core.common.net.toPort
object AutoDiscoverySettingsFixture {
val settings = AutoDiscoveryResult.Settings(
incomingServerSettings = ImapServerSettings(
hostname = "incoming.example.com".toHostname(),
port = 123.toPort(),
connectionSecurity = ConnectionSecurity.TLS,
authenticationType = AuthenticationType.PasswordEncrypted,
username = "incoming_username",
),
outgoingServerSettings = SmtpServerSettings(
hostname = "outgoing.example.com".toHostname(),
port = 456.toPort(),
connectionSecurity = ConnectionSecurity.TLS,
authenticationType = AuthenticationType.PasswordEncrypted,
username = "outgoing_username",
),
isTrusted = true,
)
}

View file

@ -18,7 +18,7 @@ class AccountAutoConfigStateTest {
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(),
password = StringInputField(),
autoDiscovery = null,
autoDiscoverySettings = null,
error = null,
isLoading = false,
),

View file

@ -1,9 +1,11 @@
package app.k9mail.feature.account.setup.ui.autoconfig
import app.cash.turbine.testIn
import app.k9mail.autodiscovery.api.AutoDiscoveryResult
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
import app.k9mail.feature.account.setup.domain.entity.AutoDiscoverySettingsFixture
import app.k9mail.feature.account.setup.domain.input.StringInputField
import app.k9mail.feature.account.setup.testing.eventStateTest
import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.ConfigStep
@ -13,12 +15,11 @@ import app.k9mail.feature.account.setup.ui.autoconfig.AccountAutoConfigContract.
import assertk.assertThat
import assertk.assertions.assertThatAndTurbinesConsumed
import assertk.assertions.isEqualTo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class AccountAutoConfigViewModelTest {
@get:Rule
@ -26,6 +27,10 @@ class AccountAutoConfigViewModelTest {
private val testSubject = AccountAutoConfigViewModel(
validator = FakeAccountAutoConfigValidator(),
getAutoDiscovery = {
delay(50)
AutoDiscoveryResult.NoUsableSettingsFound
},
)
@Test
@ -64,28 +69,115 @@ class AccountAutoConfigViewModelTest {
}
@Test
fun `should change config step to password when OnNextClicked event is received`() = runTest {
val initialState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "email"),
)
testSubject.initState(initialState)
fun `should change state to password when OnNextClicked event is received, input valid and discovery loaded`() =
runTest {
val autoDiscoverySettings = AutoDiscoverySettingsFixture.settings
val initialState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "email"),
)
val viewModel = AccountAutoConfigViewModel(
validator = FakeAccountAutoConfigValidator(),
getAutoDiscovery = {
delay(50)
autoDiscoverySettings
},
initialState = initialState,
)
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
eventStateTest(
viewModel = testSubject,
initialState = initialState,
event = Event.OnNextClicked,
expectedState = State(
configStep = ConfigStep.PASSWORD,
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
viewModel.event(Event.OnNextClicked)
val validatedState = initialState.copy(
emailAddress = StringInputField(
value = "email",
error = null,
isValid = true,
),
),
coroutineScope = backgroundScope,
)
}
)
assertThat(stateTurbine.awaitItem()).isEqualTo(validatedState)
val loadingState = validatedState.copy(
isLoading = true,
)
assertThat(stateTurbine.awaitItem()).isEqualTo(loadingState)
val successState = validatedState.copy(
autoDiscoverySettings = autoDiscoverySettings,
configStep = ConfigStep.PASSWORD,
isLoading = false,
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(successState)
}
}
@Test
fun `should not change state when OnNextClicked event is received, input valid but discovery failed`() =
runTest {
val initialState = State(
configStep = ConfigStep.EMAIL_ADDRESS,
emailAddress = StringInputField(value = "email"),
)
val discoveryError = Exception("discovery error")
val viewModel = AccountAutoConfigViewModel(
validator = FakeAccountAutoConfigValidator(),
getAutoDiscovery = {
delay(50)
AutoDiscoveryResult.UnexpectedException(discoveryError)
},
initialState = initialState,
)
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(initialState)
}
viewModel.event(Event.OnNextClicked)
val validatedState = initialState.copy(
emailAddress = StringInputField(
value = "email",
error = null,
isValid = true,
),
)
assertThat(stateTurbine.awaitItem()).isEqualTo(validatedState)
val loadingState = validatedState.copy(
isLoading = true,
)
assertThat(stateTurbine.awaitItem()).isEqualTo(loadingState)
val failureState = validatedState.copy(
isLoading = false,
error = AccountAutoConfigContract.Error.UnknownError,
)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(failureState)
}
}
@Test
fun `should not change config step to password when OnNextClicked event is received and input invalid`() = runTest {
@ -97,6 +189,7 @@ class AccountAutoConfigViewModelTest {
validator = FakeAccountAutoConfigValidator(
emailAddressAnswer = ValidationResult.Failure(TestError),
),
getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
initialState = initialState,
)
@ -174,6 +267,7 @@ class AccountAutoConfigViewModelTest {
validator = FakeAccountAutoConfigValidator(
passwordAnswer = ValidationResult.Failure(TestError),
),
getAutoDiscovery = { AutoDiscoveryResult.NoUsableSettingsFound },
initialState = initialState,
)
val stateTurbine = viewModel.state.testIn(backgroundScope)