Merge pull request #6952 from thundernest/add_account_setup_outgoing_config_validation
Add account setup outgoing config validation
This commit is contained in:
commit
d547e845d2
18 changed files with 507 additions and 29 deletions
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 1–65535).</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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue