Merge pull request #6952 from thundernest/add_account_setup_outgoing_config_validation

Add account setup outgoing config validation
This commit is contained in:
Wolf-Martell Montwé 2023-06-07 15:23:22 +02:00 committed by GitHub
commit d547e845d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 507 additions and 29 deletions

View file

@ -4,16 +4,23 @@ import app.k9mail.feature.account.setup.ui.AccountSetupViewModel
import app.k9mail.feature.account.setup.ui.options.AccountOptionsContract
import app.k9mail.feature.account.setup.ui.options.AccountOptionsValidator
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 org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.module.Module
import org.koin.dsl.module
val featureAccountSetupModule: Module = module {
factory<AccountOutgoingConfigContract.Validator> { AccountOutgoingConfigValidator() }
factory<AccountOptionsContract.Validator> { AccountOptionsValidator() }
viewModel { AccountSetupViewModel() }
viewModel { AccountOutgoingConfigViewModel() }
viewModel {
AccountOutgoingConfigViewModel(
validator = get(),
)
}
viewModel {
AccountOptionsViewModel(
validator = get(),

View file

@ -0,0 +1,21 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
class ValidatePassword : ValidationUseCase<String> {
// TODO change behavior to allow empty password when no password is required based on auth type
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(ValidatePasswordError.EmptyPassword)
else -> ValidationResult.Success
}
}
sealed interface ValidatePasswordError : ValidationError {
object EmptyPassword : ValidatePasswordError
}
}

View file

@ -0,0 +1,26 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
class ValidatePort : ValidationUseCase<Long?> {
override fun execute(input: Long?): ValidationResult {
return when (input) {
null -> ValidationResult.Failure(ValidatePortError.EmptyPort)
in MIN_PORT_NUMBER..MAX_PORT_NUMBER -> ValidationResult.Success
else -> ValidationResult.Failure(ValidatePortError.InvalidPort)
}
}
sealed interface ValidatePortError : ValidationError {
object EmptyPort : ValidatePortError
object InvalidPort : ValidatePortError
}
companion object {
const val MAX_PORT_NUMBER = 65535
const val MIN_PORT_NUMBER = 1
}
}

View file

@ -0,0 +1,20 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
class ValidateServer : ValidationUseCase<String> {
// TODO validate domain, ip4 or ip6
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(ValidateServerError.EmptyServer)
else -> ValidationResult.Success
}
}
sealed interface ValidateServerError : ValidationError {
object EmptyServer : ValidateServerError
}
}

View file

@ -0,0 +1,20 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
class ValidateUsername : ValidationUseCase<String> {
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(ValidateUsernameError.EmptyUsername)
else -> ValidationResult.Success
}
}
sealed interface ValidateUsernameError : ValidationError {
object EmptyUsername : ValidateUsernameError
}
}

View file

@ -63,8 +63,10 @@ internal fun AccountOutgoingConfigContent(
item {
TextInput(
text = state.server.value,
errorMessage = state.server.error?.toResourceString(resources),
onTextChange = { onEvent(Event.ServerChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_server_label),
isRequired = true,
contentPadding = defaultItemPadding(),
)
}
@ -83,8 +85,10 @@ internal fun AccountOutgoingConfigContent(
item {
NumberInput(
value = state.port.value,
errorMessage = state.port.error?.toResourceString(resources),
onValueChange = { onEvent(Event.PortChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_port_label),
isRequired = true,
contentPadding = defaultItemPadding(),
)
}
@ -92,8 +96,10 @@ internal fun AccountOutgoingConfigContent(
item {
TextInput(
text = state.username.value,
errorMessage = state.username.error?.toResourceString(resources),
onTextChange = { onEvent(Event.UsernameChanged(it)) },
label = stringResource(id = R.string.account_setup_outgoing_config_username_label),
isRequired = true,
contentPadding = defaultItemPadding(),
)
}
@ -101,7 +107,9 @@ internal fun AccountOutgoingConfigContent(
item {
PasswordInput(
password = state.password.value,
errorMessage = state.password.error?.toResourceString(resources),
onPasswordChange = { onEvent(Event.PasswordChanged(it)) },
isRequired = true,
contentPadding = defaultItemPadding(),
)
}

View file

@ -1,5 +1,6 @@
package app.k9mail.feature.account.setup.ui.outgoing
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.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.toSmtpDefaultPort
@ -40,4 +41,12 @@ interface AccountOutgoingConfigContract {
object NavigateNext : Effect()
object NavigateBack : Effect()
}
interface Validator {
fun validateServer(server: String): ValidationResult
fun validatePort(port: Long?): ValidationResult
fun validateUsername(username: String): ValidationResult
fun validatePassword(password: String): ValidationResult
}
}

View file

@ -27,7 +27,7 @@ fun AccountOutgoingConfigScreen(
Effect.NavigateBack -> onBack()
Effect.NavigateNext -> onNext()
}
}
}
Scaffold(
topBar = {
@ -60,7 +60,9 @@ internal fun AccountOutgoingConfigScreenK9Preview() {
AccountOutgoingConfigScreen(
onNext = {},
onBack = {},
viewModel = AccountOutgoingConfigViewModel(),
viewModel = AccountOutgoingConfigViewModel(
validator = AccountOutgoingConfigValidator(),
),
)
}
}
@ -72,7 +74,9 @@ internal fun AccountOutgoingConfigScreenThunderbirdPreview() {
AccountOutgoingConfigScreen(
onNext = {},
onBack = {},
viewModel = AccountOutgoingConfigViewModel(),
viewModel = AccountOutgoingConfigViewModel(
validator = AccountOutgoingConfigValidator(),
),
)
}
}

View file

@ -0,0 +1,30 @@
package app.k9mail.feature.account.setup.ui.outgoing
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidatePassword
import app.k9mail.feature.account.setup.domain.usecase.ValidatePort
import app.k9mail.feature.account.setup.domain.usecase.ValidateServer
import app.k9mail.feature.account.setup.domain.usecase.ValidateUsername
class AccountOutgoingConfigValidator(
private val serverValidator: ValidateServer = ValidateServer(),
private val portValidator: ValidatePort = ValidatePort(),
private val usernameValidator: ValidateUsername = ValidateUsername(),
private val passwordValidator: ValidatePassword = ValidatePassword(),
) : AccountOutgoingConfigContract.Validator {
override fun validateServer(server: String): ValidationResult {
return serverValidator.execute(server)
}
override fun validatePort(port: Long?): ValidationResult {
return portValidator.execute(port)
}
override fun validateUsername(username: String): ValidationResult {
return usernameValidator.execute(username)
}
override fun validatePassword(password: String): ValidationResult {
return passwordValidator.execute(password)
}
}

View file

@ -1,5 +1,6 @@
package app.k9mail.feature.account.setup.ui.outgoing
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.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.toSmtpDefaultPort
@ -16,10 +17,12 @@ import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContrac
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Event.UseCompressionChanged
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Event.UsernameChanged
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Validator
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.ViewModel
class AccountOutgoingConfigViewModel(
initialState: State = State(),
private val validator: Validator,
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
override fun initState(state: State) {
@ -56,8 +59,27 @@ class AccountOutgoingConfigViewModel(
}
}
private fun submit() {
navigateNext()
private fun submit() = with(state.value) {
val serverResult = validator.validateServer(server.value)
val portResult = validator.validatePort(port.value)
val usernameResult = validator.validateUsername(username.value)
val passwordResult = validator.validatePassword(password.value)
val hasError = listOf(serverResult, portResult, usernameResult, passwordResult)
.any { it is ValidationResult.Failure }
updateState {
it.copy(
server = it.server.updateFromValidationResult(serverResult),
port = it.port.updateFromValidationResult(portResult),
username = it.username.updateFromValidationResult(usernameResult),
password = it.password.updateFromValidationResult(passwordResult),
)
}
if (!hasError) {
navigateNext()
}
}
private fun navigateBack() = emitEffect(Effect.NavigateBack)

View file

@ -1,8 +1,13 @@
package app.k9mail.feature.account.setup.ui.outgoing
import android.content.res.Resources
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.feature.account.setup.R
import app.k9mail.feature.account.setup.domain.entity.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.usecase.ValidatePassword.ValidatePasswordError
import app.k9mail.feature.account.setup.domain.usecase.ValidatePort.ValidatePortError
import app.k9mail.feature.account.setup.domain.usecase.ValidateServer.ValidateServerError
import app.k9mail.feature.account.setup.domain.usecase.ValidateUsername.ValidateUsernameError
internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
return when (this) {
@ -11,3 +16,49 @@ internal fun ConnectionSecurity.toResourceString(resources: Resources): String {
ConnectionSecurity.TLS -> resources.getString(R.string.account_setup_outgoing_config_security_ssl)
}
}
internal fun ValidationError.toResourceString(resources: Resources): String {
return when (this) {
is ValidateServerError -> toServerErrorString(resources)
is ValidatePortError -> toPortErrorString(resources)
is ValidateUsernameError -> toUsernameErrorString(resources)
is ValidatePasswordError -> toPasswordErrorString(resources)
else -> throw IllegalArgumentException("Unknown error: $this")
}
}
private fun ValidateServerError.toServerErrorString(resources: Resources): String {
return when (this) {
is ValidateServerError.EmptyServer -> resources.getString(
R.string.account_setup_outgoing_config_server_error_required,
)
}
}
private fun ValidatePortError.toPortErrorString(resources: Resources): String {
return when (this) {
is ValidatePortError.EmptyPort -> resources.getString(
R.string.account_setup_outgoing_config_port_error_required,
)
is ValidatePortError.InvalidPort -> resources.getString(
R.string.account_setup_outgoing_config_port_error_invalid,
)
}
}
private fun ValidateUsernameError.toUsernameErrorString(resources: Resources): String {
return when (this) {
ValidateUsernameError.EmptyUsername -> resources.getString(
R.string.account_setup_outgoing_config_username_error_required,
)
}
}
private fun ValidatePasswordError.toPasswordErrorString(resources: Resources): String {
return when (this) {
ValidatePasswordError.EmptyPassword -> resources.getString(
R.string.account_setup_outgoing_config_password_error_required,
)
}
}

View file

@ -11,18 +11,23 @@
<string name="account_setup_outgoing_config_top_bar_title">Outgoing server settings</string>
<string name="account_setup_outgoing_config_server_label">Server</string>
<string name="account_setup_outgoing_config_server_error_required">Server name is required.</string>
<string name="account_setup_outgoing_config_security_label">Security</string>
<string name="account_setup_outgoing_config_security_none">None</string>
<string name="account_setup_outgoing_config_security_ssl">SSL/TLS</string>
<string name="account_setup_outgoing_config_security_start_tls">StartTLS</string>
<string name="account_setup_outgoing_config_port_label">Port</string>
<string name="account_setup_outgoing_config_port_error_required">Port is required.</string>
<string name="account_setup_outgoing_config_port_error_invalid">Port is invalid (must be 165535).</string>
<string name="account_setup_outgoing_config_username_label">Username</string>
<string name="account_setup_outgoing_config_username_error_required">Username is required.</string>
<string name="account_setup_outgoing_config_password_error_required">Password is required.</string>
<string name="account_setup_outgoing_config_client_certificate_label">Client certificate</string>
<string name="account_setup_outgoing_config_client_certificate_none_available">None available</string>
<string name="account_setup_outgoing_config_imap_namespace_label">Auto-detect IMAP namespace</string>
<string name="account_setup_outgoing_config_compression_label">Use compression</string>
<string name="account_setup_options_top_bar_title">Account options</string>
<string name="account_setup_options_top_bar_title">Account options</string>
<string name="account_setup_options_section_display_options">Display options</string>
<string name="account_setup_options_account_name_label">Account name</string>
<string name="account_setup_options_account_name_error_blank">Account name can\'t be blank.</string>

View file

@ -0,0 +1,41 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import org.junit.Test
class ValidatePasswordTest {
@Test
fun `should succeed when password is set`() {
val useCase = ValidatePassword()
val result = useCase.execute("password")
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
fun `should fail when password is empty`() {
val useCase = ValidatePassword()
val result = useCase.execute("")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidatePassword.ValidatePasswordError.EmptyPassword::class)
}
@Test
fun `should fail when password is blank`() {
val useCase = ValidatePassword()
val result = useCase.execute(" ")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidatePassword.ValidatePasswordError.EmptyPassword::class)
}
}

View file

@ -0,0 +1,64 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidatePort.ValidatePortError
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import org.junit.Test
class ValidatePortTest {
@Test
fun `should succeed when port is set`() {
val useCase = ValidatePort()
val result = useCase.execute(123L)
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
fun `should fail when port is negative`() {
val useCase = ValidatePort()
val result = useCase.execute(-1L)
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidatePortError.InvalidPort::class)
}
@Test
fun `should fail when port is zero`() {
val useCase = ValidatePort()
val result = useCase.execute(0)
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidatePortError.InvalidPort::class)
}
@Test
fun `should fail when port exceeds maximum`() {
val useCase = ValidatePort()
val result = useCase.execute(65536L)
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidatePortError.InvalidPort::class)
}
@Test
fun `should fail when port is null`() {
val useCase = ValidatePort()
val result = useCase.execute(null)
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidatePortError.EmptyPort::class)
}
}

View file

@ -0,0 +1,41 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import org.junit.Test
class ValidateServerTest {
@Test
fun `should succeed when server is set`() {
val useCase = ValidateServer()
val result = useCase.execute("server")
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
fun `should fail when server is empty`() {
val useCase = ValidateServer()
val result = useCase.execute("")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateServer.ValidateServerError.EmptyServer::class)
}
@Test
fun `should fail when server is blank`() {
val useCase = ValidateServer()
val result = useCase.execute(" ")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateServer.ValidateServerError.EmptyServer::class)
}
}

View file

@ -0,0 +1,41 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
import org.junit.Test
class ValidateUsernameTest {
@Test
fun `should succeed when username is set`() {
val useCase = ValidateUsername()
val result = useCase.execute("username")
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
fun `should fail when username is empty`() {
val useCase = ValidateUsername()
val result = useCase.execute("")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateUsername.ValidateUsernameError.EmptyUsername::class)
}
@Test
fun `should fail when username is blank`() {
val useCase = ValidateUsername()
val result = useCase.execute(" ")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateUsername.ValidateUsernameError.EmptyUsername::class)
}
}

View file

@ -1,6 +1,8 @@
package app.k9mail.feature.account.setup.ui.outgoing
import app.cash.turbine.testIn
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.ConnectionSecurity
import app.k9mail.feature.account.setup.domain.entity.toSmtpDefaultPort
@ -10,6 +12,7 @@ import app.k9mail.feature.account.setup.testing.eventStateTest
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Effect
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.Event
import app.k9mail.feature.account.setup.ui.outgoing.AccountOutgoingConfigContract.State
import assertk.assertThat
import assertk.assertions.assertThatAndTurbinesConsumed
import assertk.assertions.isEqualTo
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -23,7 +26,9 @@ class AccountOutgoingConfigViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val testSubject = AccountOutgoingConfigViewModel()
private val testSubject = AccountOutgoingConfigViewModel(
validator = FakeAccountOutgoingConfigValidator(),
)
@Test
fun `should change state when ServerChanged event is received`() = runTest {
@ -117,31 +122,77 @@ class AccountOutgoingConfigViewModelTest {
}
@Test
fun `should emit NavigateNext effect when OnNextClicked event is received`() = runTest {
val viewModel = testSubject
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
fun `should change state and emit NavigateNext effect when OnNextClicked event is received and input is valid`() =
runTest {
val viewModel = testSubject
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(State())
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(State())
}
viewModel.event(Event.OnNextClicked)
assertThat(stateTurbine.awaitItem()).isEqualTo(
State(
server = StringInputField(value = "", isValid = true),
port = NumberInputField(value = 465L, isValid = true),
username = StringInputField(value = "", isValid = true),
password = StringInputField(value = "", isValid = true),
),
)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.NavigateNext)
}
}
viewModel.event(Event.OnNextClicked)
assertThatAndTurbinesConsumed(
actual = effectTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(Effect.NavigateNext)
}
}
@Test
fun `should emit NavigateBack effect when OnBackClicked event is received`() = runTest {
fun `should change state and not emit NavigateNext effect when OnNextClicked event received and input invalid`() =
runTest {
val viewModel = AccountOutgoingConfigViewModel(
validator = FakeAccountOutgoingConfigValidator(
serverAnswer = ValidationResult.Failure(TestError),
),
)
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
val turbines = listOf(stateTurbine, effectTurbine)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(State())
}
viewModel.event(Event.OnNextClicked)
assertThatAndTurbinesConsumed(
actual = stateTurbine.awaitItem(),
turbines = turbines,
) {
isEqualTo(
State(
server = StringInputField(value = "", error = TestError, isValid = false),
port = NumberInputField(value = 465L, isValid = true),
username = StringInputField(value = "", isValid = true),
password = StringInputField(value = "", isValid = true),
),
)
}
}
@Test
fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
val viewModel = testSubject
val stateTurbine = viewModel.state.testIn(backgroundScope)
val effectTurbine = viewModel.effect.testIn(backgroundScope)
@ -163,4 +214,6 @@ class AccountOutgoingConfigViewModelTest {
isEqualTo(Effect.NavigateBack)
}
}
private object TestError : ValidationError
}

View file

@ -0,0 +1,15 @@
package app.k9mail.feature.account.setup.ui.outgoing
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
class FakeAccountOutgoingConfigValidator(
private val serverAnswer: ValidationResult = ValidationResult.Success,
private val portAnswer: ValidationResult = ValidationResult.Success,
private val usernameAnswer: ValidationResult = ValidationResult.Success,
private val passwordAnswer: ValidationResult = ValidationResult.Success,
) : AccountOutgoingConfigContract.Validator {
override fun validateServer(server: String): ValidationResult = serverAnswer
override fun validatePort(port: Long?): ValidationResult = portAnswer
override fun validateUsername(username: String): ValidationResult = usernameAnswer
override fun validatePassword(password: String): ValidationResult = passwordAnswer
}