Merge pull request #7091 from thundernest/add_config_validation_screen
Add config validation screen
This commit is contained in:
commit
6b3c7a8f57
15 changed files with 832 additions and 29 deletions
|
@ -7,17 +7,17 @@ import assertk.Assert
|
|||
import assertk.all
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
||||
/**
|
||||
* The `turbines` extension function creates a MviTurbines instance for the given MVI ViewModel.
|
||||
*/
|
||||
inline fun <reified STATE, EVENT, EFFECT> UnidirectionalViewModel<STATE, EVENT, EFFECT>.turbines(
|
||||
coroutineScope: CoroutineScope,
|
||||
inline fun <reified STATE, EVENT, EFFECT> TestScope.turbines(
|
||||
viewModel: UnidirectionalViewModel<STATE, EVENT, EFFECT>,
|
||||
): MviTurbines<STATE, EFFECT> {
|
||||
return MviTurbines(
|
||||
stateTurbine = state.testIn(coroutineScope),
|
||||
effectTurbine = effect.testIn(coroutineScope),
|
||||
stateTurbine = viewModel.state.testIn(backgroundScope),
|
||||
effectTurbine = viewModel.effect.testIn(backgroundScope),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -25,15 +25,11 @@ inline fun <reified STATE, EVENT, EFFECT> UnidirectionalViewModel<STATE, EVENT,
|
|||
* The `turbinesWithInitialStateCheck` extension function creates a MviTurbines instance for the given MVI ViewModel
|
||||
* and ensures that the initial state is emitted.
|
||||
*/
|
||||
suspend inline fun <reified STATE, EVENT, EFFECT>
|
||||
UnidirectionalViewModel<STATE, EVENT, EFFECT>.turbinesWithInitialStateCheck(
|
||||
coroutineScope: CoroutineScope,
|
||||
initialState: STATE,
|
||||
): MviTurbines<STATE, EFFECT> {
|
||||
val turbines = MviTurbines(
|
||||
stateTurbine = state.testIn(coroutineScope),
|
||||
effectTurbine = effect.testIn(coroutineScope),
|
||||
)
|
||||
suspend inline fun <reified STATE, EVENT, EFFECT> TestScope.turbinesWithInitialStateCheck(
|
||||
viewModel: UnidirectionalViewModel<STATE, EVENT, EFFECT>,
|
||||
initialState: STATE,
|
||||
): MviTurbines<STATE, EFFECT> {
|
||||
val turbines = turbines(viewModel)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
|
|
|
@ -8,6 +8,7 @@ import app.k9mail.feature.account.setup.domain.usecase.CheckIncomingServerConfig
|
|||
import app.k9mail.feature.account.setup.domain.usecase.CheckOutgoingServerConfig
|
||||
import app.k9mail.feature.account.setup.domain.usecase.CreateAccount
|
||||
import app.k9mail.feature.account.setup.domain.usecase.GetAutoDiscovery
|
||||
import app.k9mail.feature.account.setup.domain.usecase.ValidateServerSettings
|
||||
import app.k9mail.feature.account.setup.ui.AccountSetupViewModel
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryContract
|
||||
import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryValidator
|
||||
|
@ -21,6 +22,7 @@ import app.k9mail.feature.account.setup.ui.options.AccountOptionsViewModel
|
|||
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
|
||||
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigValidator
|
||||
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigViewModel
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationViewModel
|
||||
import com.fsck.k9.mail.store.imap.ImapServerSettingsValidator
|
||||
import com.fsck.k9.mail.store.pop3.Pop3ServerSettingsValidator
|
||||
import com.fsck.k9.mail.transport.smtp.SmtpServerSettingsValidator
|
||||
|
@ -49,6 +51,23 @@ val featureAccountSetupModule: Module = module {
|
|||
)
|
||||
}
|
||||
|
||||
factory<DomainContract.UseCase.ValidateServerSettings> {
|
||||
ValidateServerSettings(
|
||||
imapValidator = ImapServerSettingsValidator(
|
||||
trustedSocketFactory = get(),
|
||||
oAuth2TokenProvider = null, // TODO
|
||||
clientIdAppName = "null",
|
||||
),
|
||||
pop3Validator = Pop3ServerSettingsValidator(
|
||||
trustedSocketFactory = get(),
|
||||
),
|
||||
smtpValidator = SmtpServerSettingsValidator(
|
||||
trustedSocketFactory = get(),
|
||||
oAuth2TokenProvider = null, // TODO
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
factory<DomainContract.UseCase.CheckIncomingServerConfig> {
|
||||
CheckIncomingServerConfig(
|
||||
imapValidator = ImapServerSettingsValidator(
|
||||
|
@ -91,6 +110,11 @@ val featureAccountSetupModule: Module = module {
|
|||
getAutoDiscovery = get(),
|
||||
)
|
||||
}
|
||||
viewModel {
|
||||
AccountValidationViewModel(
|
||||
validateServerSettings = get(),
|
||||
)
|
||||
}
|
||||
viewModel {
|
||||
AccountIncomingConfigViewModel(
|
||||
validator = get(),
|
||||
|
|
|
@ -7,23 +7,23 @@ import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContrac
|
|||
internal fun Error.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is Error.AuthenticationError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_authentication,
|
||||
R.string.account_setup_settings_validation_error_authentication,
|
||||
)
|
||||
|
||||
is Error.CertificateError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_certificate,
|
||||
R.string.account_setup_settings_validation_error_certificate,
|
||||
)
|
||||
|
||||
is Error.NetworkError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_network,
|
||||
R.string.account_setup_settings_validation_error_network,
|
||||
)
|
||||
|
||||
is Error.ServerError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_server,
|
||||
R.string.account_setup_settings_validation_error_server,
|
||||
)
|
||||
|
||||
is Error.UnknownError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_unknown,
|
||||
R.string.account_setup_settings_validation_error_unknown,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,23 +7,23 @@ import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContrac
|
|||
internal fun Error.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is Error.AuthenticationError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_authentication,
|
||||
R.string.account_setup_settings_validation_error_authentication,
|
||||
)
|
||||
|
||||
is Error.CertificateError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_certificate,
|
||||
R.string.account_setup_settings_validation_error_certificate,
|
||||
)
|
||||
|
||||
is Error.NetworkError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_network,
|
||||
R.string.account_setup_settings_validation_error_network,
|
||||
)
|
||||
|
||||
is Error.ServerError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_server,
|
||||
R.string.account_setup_settings_validation_error_server,
|
||||
)
|
||||
|
||||
is Error.UnknownError -> resources.getString(
|
||||
R.string.account_setup_check_config_error_unknown,
|
||||
R.string.account_setup_settings_validation_error_unknown,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.k9mail.core.ui.compose.common.DevicePreviews
|
||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
|
||||
import app.k9mail.core.ui.compose.designsystem.template.ResponsiveWidthContainer
|
||||
import app.k9mail.core.ui.compose.theme.K9Theme
|
||||
import app.k9mail.core.ui.compose.theme.MainTheme
|
||||
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
|
||||
import app.k9mail.feature.account.common.ui.item.ErrorItem
|
||||
import app.k9mail.feature.account.common.ui.item.LoadingItem
|
||||
import app.k9mail.feature.account.common.ui.item.SuccessItem
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun AccountValidationContent(
|
||||
state: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val resources = LocalContext.current.resources
|
||||
|
||||
ResponsiveWidthContainer(
|
||||
modifier = Modifier
|
||||
.testTag("AccountIncomingConfigContent")
|
||||
.padding(contentPadding)
|
||||
.fillMaxWidth()
|
||||
.then(modifier),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = if (state.isLoading || state.error != null || state.isSuccess) {
|
||||
Arrangement.spacedBy(MainTheme.spacings.double, Alignment.CenterVertically)
|
||||
} else {
|
||||
Arrangement.spacedBy(MainTheme.spacings.default)
|
||||
},
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
item(key = "loading") {
|
||||
LoadingItem(
|
||||
message = stringResource(id = R.string.account_setup_incoming_config_loading_message),
|
||||
)
|
||||
}
|
||||
} else if (state.error != null) {
|
||||
item(key = "error") {
|
||||
// TODO add raw error message
|
||||
ErrorItem(
|
||||
title = stringResource(id = R.string.account_setup_incoming_config_loading_error),
|
||||
message = state.error.toResourceString(resources),
|
||||
onRetry = { onEvent(Event.OnRetryClicked) },
|
||||
)
|
||||
}
|
||||
} else if (state.isSuccess) {
|
||||
item(key = "success") {
|
||||
SuccessItem(
|
||||
message = stringResource(id = R.string.account_setup_incoming_config_success),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
TextSubtitle1(text = "Should not happen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DevicePreviews
|
||||
internal fun AccountIncomingConfigContentK9Preview() {
|
||||
K9Theme {
|
||||
AccountValidationContent(
|
||||
onEvent = { },
|
||||
state = State(),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DevicePreviews
|
||||
internal fun AccountIncomingConfigContentThunderbirdPreview() {
|
||||
ThunderbirdTheme {
|
||||
AccountValidationContent(
|
||||
onEvent = { },
|
||||
state = State(),
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import java.io.IOException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
interface AccountValidationContract {
|
||||
|
||||
interface ViewModel : UnidirectionalViewModel<State, Event, Effect> {
|
||||
fun initState(state: State)
|
||||
}
|
||||
|
||||
data class State(
|
||||
val serverSettings: ServerSettings? = null,
|
||||
val authorizationState: AuthorizationState? = null,
|
||||
val wizardNavigationBarState: WizardNavigationBarState = WizardNavigationBarState(
|
||||
showNext = false,
|
||||
),
|
||||
val isSuccess: Boolean = false,
|
||||
val error: Error? = null,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
object ValidateServerSettings : Event
|
||||
object OnNextClicked : Event
|
||||
object OnBackClicked : Event
|
||||
object OnRetryClicked : Event
|
||||
}
|
||||
|
||||
sealed interface Effect {
|
||||
object NavigateNext : Effect
|
||||
object NavigateBack : Effect
|
||||
}
|
||||
|
||||
sealed interface Error {
|
||||
data class NetworkError(val exception: IOException) : Error
|
||||
data class CertificateError(val certificateChain: List<X509Certificate>) : Error
|
||||
data class AuthenticationError(val serverMessage: String?) : Error
|
||||
data class ServerError(val serverMessage: String?) : Error
|
||||
data class UnknownError(val message: String) : Error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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
|
||||
import app.k9mail.core.ui.compose.theme.K9Theme
|
||||
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
|
||||
import app.k9mail.feature.account.common.ui.AppTitleTopHeader
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBar
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.ViewModel
|
||||
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||
|
||||
@Composable
|
||||
internal fun AccountValidationScreen(
|
||||
onNext: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
when (effect) {
|
||||
is Effect.NavigateNext -> onNext()
|
||||
is Effect.NavigateBack -> onBack()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
dispatch(Event.ValidateServerSettings)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
dispatch(Event.OnBackClicked)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTitleTopHeader(title = stringResource(id = R.string.account_setup_title))
|
||||
},
|
||||
bottomBar = {
|
||||
WizardNavigationBar(
|
||||
nextButtonText = stringResource(id = R.string.account_setup_button_next),
|
||||
backButtonText = stringResource(id = R.string.account_setup_button_back),
|
||||
onNextClick = { },
|
||||
onBackClick = { dispatch(Event.OnBackClicked) },
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
) { innerPadding ->
|
||||
AccountValidationContent(
|
||||
onEvent = { dispatch(it) },
|
||||
state = state.value,
|
||||
contentPadding = innerPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DevicePreviews
|
||||
internal fun AccountIncomingConfigScreenK9Preview() {
|
||||
K9Theme {
|
||||
AccountValidationScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = AccountValidationViewModel(
|
||||
validateServerSettings = {
|
||||
ServerSettingsValidationResult.Success
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@DevicePreviews
|
||||
internal fun AccountIncomingConfigScreenThunderbirdPreview() {
|
||||
ThunderbirdTheme {
|
||||
AccountValidationScreen(
|
||||
onNext = {},
|
||||
onBack = {},
|
||||
viewModel = AccountValidationViewModel(
|
||||
validateServerSettings = {
|
||||
ServerSettingsValidationResult.Success
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import android.content.res.Resources
|
||||
import app.k9mail.feature.account.setup.R
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Error
|
||||
|
||||
internal fun Error.toResourceString(resources: Resources): String {
|
||||
return when (this) {
|
||||
is Error.AuthenticationError -> resources.getString(
|
||||
R.string.account_setup_settings_validation_error_authentication,
|
||||
)
|
||||
|
||||
is Error.CertificateError -> resources.getString(
|
||||
R.string.account_setup_settings_validation_error_certificate,
|
||||
)
|
||||
|
||||
is Error.NetworkError -> resources.getString(
|
||||
R.string.account_setup_settings_validation_error_network,
|
||||
)
|
||||
|
||||
is Error.ServerError -> resources.getString(
|
||||
R.string.account_setup_settings_validation_error_server,
|
||||
)
|
||||
|
||||
is Error.UnknownError -> resources.getString(
|
||||
R.string.account_setup_settings_validation_error_unknown,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.setup.domain.DomainContract
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Error
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.ViewModel
|
||||
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val CONTINUE_NEXT_DELAY = 1000L
|
||||
|
||||
internal class AccountValidationViewModel(
|
||||
initialState: State = State(),
|
||||
private val validateServerSettings: DomainContract.UseCase.ValidateServerSettings,
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState {
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
isSuccess = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.ValidateServerSettings -> onValidateConfig()
|
||||
Event.OnNextClicked -> TODO()
|
||||
Event.OnBackClicked -> onBack()
|
||||
Event.OnRetryClicked -> onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onValidateConfig() {
|
||||
if (state.value.isSuccess) {
|
||||
navigateNext()
|
||||
} else {
|
||||
validateServerSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateServerSettings() {
|
||||
viewModelScope.launch {
|
||||
val serverSettings = state.value.serverSettings
|
||||
if (serverSettings == null) {
|
||||
updateError(Error.UnknownError("Server settings not set"))
|
||||
return@launch
|
||||
}
|
||||
|
||||
updateState {
|
||||
it.copy(isLoading = true)
|
||||
}
|
||||
|
||||
when (val result = validateServerSettings.execute(serverSettings)) {
|
||||
ServerSettingsValidationResult.Success -> updateSuccess()
|
||||
|
||||
is ServerSettingsValidationResult.AuthenticationError -> updateError(
|
||||
Error.AuthenticationError(result.serverMessage),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.CertificateError -> updateError(
|
||||
Error.CertificateError(result.certificateChain),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.NetworkError -> updateError(
|
||||
Error.NetworkError(result.exception),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.ServerError -> updateError(
|
||||
Error.ServerError(result.serverMessage),
|
||||
)
|
||||
|
||||
is ServerSettingsValidationResult.UnknownError -> updateError(
|
||||
Error.UnknownError(result.exception.message ?: "Unknown error"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSuccess() {
|
||||
updateState {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isSuccess = true,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
delay(CONTINUE_NEXT_DELAY)
|
||||
navigateNext()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateError(error: Error) {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = error,
|
||||
isLoading = false,
|
||||
isSuccess = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBack() {
|
||||
if (state.value.isSuccess) {
|
||||
updateState {
|
||||
it.copy(
|
||||
isSuccess = false,
|
||||
)
|
||||
}
|
||||
} else if (state.value.error != null) {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetry() {
|
||||
updateState {
|
||||
it.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
onValidateConfig()
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext() {
|
||||
emitEffect(Effect.NavigateNext)
|
||||
}
|
||||
}
|
|
@ -27,11 +27,11 @@
|
|||
<string name="account_setup_validation_error_password_required">Password is required.</string>
|
||||
<string name="account_setup_validation_error_imap_prefix_blank">Imap prefix can\'t be blank.</string>
|
||||
|
||||
<string name="account_setup_check_config_error_authentication">Authentication error</string>
|
||||
<string name="account_setup_check_config_error_certificate">Certificate error</string>
|
||||
<string name="account_setup_check_config_error_network">Network error</string>
|
||||
<string name="account_setup_check_config_error_server">Server error</string>
|
||||
<string name="account_setup_check_config_error_unknown">Unknown error</string>
|
||||
<string name="account_setup_settings_validation_error_authentication">Authentication error</string>
|
||||
<string name="account_setup_settings_validation_error_certificate">Certificate error</string>
|
||||
<string name="account_setup_settings_validation_error_network">Network error</string>
|
||||
<string name="account_setup_settings_validation_error_server">Server error</string>
|
||||
<string name="account_setup_settings_validation_error_unknown">Unknown error</string>
|
||||
|
||||
<string name="account_setup_auto_config_loading_message">Finding email details</string>
|
||||
<string name="account_setup_auto_config_loading_error">Failed to load email configuration</string>
|
||||
|
|
|
@ -8,6 +8,7 @@ import app.k9mail.feature.account.setup.ui.autodiscovery.AccountAutoDiscoveryCon
|
|||
import app.k9mail.feature.account.setup.ui.incoming.AccountIncomingConfigContract
|
||||
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
|
||||
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract
|
||||
import com.fsck.k9.mail.ssl.TrustedSocketFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Test
|
||||
|
@ -44,6 +45,7 @@ class AccountSetupModuleKtTest : KoinTest {
|
|||
extraTypes = listOf(
|
||||
AccountSetupContract.State::class,
|
||||
AccountAutoDiscoveryContract.State::class,
|
||||
AccountValidationContract.State::class,
|
||||
AccountIncomingConfigContract.State::class,
|
||||
AccountOutgoingConfigContract.State::class,
|
||||
AccountOptionsContract.State::class,
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.ComposeTest
|
||||
import app.k9mail.core.ui.compose.testing.setContent
|
||||
import app.k9mail.core.ui.compose.theme.ThunderbirdTheme
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class AccountValidationScreenKtTest : ComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `should delegate navigation effects`() = runTest {
|
||||
val initialState = State()
|
||||
val viewModel = FakeAccountValidationViewModel(initialState)
|
||||
var onNextCounter = 0
|
||||
var onBackCounter = 0
|
||||
|
||||
setContent {
|
||||
ThunderbirdTheme {
|
||||
AccountValidationScreen(
|
||||
onNext = { onNextCounter++ },
|
||||
onBack = { onBackCounter++ },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(onNextCounter).isEqualTo(0)
|
||||
assertThat(onBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateNext)
|
||||
|
||||
assertThat(onNextCounter).isEqualTo(1)
|
||||
assertThat(onBackCounter).isEqualTo(0)
|
||||
|
||||
viewModel.effect(Effect.NavigateBack)
|
||||
|
||||
assertThat(onNextCounter).isEqualTo(1)
|
||||
assertThat(onBackCounter).isEqualTo(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import app.k9mail.feature.account.common.ui.WizardNavigationBarState
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
class AccountValidationStateTest {
|
||||
|
||||
@Test
|
||||
fun `should set default values`() {
|
||||
val state = State()
|
||||
|
||||
assertThat(state).isEqualTo(
|
||||
State(
|
||||
serverSettings = null,
|
||||
authorizationState = null,
|
||||
wizardNavigationBarState = WizardNavigationBarState(
|
||||
showNext = false,
|
||||
),
|
||||
isSuccess = false,
|
||||
error = null,
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
|
||||
import app.k9mail.core.ui.compose.testing.mvi.assertThatAndMviTurbinesConsumed
|
||||
import app.k9mail.core.ui.compose.testing.mvi.turbinesWithInitialStateCheck
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Error
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isTrue
|
||||
import com.fsck.k9.mail.AuthType
|
||||
import com.fsck.k9.mail.ConnectionSecurity
|
||||
import com.fsck.k9.mail.ServerSettings
|
||||
import com.fsck.k9.mail.server.ServerSettingsValidationResult
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AccountValidationViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `should reset state when InitState event received`() = runTest {
|
||||
val testSubject = createTestSubject()
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, State())
|
||||
val newState = State(
|
||||
serverSettings = IMAP_SERVER_SETTINGS,
|
||||
isLoading = true,
|
||||
error = Error.ServerError("server error"),
|
||||
isSuccess = true,
|
||||
)
|
||||
val expectedState = newState.copy(
|
||||
isLoading = false,
|
||||
error = null,
|
||||
isSuccess = false,
|
||||
)
|
||||
|
||||
testSubject.initState(newState)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(expectedState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should fail when ValidateServerSettings event received and server settings null`() = runTest {
|
||||
val initialState = State()
|
||||
val testSubject = createTestSubject()
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.ValidateServerSettings)
|
||||
|
||||
val errorState = initialState.copy(
|
||||
error = Error.UnknownError("Server settings not set"),
|
||||
)
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(errorState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should validate server settings when ValidateServerSettings event received`() = runTest {
|
||||
val initialState = State(
|
||||
serverSettings = IMAP_SERVER_SETTINGS,
|
||||
)
|
||||
val testSubject = createTestSubject(
|
||||
serverSettingsValidationResult = ServerSettingsValidationResult.Success,
|
||||
initialState = initialState,
|
||||
)
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.ValidateServerSettings)
|
||||
|
||||
val loadingState = initialState.copy(isLoading = true)
|
||||
assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(loadingState)
|
||||
|
||||
val successState = loadingState.copy(
|
||||
isLoading = false,
|
||||
isSuccess = true,
|
||||
)
|
||||
assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(successState)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set error state when ValidateServerSettings received and check settings failed`() = runTest {
|
||||
val initialState = State(
|
||||
serverSettings = IMAP_SERVER_SETTINGS,
|
||||
)
|
||||
val testSubject = createTestSubject(
|
||||
serverSettingsValidationResult = ServerSettingsValidationResult.ServerError("server error"),
|
||||
initialState = initialState,
|
||||
)
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.ValidateServerSettings)
|
||||
|
||||
val loadingState = initialState.copy(isLoading = true)
|
||||
assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(loadingState)
|
||||
|
||||
val failureState = loadingState.copy(
|
||||
isLoading = false,
|
||||
error = Error.ServerError("server error"),
|
||||
)
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(failureState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit effect NavigateNext when ValidateConfig is successful`() = runTest {
|
||||
val initialState = State(
|
||||
serverSettings = IMAP_SERVER_SETTINGS,
|
||||
isSuccess = true,
|
||||
)
|
||||
val testSubject = createTestSubject(
|
||||
initialState = initialState,
|
||||
)
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.ValidateServerSettings)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
|
||||
val testSubject = createTestSubject()
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, State())
|
||||
|
||||
testSubject.event(Event.OnBackClicked)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear isSuccess when OnBackClicked event received when in success state`() = runTest {
|
||||
val initialState = State(isSuccess = true)
|
||||
val testSubject = createTestSubject(initialState = initialState)
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.OnBackClicked)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState.copy(isSuccess = false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear error when OnBackClicked event received when in error state`() = runTest {
|
||||
val initialState = State(error = Error.ServerError("server error"))
|
||||
val testSubject = createTestSubject(initialState = initialState)
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.OnBackClicked)
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState.copy(error = null))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should clear error and trigger check settings when OnRetryClicked event received`() = runTest {
|
||||
val initialState = State(
|
||||
serverSettings = IMAP_SERVER_SETTINGS,
|
||||
error = Error.ServerError("server error"),
|
||||
)
|
||||
var checkSettingsCalled = false
|
||||
val testSubject = AccountValidationViewModel(
|
||||
validateServerSettings = {
|
||||
delay(50)
|
||||
checkSettingsCalled = true
|
||||
ServerSettingsValidationResult.Success
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
val turbines = turbinesWithInitialStateCheck(testSubject, initialState)
|
||||
|
||||
testSubject.event(Event.OnRetryClicked)
|
||||
|
||||
val stateWithoutError = initialState.copy(error = null)
|
||||
assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(stateWithoutError)
|
||||
|
||||
val loadingState = stateWithoutError.copy(isLoading = true)
|
||||
assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(loadingState)
|
||||
|
||||
val successState = loadingState.copy(isLoading = false, isSuccess = true)
|
||||
assertThat(turbines.stateTurbine.awaitItem()).isEqualTo(successState)
|
||||
assertThat(checkSettingsCalled).isTrue()
|
||||
|
||||
assertThatAndMviTurbinesConsumed(
|
||||
actual = turbines.effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun createTestSubject(
|
||||
serverSettingsValidationResult: ServerSettingsValidationResult = ServerSettingsValidationResult.Success,
|
||||
initialState: State = State(),
|
||||
): AccountValidationViewModel {
|
||||
return AccountValidationViewModel(
|
||||
validateServerSettings = {
|
||||
delay(50)
|
||||
serverSettingsValidationResult
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
}
|
||||
|
||||
val IMAP_SERVER_SETTINGS = ServerSettings(
|
||||
type = "imap",
|
||||
host = "imap.example.com",
|
||||
port = 465,
|
||||
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED,
|
||||
authenticationType = AuthType.PLAIN,
|
||||
username = "username",
|
||||
password = "password",
|
||||
clientCertificateAlias = null,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package app.k9mail.feature.account.setup.ui.validation
|
||||
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Effect
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.Event
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.State
|
||||
import app.k9mail.feature.account.setup.ui.validation.AccountValidationContract.ViewModel
|
||||
|
||||
class FakeAccountValidationViewModel(
|
||||
initialState: State = State(),
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
val events = mutableListOf<Event>()
|
||||
|
||||
override fun initState(state: State) {
|
||||
updateState { state }
|
||||
}
|
||||
|
||||
override fun event(event: Event) {
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
fun effect(effect: Effect) {
|
||||
emitEffect(effect)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue