Add AutoDiscovery loading
This commit is contained in:
parent
54d082c919
commit
8df500b52e
9 changed files with 198 additions and 24 deletions
|
@ -43,6 +43,7 @@ val featureAccountSetupModule: Module = module {
|
|||
viewModel {
|
||||
AccountAutoConfigViewModel(
|
||||
validator = get(),
|
||||
getAutoDiscovery = get(),
|
||||
)
|
||||
}
|
||||
viewModel {
|
||||
|
|
|
@ -4,7 +4,7 @@ import app.k9mail.autodiscovery.api.AutoDiscoveryResult
|
|||
|
||||
interface DomainContract {
|
||||
|
||||
interface GetAutoDiscoveryUseCase {
|
||||
fun interface GetAutoDiscoveryUseCase {
|
||||
suspend fun execute(emailAddress: String): AutoDiscoveryResult
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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 },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -18,7 +18,7 @@ class AccountAutoConfigStateTest {
|
|||
configStep = ConfigStep.EMAIL_ADDRESS,
|
||||
emailAddress = StringInputField(),
|
||||
password = StringInputField(),
|
||||
autoDiscovery = null,
|
||||
autoDiscoverySettings = null,
|
||||
error = null,
|
||||
isLoading = false,
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue