From b2fd3b17835c7e6e89489971cc2d9fe7e47a332c Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Wed, 10 Nov 2021 20:59:54 +0700 Subject: [PATCH 01/12] domain --- .../hoc/flowmvi/data/UserRepositoryImpl.kt | 28 +++++++++---- .../data/mapper/UserDomainToUserBodyMapper.kt | 8 ++-- .../flowmvi/data/mapper/UserErrorMapper.kt | 5 ++- .../mapper/UserResponseToUserDomainMapper.kt | 10 +++-- .../flowmvi/data/UserRepositoryImplTest.kt | 4 +- .../mapper/UserDomainToUserBodyMapperTest.kt | 2 +- .../data/mapper/UserErrorMapperTest.kt | 4 +- .../UserResponseToUserDomainMapperTest.kt | 2 +- .../com/hoc/flowmvi/domain/entity/User.kt | 9 ---- .../com/hoc/flowmvi/domain/model/Email.kt | 11 +++++ .../com/hoc/flowmvi/domain/model/FirstName.kt | 11 +++++ .../com/hoc/flowmvi/domain/model/LastName.kt | 11 +++++ .../java/com/hoc/flowmvi/domain/model/User.kt | 42 ++++++++++++++++--- .../com/hoc/flowmvi/domain/model/UserError.kt | 10 +++++ .../flowmvi/domain/model/ValidationError.kt | 12 ++++++ .../domain/repository/UserRepository.kt | 12 +----- .../flowmvi/domain/usecase/AddUserUseCase.kt | 4 +- .../flowmvi/domain/usecase/GetUsersUseCase.kt | 4 +- .../domain/usecase/RefreshGetUsersUseCase.kt | 2 +- .../domain/usecase/RemoveUserUseCase.kt | 4 +- .../domain/usecase/SearchUsersUseCase.kt | 4 +- .../com/hoc/flowmvi/domain/UseCaseTest.kt | 13 +++--- .../com/hoc/flowmvi/ui/add/AddActivity.kt | 1 + .../com/hoc/flowmvi/ui/add/AddContract.kt | 15 ++----- .../main/java/com/hoc/flowmvi/ui/add/AddVM.kt | 27 ++++++------ .../java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 4 +- .../com/hoc/flowmvi/ui/main/MainActivity.kt | 4 +- .../com/hoc/flowmvi/ui/main/MainContract.kt | 14 +++---- .../java/com/hoc/flowmvi/ui/main/MainVM.kt | 3 +- .../hoc/flowmvi/ui/main/MainContractTest.kt | 2 +- .../com/hoc/flowmvi/ui/main/MainVMTest.kt | 4 +- .../java/com/hoc/flowmvi/ui/main/TestData.kt | 2 +- .../hoc/flowmvi/ui/search/SearchActivity.kt | 4 +- .../hoc/flowmvi/ui/search/SearchContract.kt | 6 +-- 34 files changed, 186 insertions(+), 112 deletions(-) delete mode 100644 domain/src/main/java/com/hoc/flowmvi/domain/entity/User.kt create mode 100644 domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt create mode 100644 domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt create mode 100644 domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt rename feature-add/src/main/java/com/hoc/flowmvi/ui/add/Validators.kt => domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt (51%) create mode 100644 domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt create mode 100644 domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index f2da9786..820d0730 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -1,17 +1,21 @@ package com.hoc.flowmvi.data import arrow.core.Either +import arrow.core.ValidatedNel +import arrow.core.andThen import arrow.core.left import arrow.core.leftWiden import arrow.core.right +import arrow.core.valueOr import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers import com.hoc.flowmvi.core.retrySuspend import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError +import com.hoc.flowmvi.domain.model.ValidationError import com.hoc.flowmvi.domain.repository.UserRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -33,7 +37,7 @@ import kotlin.time.ExperimentalTime internal class UserRepositoryImpl( private val userApiService: UserApiService, private val dispatchers: CoroutineDispatchers, - private val responseToDomain: Mapper, + private val responseToDomain: Mapper>, private val domainToBody: Mapper, private val errorMapper: Mapper, ) : UserRepository { @@ -53,9 +57,11 @@ internal class UserRepositoryImpl( initialDelay = Duration.milliseconds(500), factor = 2.0, shouldRetry = { it is IOException } - ) { - Timber.d("[USER_REPO] Retry times=$it") - userApiService.getUsers().map(responseToDomain) + ) { times -> + Timber.d("[USER_REPO] Retry times=$times") + userApiService + .getUsers() + .map(responseToDomain andThen { it.valueOrThrowUserError() }) } } } @@ -89,7 +95,7 @@ internal class UserRepositoryImpl( override suspend fun remove(user: User) = Either.catch { withContext(dispatchers.io) { val response = userApiService.remove(user.id) - changesFlow.emit(Change.Removed(responseToDomain(response))) + changesFlow.emit(Change.Removed(responseToDomain(response).valueOrThrowUserError())) } }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "remove user=$user") } .mapLeft(errorMapper) @@ -98,7 +104,7 @@ internal class UserRepositoryImpl( withContext(dispatchers.io) { val body = domainToBody(user) val response = userApiService.add(body) - changesFlow.emit(Change.Added(responseToDomain(response))) + changesFlow.emit(Change.Added(responseToDomain(response).valueOrThrowUserError())) extraDelay() } }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "add user=$user") } @@ -107,10 +113,14 @@ internal class UserRepositoryImpl( override suspend fun search(query: String) = Either.catch { withContext(dispatchers.io) { extraDelay() - userApiService.search(query).map(responseToDomain) + userApiService.search(query).map(responseToDomain andThen { it.valueOrThrowUserError() }) } }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "search query=$query") } .mapLeft(errorMapper) private suspend inline fun extraDelay() = delay(400) } + +@Suppress("NOTHING_TO_INLINE") +private inline fun ValidatedNel.valueOrThrowUserError(): A = + valueOr { throw UserError.ValidationFailed(it) } diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt index 85265caf..088b3a50 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt @@ -2,14 +2,14 @@ package com.hoc.flowmvi.data.mapper import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserBody -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User internal class UserDomainToUserBodyMapper : Mapper { override fun invoke(domain: User): UserBody { return UserBody( - email = domain.email, - firstName = domain.firstName, - lastName = domain.lastName + email = domain.email.value, + firstName = domain.firstName.value, + lastName = domain.lastName.value ) } } diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt index 033ed08b..e501bb78 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt @@ -3,7 +3,7 @@ package com.hoc.flowmvi.data.mapper import arrow.core.nonFatalOrThrow import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.ErrorResponse -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.UserError import com.squareup.moshi.JsonAdapter import okhttp3.ResponseBody import retrofit2.HttpException @@ -19,6 +19,7 @@ internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter return runCatching { when (throwable) { + is UserError -> throwable is IOException -> when (throwable) { is UnknownHostException -> UserError.NetworkError is SocketTimeoutException -> UserError.NetworkError @@ -47,7 +48,7 @@ internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter "internal-error" -> UserError.ServerError "invalid-id" -> UserError.InvalidId(id = errorResponse.data as String) "user-not-found" -> UserError.UserNotFound(id = errorResponse.data as String) - "validation-failed" -> UserError.ValidationFailed + "validation-failed" -> UserError.ValidationFailed(errors = emptyList()) else -> UserError.Unexpected } } diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt index f8827cbf..42ba275e 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt @@ -1,12 +1,14 @@ package com.hoc.flowmvi.data.mapper +import arrow.core.ValidatedNel import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserResponse -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.ValidationError -internal class UserResponseToUserDomainMapper : Mapper { - override fun invoke(response: UserResponse): User { - return User( +internal class UserResponseToUserDomainMapper : Mapper> { + override fun invoke(response: UserResponse): ValidatedNel { + return User.create( id = response.id, avatar = response.avatar, email = response.email, diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 18541457..8418114c 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -7,8 +7,8 @@ import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import com.hoc.flowmvi.test_utils.TestDispatchers import io.mockk.clearAllMocks diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt index 3ecea14a..5d2059db 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.data.mapper import com.hoc.flowmvi.data.remote.UserBody -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User import kotlin.test.Test import kotlin.test.assertEquals diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt index e2ac0c1e..273c09f5 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.data.mapper import com.hoc.flowmvi.data.remote.ErrorResponse -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.UserError import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -145,7 +145,7 @@ class UserErrorMapperTest { errorMapper(buildHttpException("user-not-found", id)), ) assertEquals( - UserError.ValidationFailed, + UserError.ValidationFailed(emptyList()), errorMapper(buildHttpException("validation-failed", null)), ) } diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt index 718bb8c9..bbc2922e 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.data.mapper import com.hoc.flowmvi.data.remote.UserResponse -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User import kotlin.test.Test import kotlin.test.assertEquals diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/entity/User.kt b/domain/src/main/java/com/hoc/flowmvi/domain/entity/User.kt deleted file mode 100644 index 63d0b6be..00000000 --- a/domain/src/main/java/com/hoc/flowmvi/domain/entity/User.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.hoc.flowmvi.domain.entity - -data class User( - val id: String, - val email: String, - val firstName: String, - val lastName: String, - val avatar: String -) diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt new file mode 100644 index 00000000..9a871560 --- /dev/null +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt @@ -0,0 +1,11 @@ +package com.hoc.flowmvi.domain.model + +import arrow.core.ValidatedNel + +@JvmInline +value class Email private constructor(val value: String) { + companion object { + fun create(value: String?): ValidatedNel = + validateEmail(value).map(::Email) + } +} diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt new file mode 100644 index 00000000..e0f2578a --- /dev/null +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt @@ -0,0 +1,11 @@ +package com.hoc.flowmvi.domain.model + +import arrow.core.ValidatedNel + +@JvmInline +value class FirstName private constructor(val value: String) { + companion object { + fun create(value: String?): ValidatedNel = + validateFirstName(value).map(::FirstName) + } +} diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt new file mode 100644 index 00000000..bc86b9a4 --- /dev/null +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt @@ -0,0 +1,11 @@ +package com.hoc.flowmvi.domain.model + +import arrow.core.ValidatedNel + +@JvmInline +value class LastName private constructor(val value: String) { + companion object { + fun create(value: String?): ValidatedNel = + validateLastName(value).map(::LastName) + } +} diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/Validators.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt similarity index 51% rename from feature-add/src/main/java/com/hoc/flowmvi/ui/add/Validators.kt rename to domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt index 99b3e528..12efc210 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/Validators.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt @@ -1,11 +1,38 @@ -package com.hoc.flowmvi.ui.add +package com.hoc.flowmvi.domain.model -import androidx.core.util.PatternsCompat import arrow.core.ValidatedNel import arrow.core.validNel +import arrow.core.zip -private const val MIN_LENGTH_FIRST_NAME = 3 -private const val MIN_LENGTH_LAST_NAME = 3 +data class User( + val id: String, + val email: Email, + val firstName: FirstName, + val lastName: LastName, + val avatar: String, +) { + companion object { + fun create( + id: String, + email: String?, + firstName: String?, + lastName: String?, + avatar: String, + ): ValidatedNel = Email.create(email) + .zip( + FirstName.create(firstName), + LastName.create(lastName), + ) { e, f, l -> + User( + firstName = f, + email = e, + lastName = l, + id = id, + avatar = avatar + ) + } + } +} internal fun validateFirstName(firstName: String?): ValidatedNel { if (firstName == null || firstName.length < MIN_LENGTH_FIRST_NAME) { @@ -24,9 +51,14 @@ internal fun validateLastName(lastName: String?): ValidatedNel { - if (email == null || !PatternsCompat.EMAIL_ADDRESS.matcher(email).matches()) { + if (email == null || !EMAIL_ADDRESS.matches(email)) { return ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel } // more validations here return email.validNel() } + +private const val MIN_LENGTH_FIRST_NAME = 3 +private const val MIN_LENGTH_LAST_NAME = 3 +private val EMAIL_ADDRESS = + Regex("""[a-zA-Z0-9+._%\-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\-]{0,64}(\.[a-zA-Z0-9][a-zA-Z0-9\-]{0,25})+""") diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt new file mode 100644 index 00000000..dbf346c2 --- /dev/null +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt @@ -0,0 +1,10 @@ +package com.hoc.flowmvi.domain.model + +sealed class UserError : Throwable() { + object NetworkError : UserError() + data class UserNotFound(val id: String) : UserError() + data class InvalidId(val id: String) : UserError() + data class ValidationFailed(val errors: List) : UserError() + object ServerError : UserError() + object Unexpected : UserError() +} diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt new file mode 100644 index 00000000..3e15a344 --- /dev/null +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt @@ -0,0 +1,12 @@ +package com.hoc.flowmvi.domain.model + +import arrow.core.ValidatedNel +import arrow.core.invalidNel + +enum class ValidationError { + INVALID_EMAIL_ADDRESS, + TOO_SHORT_FIRST_NAME, + TOO_SHORT_LAST_NAME; + + val asInvalidNel: ValidatedNel = invalidNel() +} diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt b/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt index 9117af40..acb9843e 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/repository/UserRepository.kt @@ -1,18 +1,10 @@ package com.hoc.flowmvi.domain.repository import arrow.core.Either -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import kotlinx.coroutines.flow.Flow -sealed interface UserError { - object NetworkError : UserError - data class UserNotFound(val id: String) : UserError - data class InvalidId(val id: String) : UserError - object ValidationFailed : UserError - object ServerError : UserError - object Unexpected : UserError -} - interface UserRepository { fun getUsers(): Flow>> diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt index d04acce1..59eeb0e6 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt @@ -1,8 +1,8 @@ package com.hoc.flowmvi.domain.usecase import arrow.core.Either -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository class AddUserUseCase(private val userRepository: UserRepository) { diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt index 53b59862..8fdb9de9 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt @@ -1,8 +1,8 @@ package com.hoc.flowmvi.domain.usecase import arrow.core.Either -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository import kotlinx.coroutines.flow.Flow diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt index f67cee4c..ae6e17a5 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.domain.usecase import arrow.core.Either -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository class RefreshGetUsersUseCase(private val userRepository: UserRepository) { diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt index 72aae8c1..fce13af0 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt @@ -1,8 +1,8 @@ package com.hoc.flowmvi.domain.usecase import arrow.core.Either -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository class RemoveUserUseCase(private val userRepository: UserRepository) { diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt index 60858a7c..fd6713c0 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt @@ -1,8 +1,8 @@ package com.hoc.flowmvi.domain.usecase import arrow.core.Either -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository class SearchUsersUseCase(private val userRepository: UserRepository) { diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt index d35c840e..2efe086d 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt @@ -2,8 +2,9 @@ package com.hoc.flowmvi.domain import arrow.core.left import arrow.core.right -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import arrow.core.valueOr +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository import com.hoc.flowmvi.domain.usecase.AddUserUseCase import com.hoc.flowmvi.domain.usecase.GetUsersUseCase @@ -29,28 +30,28 @@ import kotlin.test.Test import kotlin.test.assertEquals private val USERS = listOf( - User( + User.create( id = "1", email = "email1@gmail.com", firstName = "first1", lastName = "last1", avatar = "1.png" ), - User( + User.create( id = "2", email = "email1@gmail.com", firstName = "first2", lastName = "last2", avatar = "2.png" ), - User( + User.create( id = "3", email = "email1@gmail.com", firstName = "first3", lastName = "last3", avatar = "3.png" ), -) +).map { it.valueOr { error("Invalid user $it") } } @ExperimentalCoroutinesApi class UseCaseTest { diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt index 79406d72..b1b912e9 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt @@ -12,6 +12,7 @@ import com.hoc.flowmvi.core_ui.firstChange import com.hoc.flowmvi.core_ui.navigator.IntentProviders import com.hoc.flowmvi.core_ui.textChanges import com.hoc.flowmvi.core_ui.toast +import com.hoc.flowmvi.domain.model.ValidationError import com.hoc.flowmvi.mvi_base.AbstractMviActivity import com.hoc.flowmvi.ui.add.databinding.ActivityAddBinding import com.hoc081098.flowext.mapTo diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt index 6b18b8a3..d45630de 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt @@ -1,23 +1,14 @@ package com.hoc.flowmvi.ui.add import android.os.Parcelable -import arrow.core.ValidatedNel -import arrow.core.invalidNel -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError +import com.hoc.flowmvi.domain.model.ValidationError import com.hoc.flowmvi.mvi_base.MviIntent import com.hoc.flowmvi.mvi_base.MviSingleEvent import com.hoc.flowmvi.mvi_base.MviViewState import kotlinx.parcelize.Parcelize -enum class ValidationError { - INVALID_EMAIL_ADDRESS, - TOO_SHORT_FIRST_NAME, - TOO_SHORT_LAST_NAME; - - val asInvalidNel: ValidatedNel = invalidNel() -} - @Parcelize data class ViewState( val errors: Set, diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt index 2e995900..e0edd2fe 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt @@ -3,8 +3,7 @@ package com.hoc.flowmvi.ui.add import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import arrow.core.orNull -import arrow.core.zip -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.usecase.AddUserUseCase import com.hoc.flowmvi.mvi_base.AbstractMviViewModel import com.hoc081098.flowext.flatMapFirst @@ -93,19 +92,17 @@ class AddVM( .shareWhileSubscribed() val userFormFlow = combine( - emailFlow.map { validateEmail(it) }.distinctUntilChanged(), - firstNameFlow.map { validateFirstName(it) }.distinctUntilChanged(), - lastNameFlow.map { validateLastName(it) }.distinctUntilChanged(), - ) { emailValidated, firstNameValidated, lastNameValidated -> - emailValidated.zip(firstNameValidated, lastNameValidated) { email, firstName, lastName -> - User( - firstName = firstName, - email = email, - lastName = lastName, - id = "", - avatar = "" - ) - } + emailFlow, + firstNameFlow, + lastNameFlow, + ) { email, firstName, lastName -> + User.create( + email = email, + firstName = firstName, + lastName = lastName, + id = "", + avatar = "", + ) }.stateWithInitialNullWhileSubscribed() val addUserChanges = filterIsInstance() diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index df4a76c2..d6b4dea9 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -5,8 +5,8 @@ import arrow.core.left import arrow.core.right import com.flowmvi.mvi_testing.BaseMviViewModelTest import com.flowmvi.mvi_testing.mapRight -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.usecase.AddUserUseCase import com.hoc.flowmvi.ui.add.ValidationError.TOO_SHORT_FIRST_NAME import com.hoc.flowmvi.ui.add.ValidationError.TOO_SHORT_LAST_NAME diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt index 18e99ffa..e4560a04 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt @@ -12,7 +12,7 @@ import com.hoc.flowmvi.core_ui.clicks import com.hoc.flowmvi.core_ui.navigator.Navigator import com.hoc.flowmvi.core_ui.refreshes import com.hoc.flowmvi.core_ui.toast -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.mvi_base.AbstractMviActivity import com.hoc.flowmvi.ui.main.databinding.ActivityMainBinding import com.hoc081098.viewbindingdelegate.viewBinding @@ -108,7 +108,7 @@ class MainActivity : UserError.ServerError -> "Server error" UserError.Unexpected -> "Unexpected error" is UserError.UserNotFound -> "User not found" - UserError.ValidationFailed -> "Validation failed" + is UserError.ValidationFailed -> "Validation failed" } } diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt index b82e4f21..3be59356 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.ui.main -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.mvi_base.MviIntent import com.hoc.flowmvi.mvi_base.MviSingleEvent import com.hoc.flowmvi.mvi_base.MviViewState @@ -17,19 +17,19 @@ data class UserItem( constructor(domain: User) : this( id = domain.id, - email = domain.email, + email = domain.email.value, avatar = domain.avatar, - firstName = domain.firstName, - lastName = domain.lastName + firstName = domain.firstName.value, + lastName = domain.lastName.value ) - fun toDomain() = User( + fun toDomain() = User.create( id = id, lastName = lastName, firstName = firstName, avatar = avatar, email = email - ) + ).toEither().mapLeft(UserError::ValidationFailed) } sealed interface ViewIntent : MviIntent { diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt index c27f48a0..41be5fa9 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt @@ -1,6 +1,7 @@ package com.hoc.flowmvi.ui.main import androidx.lifecycle.viewModelScope +import arrow.core.flatMap import com.hoc.flowmvi.domain.usecase.GetUsersUseCase import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase @@ -123,7 +124,7 @@ class MainVM( flow { userItem .toDomain() - .let { removeUser(it) } + .flatMap { removeUser(it) } .let { emit(it) } } .map { result -> diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt index c01dfba5..a0d817b0 100644 --- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt +++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt @@ -1,6 +1,6 @@ package com.hoc.flowmvi.ui.main -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User import kotlin.test.Test import kotlin.test.assertEquals diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt index c77e9bde..856edc7a 100644 --- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt +++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt @@ -4,8 +4,8 @@ import arrow.core.left import arrow.core.right import com.flowmvi.mvi_testing.BaseMviViewModelTest import com.flowmvi.mvi_testing.mapRight -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.usecase.GetUsersUseCase import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt index 5d473401..078ca36d 100644 --- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt +++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt @@ -1,6 +1,6 @@ package com.hoc.flowmvi.ui.main -import com.hoc.flowmvi.domain.entity.User +import com.hoc.flowmvi.domain.model.User internal val USERS = listOf( User( diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt index 8524cd48..a5559176 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt @@ -15,7 +15,7 @@ import com.hoc.flowmvi.core_ui.clicks import com.hoc.flowmvi.core_ui.navigator.IntentProviders import com.hoc.flowmvi.core_ui.queryTextEvents import com.hoc.flowmvi.core_ui.toast -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.mvi_base.AbstractMviActivity import com.hoc.flowmvi.ui.search.databinding.ActivitySearchBinding import com.hoc081098.viewbindingdelegate.viewBinding @@ -67,7 +67,7 @@ class SearchActivity : UserError.ServerError -> "Server error" UserError.Unexpected -> "Unexpected error" is UserError.UserNotFound -> "User not found" - UserError.ValidationFailed -> "Validation failed" + is UserError.ValidationFailed -> "Validation failed" } } } diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt index d970eb30..48c01a30 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.ui.search -import com.hoc.flowmvi.domain.entity.User -import com.hoc.flowmvi.domain.repository.UserError +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.mvi_base.MviIntent import com.hoc.flowmvi.mvi_base.MviSingleEvent import com.hoc.flowmvi.mvi_base.MviViewState @@ -19,7 +19,7 @@ data class UserItem private constructor( fun from(domain: User): UserItem { return UserItem( id = domain.id, - email = domain.email, + email = domain.email.value, avatar = domain.avatar, fullName = "${domain.firstName} ${domain.lastName}", ) From fcbdd890f15c9b1e51c8a3642936f4af462c90ca Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Thu, 11 Nov 2021 20:57:04 +0700 Subject: [PATCH 02/12] fix tests --- .../flowmvi/data/UserRepositoryImplTest.kt | 29 ++-- .../mapper/UserDomainToUserBodyMapperTest.kt | 5 +- .../data/mapper/UserErrorMapperTest.kt | 13 ++ .../UserResponseToUserDomainMapperTest.kt | 35 +++- .../domain/Email_FirstName_LastName_Test.kt | 105 ++++++++++++ .../com/hoc/flowmvi/domain/UseCaseTest.kt | 2 +- .../java/com/hoc/flowmvi/domain/UserTest.kt | 162 ++++++++++++++++++ .../java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 17 +- .../com/hoc/flowmvi/ui/add/ValidatorsTest.kt | 93 ---------- .../hoc/flowmvi/ui/main/MainContractTest.kt | 35 ++-- .../java/com/hoc/flowmvi/ui/main/TestData.kt | 9 +- 11 files changed, 363 insertions(+), 142 deletions(-) create mode 100644 domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt create mode 100644 domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt delete mode 100644 feature-add/src/test/java/com/hoc/flowmvi/ui/add/ValidatorsTest.kt diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 8418114c..604eb548 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -1,14 +1,18 @@ package com.hoc.flowmvi.data import arrow.core.Either +import arrow.core.ValidatedNel import arrow.core.getOrHandle import arrow.core.identity +import arrow.core.validNel +import arrow.core.valueOr import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError +import com.hoc.flowmvi.domain.model.ValidationError import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import com.hoc.flowmvi.test_utils.TestDispatchers import io.mockk.clearAllMocks @@ -69,28 +73,30 @@ private val USER_RESPONSES = listOf( ) private val USERS = listOf( - User( + User.create( id = "1", email = "email1@gmail.com", firstName = "first", lastName = "last", avatar = "avatar1", ), - User( + User.create( id = "2", email = "email2@gmail.com", firstName = "first", lastName = "last", avatar = "avatar2", ), - User( + User.create( id = "3", email = "email3@gmail.com", firstName = "first", lastName = "last", avatar = "avatar3", ), -) +).map { validated -> validated.valueOr { error("$it") } } + +private val VALID_NEL_USERS = USERS.map(User::validNel) @ExperimentalCoroutinesApi @ExperimentalTime @@ -101,7 +107,7 @@ class UserRepositoryImplTest { private lateinit var repo: UserRepositoryImpl private lateinit var userApiService: UserApiService - private lateinit var responseToDomain: Mapper + private lateinit var responseToDomain: Mapper> private lateinit var domainToBody: Mapper private lateinit var errorMapper: Mapper @@ -135,7 +141,7 @@ class UserRepositoryImplTest { @Test fun test_refresh_withApiCallSuccess_returnsRight() = testDispatcher.runBlockingTest { coEvery { userApiService.getUsers() } returns USER_RESPONSES - every { responseToDomain(any()) } returnsMany USERS + every { responseToDomain(any()) } returnsMany VALID_NEL_USERS val result = repo.refresh() @@ -170,7 +176,7 @@ class UserRepositoryImplTest { val userResponse = USER_RESPONSES[0] coEvery { userApiService.remove(user.id) } returns userResponse - every { responseToDomain(userResponse) } returns user + every { responseToDomain(userResponse) } returns user.validNel() val result = repo.remove(user) @@ -202,7 +208,7 @@ class UserRepositoryImplTest { coEvery { userApiService.add(USER_BODY) } returns userResponse every { domainToBody(user) } returns USER_BODY - every { responseToDomain(userResponse) } returns user + every { responseToDomain(userResponse) } returns user.validNel() val result = repo.add(user) @@ -235,7 +241,7 @@ class UserRepositoryImplTest { fun test_search_withApiCallSuccess_returnsRight() = testDispatcher.runBlockingTest { val q = "hoc081098" coEvery { userApiService.search(q) } returns USER_RESPONSES - every { responseToDomain(any()) } returnsMany USERS + every { responseToDomain(any()) } returnsMany VALID_NEL_USERS val result = repo.search(q) @@ -269,7 +275,7 @@ class UserRepositoryImplTest { @Test fun test_getUsers_withApiCallSuccess_emitsInitial() = testDispatcher.runBlockingTest { coEvery { userApiService.getUsers() } returns USER_RESPONSES - every { responseToDomain(any()) } returnsMany USERS + every { responseToDomain(any()) } returnsMany VALID_NEL_USERS val events = mutableListOf>>() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -323,7 +329,8 @@ class UserRepositoryImplTest { coEvery { userApiService.add(USER_BODY) } returns userResponse coEvery { userApiService.remove(user.id) } returns userResponse every { domainToBody(user) } returns USER_BODY - USER_RESPONSES.zip(USERS).forEach { (r, u) -> every { responseToDomain(r) } returns u } + USER_RESPONSES.zip(USERS) + .forEach { (r, u) -> every { responseToDomain(r) } returns u.validNel() } val events = mutableListOf>>() val job = launch(start = CoroutineStart.UNDISPATCHED) { diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt index 5d2059db..13bdf1cd 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt @@ -1,5 +1,6 @@ package com.hoc.flowmvi.data.mapper +import arrow.core.valueOr import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.domain.model.User import kotlin.test.Test @@ -11,13 +12,13 @@ class UserDomainToUserBodyMapperTest { @Test fun test_UserDomainToUserBodyMapper() { val body = mapper( - User( + User.create( id = "id", email = "email@gmail.com", firstName = "first", lastName = "last", avatar = "avatar", - ) + ).valueOr { error("$it") } ) assertEquals( diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt index 273c09f5..b1fea84f 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt @@ -43,6 +43,19 @@ class UserErrorMapperTest { ) ) + @Test + fun test_withUserError_returnsItself() { + assertEquals(UserError.NetworkError, errorMapper(UserError.NetworkError)) + assertEquals(UserError.UserNotFound("1"), errorMapper(UserError.UserNotFound("1"))) + assertEquals(UserError.InvalidId("1"), errorMapper(UserError.InvalidId("1"))) + assertEquals( + UserError.ValidationFailed(emptyList()), + errorMapper(UserError.ValidationFailed(emptyList())), + ) + assertEquals(UserError.ServerError, errorMapper(UserError.ServerError)) + assertEquals(UserError.Unexpected, errorMapper(UserError.Unexpected)) + } + @Test fun test_withFatalError_rethrows() { assertFailsWith { errorMapper(KotlinCancellationException()) } diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt index bbc2922e..3d909f05 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt @@ -1,16 +1,21 @@ package com.hoc.flowmvi.data.mapper +import arrow.core.identity +import arrow.core.orNull import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.ValidationError import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue class UserResponseToUserDomainMapperTest { private val mapper = UserResponseToUserDomainMapper() @Test - fun test_UserDomainToUserResponseMapper() { - val domain = mapper( + fun testUserDomainToUserResponseMapper_withValidResponse_returnsValid() { + val validated = mapper( UserResponse( id = "id", email = "email@gmail.com", @@ -19,16 +24,34 @@ class UserResponseToUserDomainMapperTest { avatar = "avatar", ) ) - + assertTrue(validated.isValid) assertEquals( - User( + User.create( id = "id", email = "email@gmail.com", firstName = "first", lastName = "last", avatar = "avatar", - ), - domain + ).orNull()!!, + validated.orNull()!!, + ) + } + + @Test + fun testUserDomainToUserResponseMapper_withInvalidResponse_returnsInvalid() { + val validated = mapper( + UserResponse( + id = "id", + email = "email@", + firstName = "first", + lastName = "last", + avatar = "avatar", + ) + ) + assertTrue(validated.isInvalid) + assertEquals( + ValidationError.INVALID_EMAIL_ADDRESS, + assertNotNull(validated.fold(fe = ::identity, fa = { null })).head, ) } } diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt b/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt new file mode 100644 index 00000000..44f0fb05 --- /dev/null +++ b/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt @@ -0,0 +1,105 @@ +package com.hoc.flowmvi.ui.add + +import arrow.core.orNull +import com.hoc.flowmvi.domain.model.Email +import com.hoc.flowmvi.domain.model.FirstName +import com.hoc.flowmvi.domain.model.LastName +import com.hoc.flowmvi.domain.model.ValidationError +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@ExperimentalCoroutinesApi +@Suppress("ClassName") +class Email_FirstName_LastName_Test { + @Test + fun testCreateEmail_withValidEmail_returnsValid() { + val validated = Email.create("hoc081098@gmail.com") + assertTrue(validated.isValid) + assertEquals( + "hoc081098@gmail.com", + validated.orNull()?.value, + ) + } + + @Test + fun testCreateEmail_withInvalidEmail_returnsInvalid() { + assertEquals( + ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + Email.create(null), + ) + assertEquals( + ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + Email.create(""), + ) + assertEquals( + ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + Email.create("a"), + ) + assertEquals( + ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + Email.create("a@"), + ) + } + + @Test + fun testCreateFirstName_withValidFirstName_returnsValid() { + val validated = FirstName.create("hoc081098") + assertTrue(validated.isValid) + assertEquals( + "hoc081098", + validated.orNull()?.value, + ) + } + + @Test + fun testCreateFirstName_withInvalidFirstName_returnsInvalid() { + assertEquals( + ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + FirstName.create(null), + ) + assertEquals( + ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + FirstName.create(""), + ) + assertEquals( + ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + FirstName.create("a"), + ) + assertEquals( + ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + FirstName.create("ab"), + ) + } + + @Test + fun testCreateLastName_withValidLastName_returnsValid() { + val validated = LastName.create("hoc081098") + assertTrue(validated.isValid) + assertEquals( + "hoc081098", + validated.orNull()?.value, + ) + } + + @Test + fun testCreateLastName_withInvalidLastName_returnsInvalid() { + assertEquals( + ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + LastName.create(null), + ) + assertEquals( + ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + LastName.create(""), + ) + assertEquals( + ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + LastName.create("a"), + ) + assertEquals( + ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + LastName.create("ab"), + ) + } +} diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt index 2efe086d..5dee5255 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt @@ -51,7 +51,7 @@ private val USERS = listOf( lastName = "last3", avatar = "3.png" ), -).map { it.valueOr { error("Invalid user $it") } } +).map { validated -> validated.valueOr { error("Invalid user $it") } } @ExperimentalCoroutinesApi class UseCaseTest { diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt new file mode 100644 index 00000000..f14da342 --- /dev/null +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt @@ -0,0 +1,162 @@ +package com.hoc.flowmvi.domain + +import arrow.core.identity +import arrow.core.orNull +import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.domain.model.ValidationError +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +private const val ID = "id" +private const val VALID_EMAIL = "hoc081098@gmail.com" +private const val VALID_NAME = "hoc081098" +private const val AVATAR = "avatar" + +class UserTest { + @Test + fun testCreateUser_withValidValues_returnsValid() { + val validated = User.create( + id = ID, + email = VALID_EMAIL, + firstName = VALID_NAME, + lastName = VALID_NAME, + avatar = AVATAR, + ) + assertTrue(validated.isValid) + assertNotNull(validated.orNull()).let { user -> + assertEquals(ID, user.id) + assertEquals(VALID_EMAIL, user.email.value) + assertEquals(VALID_NAME, user.firstName.value) + assertEquals(VALID_NAME, user.lastName.value) + assertEquals(AVATAR, user.avatar) + } + } + + @Test + fun testCreateUser_withInvalidEmail_returnsInvalid() { + val validated = User.create( + id = ID, + email = "invalid email", + firstName = VALID_NAME, + lastName = VALID_NAME, + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals(setOf(ValidationError.INVALID_EMAIL_ADDRESS), errors.toSet()) + } + } + + @Test + fun testCreateUser_withInvalidFirstName_returnsInvalid() { + val validated = User.create( + id = ID, + email = VALID_EMAIL, + firstName = "h", + lastName = VALID_NAME, + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals(setOf(ValidationError.TOO_SHORT_FIRST_NAME), errors.toSet()) + } + } + + @Test + fun testCreateUser_withInvalidLastName_returnsInvalid() { + val validated = User.create( + id = ID, + email = VALID_EMAIL, + firstName = VALID_NAME, + lastName = "h", + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals(setOf(ValidationError.TOO_SHORT_LAST_NAME), errors.toSet()) + } + } + + @Test + fun testCreateUser_withInvalidEmailAndFirstName_returnsInvalid() { + val validated = User.create( + id = ID, + email = "h", + firstName = "h", + lastName = VALID_NAME, + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals( + setOf( + ValidationError.INVALID_EMAIL_ADDRESS, + ValidationError.TOO_SHORT_FIRST_NAME, + ), + errors.toSet() + ) + } + } + + @Test + fun testCreateUser_withInvalidEmailAndLastName_returnsInvalid() { + val validated = User.create( + id = ID, + email = "h", + firstName = VALID_NAME, + lastName = "h", + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals( + setOf( + ValidationError.INVALID_EMAIL_ADDRESS, + ValidationError.TOO_SHORT_LAST_NAME, + ), + errors.toSet() + ) + } + } + + @Test + fun testCreateUser_withInvalidFirstNameAndLastName_returnsInvalid() { + val validated = User.create( + id = ID, + email = VALID_EMAIL, + firstName = "h", + lastName = "h", + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals( + setOf( + ValidationError.TOO_SHORT_FIRST_NAME, + ValidationError.TOO_SHORT_LAST_NAME, + ), + errors.toSet() + ) + } + } + + @Test + fun testCreateUser_withInvalidValues_returnsInvalid() { + val validated = User.create( + id = ID, + email = "h", + firstName = "h", + lastName = "h", + avatar = AVATAR, + ) + assertTrue(validated.isInvalid) + assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> + assertEquals( + ValidationError.values().toSet(), + errors.toSet() + ) + } + } +} diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index d6b4dea9..999dc9f6 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -3,14 +3,15 @@ package com.hoc.flowmvi.ui.add import androidx.lifecycle.SavedStateHandle import arrow.core.left import arrow.core.right +import arrow.core.valueOr import com.flowmvi.mvi_testing.BaseMviViewModelTest import com.flowmvi.mvi_testing.mapRight import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError +import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.ValidationError.TOO_SHORT_FIRST_NAME +import com.hoc.flowmvi.domain.model.ValidationError.TOO_SHORT_LAST_NAME import com.hoc.flowmvi.domain.usecase.AddUserUseCase -import com.hoc.flowmvi.ui.add.ValidationError.TOO_SHORT_FIRST_NAME -import com.hoc.flowmvi.ui.add.ValidationError.TOO_SHORT_LAST_NAME -import com.hoc.flowmvi.ui.add.ValidationError.values import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified @@ -21,7 +22,7 @@ import kotlin.test.Test import kotlin.time.Duration import kotlin.time.ExperimentalTime -private val ALL_ERRORS = values().toSet() +private val ALL_ERRORS = ValidationError.values().toSet() private const val EMAIL = "hoc081098@gmail.com" private const val NAME = "hoc081098" @@ -268,13 +269,13 @@ class AddVMTest : BaseMviViewModelTest validated.valueOr { error("$it") } } internal val USER_ITEMS = USERS.map(::UserItem) From e6b93d41f1c209e79dde7b2bb6d8e4c180a95d78 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Thu, 11 Nov 2021 20:58:21 +0700 Subject: [PATCH 03/12] rename ValidationError to UserValidationError --- .../hoc/flowmvi/data/UserRepositoryImpl.kt | 6 ++--- .../mapper/UserResponseToUserDomainMapper.kt | 6 ++--- .../flowmvi/data/UserRepositoryImplTest.kt | 4 +-- .../UserResponseToUserDomainMapperTest.kt | 4 +-- .../com/hoc/flowmvi/domain/model/Email.kt | 2 +- .../com/hoc/flowmvi/domain/model/FirstName.kt | 2 +- .../com/hoc/flowmvi/domain/model/LastName.kt | 2 +- .../java/com/hoc/flowmvi/domain/model/User.kt | 14 +++++----- .../com/hoc/flowmvi/domain/model/UserError.kt | 2 +- ...idationError.kt => UserValidationError.kt} | 4 +-- .../domain/Email_FirstName_LastName_Test.kt | 26 +++++++++---------- .../java/com/hoc/flowmvi/domain/UserTest.kt | 22 ++++++++-------- .../com/hoc/flowmvi/ui/add/AddActivity.kt | 8 +++--- .../com/hoc/flowmvi/ui/add/AddContract.kt | 6 ++--- .../java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 8 +++--- 15 files changed, 58 insertions(+), 58 deletions(-) rename domain/src/main/java/com/hoc/flowmvi/domain/model/{ValidationError.kt => UserValidationError.kt} (61%) diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index 820d0730..ee801d1b 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -15,7 +15,7 @@ import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.domain.repository.UserRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -37,7 +37,7 @@ import kotlin.time.ExperimentalTime internal class UserRepositoryImpl( private val userApiService: UserApiService, private val dispatchers: CoroutineDispatchers, - private val responseToDomain: Mapper>, + private val responseToDomain: Mapper>, private val domainToBody: Mapper, private val errorMapper: Mapper, ) : UserRepository { @@ -122,5 +122,5 @@ internal class UserRepositoryImpl( } @Suppress("NOTHING_TO_INLINE") -private inline fun ValidatedNel.valueOrThrowUserError(): A = +private inline fun ValidatedNel.valueOrThrowUserError(): A = valueOr { throw UserError.ValidationFailed(it) } diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt index 42ba275e..29ab3857 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt @@ -4,10 +4,10 @@ import arrow.core.ValidatedNel import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError -internal class UserResponseToUserDomainMapper : Mapper> { - override fun invoke(response: UserResponse): ValidatedNel { +internal class UserResponseToUserDomainMapper : Mapper> { + override fun invoke(response: UserResponse): ValidatedNel { return User.create( id = response.id, avatar = response.avatar, diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 604eb548..e8f96f2d 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -12,7 +12,7 @@ import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import com.hoc.flowmvi.test_utils.TestDispatchers import io.mockk.clearAllMocks @@ -107,7 +107,7 @@ class UserRepositoryImplTest { private lateinit var repo: UserRepositoryImpl private lateinit var userApiService: UserApiService - private lateinit var responseToDomain: Mapper> + private lateinit var responseToDomain: Mapper> private lateinit var domainToBody: Mapper private lateinit var errorMapper: Mapper diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt index 3d909f05..f7d97d9f 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt @@ -4,7 +4,7 @@ import arrow.core.identity import arrow.core.orNull import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -50,7 +50,7 @@ class UserResponseToUserDomainMapperTest { ) assertTrue(validated.isInvalid) assertEquals( - ValidationError.INVALID_EMAIL_ADDRESS, + UserValidationError.INVALID_EMAIL_ADDRESS, assertNotNull(validated.fold(fe = ::identity, fa = { null })).head, ) } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt index 9a871560..10733e02 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt @@ -5,7 +5,7 @@ import arrow.core.ValidatedNel @JvmInline value class Email private constructor(val value: String) { companion object { - fun create(value: String?): ValidatedNel = + fun create(value: String?): ValidatedNel = validateEmail(value).map(::Email) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt index e0f2578a..ef8a3bfd 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt @@ -5,7 +5,7 @@ import arrow.core.ValidatedNel @JvmInline value class FirstName private constructor(val value: String) { companion object { - fun create(value: String?): ValidatedNel = + fun create(value: String?): ValidatedNel = validateFirstName(value).map(::FirstName) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt index bc86b9a4..51316b90 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt @@ -5,7 +5,7 @@ import arrow.core.ValidatedNel @JvmInline value class LastName private constructor(val value: String) { companion object { - fun create(value: String?): ValidatedNel = + fun create(value: String?): ValidatedNel = validateLastName(value).map(::LastName) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt index 12efc210..1c51329e 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt @@ -18,7 +18,7 @@ data class User( firstName: String?, lastName: String?, avatar: String, - ): ValidatedNel = Email.create(email) + ): ValidatedNel = Email.create(email) .zip( FirstName.create(firstName), LastName.create(lastName), @@ -34,25 +34,25 @@ data class User( } } -internal fun validateFirstName(firstName: String?): ValidatedNel { +internal fun validateFirstName(firstName: String?): ValidatedNel { if (firstName == null || firstName.length < MIN_LENGTH_FIRST_NAME) { - return ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel + return UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel } // more validations here return firstName.validNel() } -internal fun validateLastName(lastName: String?): ValidatedNel { +internal fun validateLastName(lastName: String?): ValidatedNel { if (lastName == null || lastName.length < MIN_LENGTH_LAST_NAME) { - return ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel + return UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNel } // more validations here return lastName.validNel() } -internal fun validateEmail(email: String?): ValidatedNel { +internal fun validateEmail(email: String?): ValidatedNel { if (email == null || !EMAIL_ADDRESS.matches(email)) { - return ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel + return UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel } // more validations here return email.validNel() diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt index dbf346c2..b9d30177 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt @@ -4,7 +4,7 @@ sealed class UserError : Throwable() { object NetworkError : UserError() data class UserNotFound(val id: String) : UserError() data class InvalidId(val id: String) : UserError() - data class ValidationFailed(val errors: List) : UserError() + data class ValidationFailed(val errors: List) : UserError() object ServerError : UserError() object Unexpected : UserError() } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt similarity index 61% rename from domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt rename to domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt index 3e15a344..b7daa835 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/ValidationError.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt @@ -3,10 +3,10 @@ package com.hoc.flowmvi.domain.model import arrow.core.ValidatedNel import arrow.core.invalidNel -enum class ValidationError { +enum class UserValidationError { INVALID_EMAIL_ADDRESS, TOO_SHORT_FIRST_NAME, TOO_SHORT_LAST_NAME; - val asInvalidNel: ValidatedNel = invalidNel() + val asInvalidNel: ValidatedNel = invalidNel() } diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt b/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt index 44f0fb05..3f1e8219 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt @@ -4,7 +4,7 @@ import arrow.core.orNull import com.hoc.flowmvi.domain.model.Email import com.hoc.flowmvi.domain.model.FirstName import com.hoc.flowmvi.domain.model.LastName -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlin.test.Test import kotlin.test.assertEquals @@ -26,19 +26,19 @@ class Email_FirstName_LastName_Test { @Test fun testCreateEmail_withInvalidEmail_returnsInvalid() { assertEquals( - ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, Email.create(null), ) assertEquals( - ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, Email.create(""), ) assertEquals( - ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, Email.create("a"), ) assertEquals( - ValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, + UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel, Email.create("a@"), ) } @@ -56,19 +56,19 @@ class Email_FirstName_LastName_Test { @Test fun testCreateFirstName_withInvalidFirstName_returnsInvalid() { assertEquals( - ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, FirstName.create(null), ) assertEquals( - ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, FirstName.create(""), ) assertEquals( - ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, FirstName.create("a"), ) assertEquals( - ValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNel, FirstName.create("ab"), ) } @@ -86,19 +86,19 @@ class Email_FirstName_LastName_Test { @Test fun testCreateLastName_withInvalidLastName_returnsInvalid() { assertEquals( - ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, LastName.create(null), ) assertEquals( - ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, LastName.create(""), ) assertEquals( - ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, LastName.create("a"), ) assertEquals( - ValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, + UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNel, LastName.create("ab"), ) } diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt index f14da342..222c6e6a 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt @@ -3,7 +3,7 @@ package com.hoc.flowmvi.domain import arrow.core.identity import arrow.core.orNull import com.hoc.flowmvi.domain.model.User -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -45,7 +45,7 @@ class UserTest { ) assertTrue(validated.isInvalid) assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals(setOf(ValidationError.INVALID_EMAIL_ADDRESS), errors.toSet()) + assertEquals(setOf(UserValidationError.INVALID_EMAIL_ADDRESS), errors.toSet()) } } @@ -60,7 +60,7 @@ class UserTest { ) assertTrue(validated.isInvalid) assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals(setOf(ValidationError.TOO_SHORT_FIRST_NAME), errors.toSet()) + assertEquals(setOf(UserValidationError.TOO_SHORT_FIRST_NAME), errors.toSet()) } } @@ -75,7 +75,7 @@ class UserTest { ) assertTrue(validated.isInvalid) assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals(setOf(ValidationError.TOO_SHORT_LAST_NAME), errors.toSet()) + assertEquals(setOf(UserValidationError.TOO_SHORT_LAST_NAME), errors.toSet()) } } @@ -92,8 +92,8 @@ class UserTest { assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> assertEquals( setOf( - ValidationError.INVALID_EMAIL_ADDRESS, - ValidationError.TOO_SHORT_FIRST_NAME, + UserValidationError.INVALID_EMAIL_ADDRESS, + UserValidationError.TOO_SHORT_FIRST_NAME, ), errors.toSet() ) @@ -113,8 +113,8 @@ class UserTest { assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> assertEquals( setOf( - ValidationError.INVALID_EMAIL_ADDRESS, - ValidationError.TOO_SHORT_LAST_NAME, + UserValidationError.INVALID_EMAIL_ADDRESS, + UserValidationError.TOO_SHORT_LAST_NAME, ), errors.toSet() ) @@ -134,8 +134,8 @@ class UserTest { assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> assertEquals( setOf( - ValidationError.TOO_SHORT_FIRST_NAME, - ValidationError.TOO_SHORT_LAST_NAME, + UserValidationError.TOO_SHORT_FIRST_NAME, + UserValidationError.TOO_SHORT_LAST_NAME, ), errors.toSet() ) @@ -154,7 +154,7 @@ class UserTest { assertTrue(validated.isInvalid) assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> assertEquals( - ValidationError.values().toSet(), + UserValidationError.values().toSet(), errors.toSet() ) } diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt index b1b912e9..812e5b66 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt @@ -12,7 +12,7 @@ import com.hoc.flowmvi.core_ui.firstChange import com.hoc.flowmvi.core_ui.navigator.IntentProviders import com.hoc.flowmvi.core_ui.textChanges import com.hoc.flowmvi.core_ui.toast -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.mvi_base.AbstractMviActivity import com.hoc.flowmvi.ui.add.databinding.ActivityAddBinding import com.hoc081098.flowext.mapTo @@ -55,15 +55,15 @@ class AddActivity : Timber.d("viewState=$viewState") addBinding.emailEditText.setErrorIfChanged(viewState.emailChanged) { - if (ValidationError.INVALID_EMAIL_ADDRESS in viewState.errors) "Invalid email" + if (UserValidationError.INVALID_EMAIL_ADDRESS in viewState.errors) "Invalid email" else null } addBinding.firstNameEditText.setErrorIfChanged(viewState.firstNameChanged) { - if (ValidationError.TOO_SHORT_FIRST_NAME in viewState.errors) "Too short first name" + if (UserValidationError.TOO_SHORT_FIRST_NAME in viewState.errors) "Too short first name" else null } addBinding.lastNameEditText.setErrorIfChanged(viewState.lastNameChanged) { - if (ValidationError.TOO_SHORT_LAST_NAME in viewState.errors) "Too short last name" + if (UserValidationError.TOO_SHORT_LAST_NAME in viewState.errors) "Too short last name" else null } diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt index d45630de..4aee6710 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt @@ -3,7 +3,7 @@ package com.hoc.flowmvi.ui.add import android.os.Parcelable import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError -import com.hoc.flowmvi.domain.model.ValidationError +import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.mvi_base.MviIntent import com.hoc.flowmvi.mvi_base.MviSingleEvent import com.hoc.flowmvi.mvi_base.MviViewState @@ -11,7 +11,7 @@ import kotlinx.parcelize.Parcelize @Parcelize data class ViewState( - val errors: Set, + val errors: Set, val isLoading: Boolean, // show error or not val emailChanged: Boolean, @@ -51,7 +51,7 @@ sealed interface ViewIntent : MviIntent { internal sealed interface PartialStateChange { fun reduce(viewState: ViewState): ViewState - data class ErrorsChanged(val errors: Set) : PartialStateChange { + data class ErrorsChanged(val errors: Set) : PartialStateChange { override fun reduce(viewState: ViewState) = if (viewState.errors == errors) viewState else viewState.copy(errors = errors) } diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index 999dc9f6..c7271454 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -8,9 +8,9 @@ import com.flowmvi.mvi_testing.BaseMviViewModelTest import com.flowmvi.mvi_testing.mapRight import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError -import com.hoc.flowmvi.domain.model.ValidationError -import com.hoc.flowmvi.domain.model.ValidationError.TOO_SHORT_FIRST_NAME -import com.hoc.flowmvi.domain.model.ValidationError.TOO_SHORT_LAST_NAME +import com.hoc.flowmvi.domain.model.UserValidationError +import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_FIRST_NAME +import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_LAST_NAME import com.hoc.flowmvi.domain.usecase.AddUserUseCase import io.mockk.coEvery import io.mockk.coVerify @@ -22,7 +22,7 @@ import kotlin.test.Test import kotlin.time.Duration import kotlin.time.ExperimentalTime -private val ALL_ERRORS = ValidationError.values().toSet() +private val ALL_ERRORS = UserValidationError.values().toSet() private const val EMAIL = "hoc081098@gmail.com" private const val NAME = "hoc081098" From dff7fa5b5332b360d3d656e32d34e0b25fc3b1fd Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Thu, 11 Nov 2021 21:43:19 +0700 Subject: [PATCH 04/12] Validated.valueOrThrow: A --- .../com/hoc/flowmvi/data/UserRepositoryImplTest.kt | 4 ++-- .../data/mapper/UserDomainToUserBodyMapperTest.kt | 4 ++-- .../test/java/com/hoc/flowmvi/domain/UseCaseTest.kt | 4 ++-- .../test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 6 +++--- .../test/java/com/hoc/flowmvi/ui/main/TestData.kt | 4 ++-- test-utils/build.gradle.kts | 1 + .../main/java/com/hoc/flowmvi/test_utils/utils.kt | 12 ++++++++++++ 7 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index e8f96f2d..e04ea4b8 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -5,7 +5,6 @@ import arrow.core.ValidatedNel import arrow.core.getOrHandle import arrow.core.identity import arrow.core.validNel -import arrow.core.valueOr import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody @@ -15,6 +14,7 @@ import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import com.hoc.flowmvi.test_utils.TestDispatchers +import com.hoc.flowmvi.test_utils.valueOrThrow import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify @@ -94,7 +94,7 @@ private val USERS = listOf( lastName = "last", avatar = "avatar3", ), -).map { validated -> validated.valueOr { error("$it") } } +).map { it.valueOrThrow } private val VALID_NEL_USERS = USERS.map(User::validNel) diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt index 13bdf1cd..c0c9a20c 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt @@ -1,8 +1,8 @@ package com.hoc.flowmvi.data.mapper -import arrow.core.valueOr import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.test_utils.valueOrThrow import kotlin.test.Test import kotlin.test.assertEquals @@ -18,7 +18,7 @@ class UserDomainToUserBodyMapperTest { firstName = "first", lastName = "last", avatar = "avatar", - ).valueOr { error("$it") } + ).valueOrThrow ) assertEquals( diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt index 5dee5255..9082db08 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt @@ -2,7 +2,6 @@ package com.hoc.flowmvi.domain import arrow.core.left import arrow.core.right -import arrow.core.valueOr import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.repository.UserRepository @@ -12,6 +11,7 @@ import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule +import com.hoc.flowmvi.test_utils.valueOrThrow import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify @@ -51,7 +51,7 @@ private val USERS = listOf( lastName = "last3", avatar = "3.png" ), -).map { validated -> validated.valueOr { error("Invalid user $it") } } +).map { it.valueOrThrow } @ExperimentalCoroutinesApi class UseCaseTest { diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index c7271454..0966642e 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -3,7 +3,6 @@ package com.hoc.flowmvi.ui.add import androidx.lifecycle.SavedStateHandle import arrow.core.left import arrow.core.right -import arrow.core.valueOr import com.flowmvi.mvi_testing.BaseMviViewModelTest import com.flowmvi.mvi_testing.mapRight import com.hoc.flowmvi.domain.model.User @@ -12,6 +11,7 @@ import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_FIRST_NAME import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_LAST_NAME import com.hoc.flowmvi.domain.usecase.AddUserUseCase +import com.hoc.flowmvi.test_utils.valueOrThrow import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified @@ -275,7 +275,7 @@ class AddVMTest : BaseMviViewModelTest validated.valueOr { error("$it") } } +).map { it.valueOrThrow } internal val USER_ITEMS = USERS.map(::UserItem) diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 3fd801bc..e09f4e86 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -10,6 +10,7 @@ java { dependencies { implementation(deps.coroutines.core) implementation(core) + api(deps.arrow.core) addUnitTest(testImplementation = false) } diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt new file mode 100644 index 00000000..8a92ed7f --- /dev/null +++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt @@ -0,0 +1,12 @@ +package com.hoc.flowmvi.test_utils + +import arrow.core.Validated +import arrow.core.valueOr + +inline val Validated.valueOrThrow: A + get() { + return valueOr { + if (it is Throwable) throw it + else error("$this - $it - Should not reach here!") + } + } From 63ab1ab0badd33687b973e8208bc4581f132d2de Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Thu, 11 Nov 2021 22:18:22 +0700 Subject: [PATCH 05/12] up --- .editorconfig | 2 +- build.gradle.kts | 4 +- buildSrc/src/main/kotlin/deps.kt | 2 +- .../flowmvi/data/UserRepositoryImplTest.kt | 13 +--- .../UserResponseToUserDomainMapperTest.kt | 11 ++- .../java/com/hoc/flowmvi/domain/UserTest.kt | 77 ++++++++----------- .../hoc/flowmvi/ui/main/MainContractTest.kt | 9 ++- .../java/com/hoc/flowmvi/test_utils/utils.kt | 24 ++++-- 8 files changed, 65 insertions(+), 77 deletions(-) diff --git a/.editorconfig b/.editorconfig index da044eaa..129c9dcd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,4 +8,4 @@ trim_trailing_whitespace=true insert_final_newline=true [*.{kt,kts}] -kotlin_imports_layout=ascii +ij_kotlin_imports_layout=* diff --git a/build.gradle.kts b/build.gradle.kts index 8dbd5d00..5a0527ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ subprojects { // TODO this should all come from editorconfig https://github.com/diffplug/spotless/issues/142 mapOf( "indent_size" to "2", - "kotlin_imports_layout" to "ascii" + "ij_kotlin_imports_layout" to "*" ) ) @@ -56,7 +56,7 @@ subprojects { ktlint(ktlintVersion).userData( mapOf( "indent_size" to "2", - "kotlin_imports_layout" to "ascii" + "ij_kotlin_imports_layout" to "*" ) ) diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt index 690412f8..6d3a5879 100644 --- a/buildSrc/src/main/kotlin/deps.kt +++ b/buildSrc/src/main/kotlin/deps.kt @@ -6,7 +6,7 @@ import org.gradle.kotlin.dsl.project import org.gradle.plugin.use.PluginDependenciesSpec import org.gradle.plugin.use.PluginDependencySpec -const val ktlintVersion = "0.41.0" +const val ktlintVersion = "0.43.0" const val kotlinVersion = "1.5.31" object appConfig { diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index e04ea4b8..83376dfd 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -2,8 +2,6 @@ package com.hoc.flowmvi.data import arrow.core.Either import arrow.core.ValidatedNel -import arrow.core.getOrHandle -import arrow.core.identity import arrow.core.validNel import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.UserApiService @@ -14,6 +12,8 @@ import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import com.hoc.flowmvi.test_utils.TestDispatchers +import com.hoc.flowmvi.test_utils.getOrThrow +import com.hoc.flowmvi.test_utils.leftOrThrow import com.hoc.flowmvi.test_utils.valueOrThrow import io.mockk.clearAllMocks import io.mockk.coEvery @@ -360,12 +360,3 @@ class UserRepositoryImplTest { } } } - -private inline val Either.leftOrThrow: L - get() = fold(::identity) { - if (it is Throwable) throw it - else error("$this - $it - Should not reach here!") - } - -private inline val Either.getOrThrow: R - get() = getOrHandle { error("$this - $it - Should not reach here!") } diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt index f7d97d9f..acdcae2f 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt @@ -1,13 +1,12 @@ package com.hoc.flowmvi.data.mapper -import arrow.core.identity -import arrow.core.orNull import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserValidationError +import com.hoc.flowmvi.test_utils.invalidValueOrThrow +import com.hoc.flowmvi.test_utils.valueOrThrow import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull import kotlin.test.assertTrue class UserResponseToUserDomainMapperTest { @@ -32,8 +31,8 @@ class UserResponseToUserDomainMapperTest { firstName = "first", lastName = "last", avatar = "avatar", - ).orNull()!!, - validated.orNull()!!, + ).valueOrThrow, + validated.valueOrThrow, ) } @@ -51,7 +50,7 @@ class UserResponseToUserDomainMapperTest { assertTrue(validated.isInvalid) assertEquals( UserValidationError.INVALID_EMAIL_ADDRESS, - assertNotNull(validated.fold(fe = ::identity, fa = { null })).head, + validated.invalidValueOrThrow.head, ) } } diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt index 222c6e6a..79d952e9 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt @@ -1,12 +1,11 @@ package com.hoc.flowmvi.domain -import arrow.core.identity -import arrow.core.orNull import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserValidationError +import com.hoc.flowmvi.test_utils.invalidValueOrThrow +import com.hoc.flowmvi.test_utils.valueOrThrow import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull import kotlin.test.assertTrue private const val ID = "id" @@ -25,7 +24,7 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isValid) - assertNotNull(validated.orNull()).let { user -> + validated.valueOrThrow.let { user -> assertEquals(ID, user.id) assertEquals(VALID_EMAIL, user.email.value) assertEquals(VALID_NAME, user.firstName.value) @@ -44,9 +43,7 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals(setOf(UserValidationError.INVALID_EMAIL_ADDRESS), errors.toSet()) - } + assertEquals(UserValidationError.INVALID_EMAIL_ADDRESS, validated.invalidValueOrThrow.single()) } @Test @@ -59,9 +56,7 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals(setOf(UserValidationError.TOO_SHORT_FIRST_NAME), errors.toSet()) - } + assertEquals(UserValidationError.TOO_SHORT_FIRST_NAME, validated.invalidValueOrThrow.single()) } @Test @@ -74,9 +69,7 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals(setOf(UserValidationError.TOO_SHORT_LAST_NAME), errors.toSet()) - } + assertEquals(UserValidationError.TOO_SHORT_LAST_NAME, validated.invalidValueOrThrow.single()) } @Test @@ -89,15 +82,13 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals( - setOf( - UserValidationError.INVALID_EMAIL_ADDRESS, - UserValidationError.TOO_SHORT_FIRST_NAME, - ), - errors.toSet() - ) - } + assertEquals( + setOf( + UserValidationError.INVALID_EMAIL_ADDRESS, + UserValidationError.TOO_SHORT_FIRST_NAME, + ), + validated.invalidValueOrThrow.toSet() + ) } @Test @@ -110,15 +101,13 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals( - setOf( - UserValidationError.INVALID_EMAIL_ADDRESS, - UserValidationError.TOO_SHORT_LAST_NAME, - ), - errors.toSet() - ) - } + assertEquals( + setOf( + UserValidationError.INVALID_EMAIL_ADDRESS, + UserValidationError.TOO_SHORT_LAST_NAME, + ), + validated.invalidValueOrThrow.toSet() + ) } @Test @@ -131,15 +120,13 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals( - setOf( - UserValidationError.TOO_SHORT_FIRST_NAME, - UserValidationError.TOO_SHORT_LAST_NAME, - ), - errors.toSet() - ) - } + assertEquals( + setOf( + UserValidationError.TOO_SHORT_FIRST_NAME, + UserValidationError.TOO_SHORT_LAST_NAME, + ), + validated.invalidValueOrThrow.toSet() + ) } @Test @@ -152,11 +139,9 @@ class UserTest { avatar = AVATAR, ) assertTrue(validated.isInvalid) - assertNotNull(validated.fold(fe = ::identity, fa = { null })).let { errors -> - assertEquals( - UserValidationError.values().toSet(), - errors.toSet() - ) - } + assertEquals( + UserValidationError.values().toSet(), + validated.invalidValueOrThrow.toSet() + ) } } diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt index 32971895..415f4592 100644 --- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt +++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt @@ -1,7 +1,8 @@ package com.hoc.flowmvi.ui.main -import arrow.core.orNull import com.hoc.flowmvi.domain.model.User +import com.hoc.flowmvi.test_utils.getOrThrow +import com.hoc.flowmvi.test_utils.valueOrThrow import kotlin.test.Test import kotlin.test.assertEquals @@ -69,14 +70,14 @@ class MainContractTest { firstName = "first", lastName = "last", avatar = "avatar.png", - ).orNull()!!, + ).valueOrThrow, UserItem( id = "0", email = "test@gmail.com", avatar = "avatar.png", firstName = "first", lastName = "last" - ).toDomain().orNull()!!, + ).toDomain().getOrThrow, ) } @@ -97,7 +98,7 @@ class MainContractTest { firstName = "first", lastName = "last", avatar = "avatar.png", - ).orNull()!! + ).valueOrThrow ), ) } diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt index 8a92ed7f..1f09c584 100644 --- a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt +++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt @@ -1,12 +1,24 @@ package com.hoc.flowmvi.test_utils +import arrow.core.Either import arrow.core.Validated +import arrow.core.getOrHandle +import arrow.core.identity import arrow.core.valueOr inline val Validated.valueOrThrow: A - get() { - return valueOr { - if (it is Throwable) throw it - else error("$this - $it - Should not reach here!") - } - } + get() = valueOr(this::throws) + +inline val Validated.invalidValueOrThrow: E + get() = fold(::identity, this::throws) + +inline val Either.leftOrThrow: L + get() = fold(::identity, this::throws) + +inline val Either.getOrThrow: R + get() = getOrHandle(this::throws) + +@PublishedApi +internal fun Any.throws(it: E): Nothing = + if (it is Throwable) throw it + else error("$this - $it - Should not reach here!") From e134b7d67a224fe3cc1d2fa70f0b886488f00602 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Fri, 12 Nov 2021 01:25:37 +0700 Subject: [PATCH 06/12] update UserRepositoryImpl.kt --- .../hoc/flowmvi/data/UserRepositoryImpl.kt | 83 ++++++++++++------- .../flowmvi/data/mapper/UserErrorMapper.kt | 2 +- .../data/mapper/UserErrorMapperTest.kt | 6 +- .../com/hoc/flowmvi/domain/model/UserError.kt | 2 +- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index ee801d1b..a65a82df 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -1,8 +1,8 @@ package com.hoc.flowmvi.data -import arrow.core.Either import arrow.core.ValidatedNel import arrow.core.andThen +import arrow.core.computations.either import arrow.core.left import arrow.core.leftWiden import arrow.core.right @@ -31,6 +31,7 @@ import timber.log.Timber import java.io.IOException import kotlin.time.Duration import kotlin.time.ExperimentalTime +import arrow.core.Either.Companion.catch as catchEither @ExperimentalTime @ExperimentalCoroutinesApi @@ -48,8 +49,17 @@ internal class UserRepositoryImpl( class Added(val user: User) : Change() } + private val responseToDomainThrows = responseToDomain andThen { validated -> + validated.valueOr { throw UserError.ValidationFailed(it.toSet()) } + } + private val changesFlow = MutableSharedFlow(extraBufferCapacity = 64) + private suspend inline fun sendChange(change: Change) = changesFlow.emit(change) + + @Suppress("NOTHING_TO_INLINE") + private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message) + private suspend fun getUsersFromRemote(): List { return withContext(dispatchers.io) { retrySuspend( @@ -61,7 +71,7 @@ internal class UserRepositoryImpl( Timber.d("[USER_REPO] Retry times=$times") userApiService .getUsers() - .map(responseToDomain andThen { it.valueOrThrowUserError() }) + .map(responseToDomainThrows) } } } @@ -83,44 +93,57 @@ internal class UserRepositoryImpl( } .map { it.right().leftWiden>() } .catch { - Timber.tag("UserRepositoryImpl").e(it, "getUsers") + logError(it, "getUsers") emit(errorMapper(it).left()) } - override suspend fun refresh() = Either.catch { - getUsersFromRemote().let { changesFlow.emit(Change.Refreshed(it)) } - }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "refresh") } + override suspend fun refresh() = catchEither { getUsersFromRemote() } + .tap { sendChange(Change.Refreshed(it)) } + .map { } + .tapLeft { logError(it, "refresh") } .mapLeft(errorMapper) - override suspend fun remove(user: User) = Either.catch { + override suspend fun remove(user: User) = either { withContext(dispatchers.io) { - val response = userApiService.remove(user.id) - changesFlow.emit(Change.Removed(responseToDomain(response).valueOrThrowUserError())) - } - }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "remove user=$user") } - .mapLeft(errorMapper) + val response = catchEither { userApiService.remove(user.id) } + .tapLeft { logError(it, "remove user=$user") } + .mapLeft(errorMapper) + .bind() - override suspend fun add(user: User) = Either.catch { - withContext(dispatchers.io) { - val body = domainToBody(user) - val response = userApiService.add(body) - changesFlow.emit(Change.Added(responseToDomain(response).valueOrThrowUserError())) - extraDelay() + val deleted = responseToDomain(response) + .mapLeft { UserError.ValidationFailed(it.toSet()) } + .tapInvalid { logError(it, "remove user=$user") } + .bind() + + sendChange(Change.Removed(deleted)) } - }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "add user=$user") } - .mapLeft(errorMapper) + } - override suspend fun search(query: String) = Either.catch { + override suspend fun add(user: User) = either { withContext(dispatchers.io) { - extraDelay() - userApiService.search(query).map(responseToDomain andThen { it.valueOrThrowUserError() }) + val response = catchEither { userApiService.add(domainToBody(user)) } + .tapLeft { logError(it, "add user=$user") } + .mapLeft(errorMapper) + .bind() + + delay(400) // TODO + + val added = responseToDomain(response) + .mapLeft { UserError.ValidationFailed(it.toSet()) } + .tapInvalid { logError(it, "add user=$user") } + .bind() + + sendChange(Change.Added(added)) } - }.tapLeft { Timber.tag("UserRepositoryImpl").e(it, "search query=$query") } - .mapLeft(errorMapper) + } - private suspend inline fun extraDelay() = delay(400) -} + override suspend fun search(query: String) = withContext(dispatchers.io) { + catchEither { userApiService.search(query).map(responseToDomainThrows) } + .tapLeft { logError(it, "search query=$query") } + .mapLeft(errorMapper) + } -@Suppress("NOTHING_TO_INLINE") -private inline fun ValidatedNel.valueOrThrowUserError(): A = - valueOr { throw UserError.ValidationFailed(it) } + private companion object { + private val TAG = UserRepositoryImpl::class.java.simpleName + } +} diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt index e501bb78..9125621a 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt @@ -48,7 +48,7 @@ internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter "internal-error" -> UserError.ServerError "invalid-id" -> UserError.InvalidId(id = errorResponse.data as String) "user-not-found" -> UserError.UserNotFound(id = errorResponse.data as String) - "validation-failed" -> UserError.ValidationFailed(errors = emptyList()) + "validation-failed" -> UserError.ValidationFailed(errors = emptySet()) else -> UserError.Unexpected } } diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt index b1fea84f..68d79432 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt @@ -49,8 +49,8 @@ class UserErrorMapperTest { assertEquals(UserError.UserNotFound("1"), errorMapper(UserError.UserNotFound("1"))) assertEquals(UserError.InvalidId("1"), errorMapper(UserError.InvalidId("1"))) assertEquals( - UserError.ValidationFailed(emptyList()), - errorMapper(UserError.ValidationFailed(emptyList())), + UserError.ValidationFailed(emptySet()), + errorMapper(UserError.ValidationFailed(emptySet())), ) assertEquals(UserError.ServerError, errorMapper(UserError.ServerError)) assertEquals(UserError.Unexpected, errorMapper(UserError.Unexpected)) @@ -158,7 +158,7 @@ class UserErrorMapperTest { errorMapper(buildHttpException("user-not-found", id)), ) assertEquals( - UserError.ValidationFailed(emptyList()), + UserError.ValidationFailed(emptySet()), errorMapper(buildHttpException("validation-failed", null)), ) } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt index b9d30177..33a723ec 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt @@ -4,7 +4,7 @@ sealed class UserError : Throwable() { object NetworkError : UserError() data class UserNotFound(val id: String) : UserError() data class InvalidId(val id: String) : UserError() - data class ValidationFailed(val errors: List) : UserError() + data class ValidationFailed(val errors: Set) : UserError() object ServerError : UserError() object Unexpected : UserError() } From 15cc2c687bec105630f4c7fc559adc9fc332448d Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Fri, 12 Nov 2021 02:09:50 +0700 Subject: [PATCH 07/12] update UserRepositoryImpl.kt --- data/build.gradle.kts | 1 + .../hoc/flowmvi/data/UserRepositoryImpl.kt | 11 +- .../data/UserRepositoryImplRealAPITest.kt | 100 ++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 7bcd2199..6aa10c16 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -56,4 +56,5 @@ dependencies { addUnitTest() testImplementation(testUtils) + testImplementation(deps.koin.testJunit4) } diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index a65a82df..f38aad37 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -1,7 +1,6 @@ package com.hoc.flowmvi.data import arrow.core.ValidatedNel -import arrow.core.andThen import arrow.core.computations.either import arrow.core.left import arrow.core.leftWiden @@ -49,8 +48,14 @@ internal class UserRepositoryImpl( class Added(val user: User) : Change() } - private val responseToDomainThrows = responseToDomain andThen { validated -> - validated.valueOr { throw UserError.ValidationFailed(it.toSet()) } + private val responseToDomainThrows: (UserResponse) -> User = { response -> + responseToDomain(response).let { validated -> + validated.valueOr { + val t = UserError.ValidationFailed(it.toSet()) + logError(t, "Map $response to user") + throw t + } + } } private val changesFlow = MutableSharedFlow(extraBufferCapacity = 64) diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt new file mode 100644 index 00000000..97f259d3 --- /dev/null +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt @@ -0,0 +1,100 @@ +package com.hoc.flowmvi.data + +import android.util.Log +import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers +import com.hoc.flowmvi.domain.repository.UserRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koin.test.inject +import timber.log.Timber +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.test.Test +import kotlin.time.ExperimentalTime + +@ExperimentalCoroutinesApi +@ExperimentalTime +@ExperimentalStdlibApi +class UserRepositoryImplRealAPITest : KoinTest { + @get:Rule + val koinRuleTest = KoinTestRule.create { + printLogger() + modules( + dataModule, + module { + factory { + object : CoroutineDispatchers { + override val main: CoroutineDispatcher get() = Main + override val io: CoroutineDispatcher get() = IO + } + } + } + ) + } + + @get:Rule + val timberRule = TimberRule() + + private val userRepo by inject() + + @Test + fun getUsers() = runBlocking { + val result = userRepo + .getUsers() + .first() + Timber.d("result=$result") + } +} + +class TimberRule : TestWatcher() { + private val tree = ConsoleTree() + + override fun starting(description: Description) { + Timber.plant(tree) + } + + override fun finished(description: Description) { + Timber.uproot(tree) + } +} + +class ConsoleTree : Timber.DebugTree() { + private val anonymousClassPattern = """(\$\d+)+$""".toRegex() + + private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + val dateTime = LocalDateTime.now().format(dateTimeFormatter) + val priorityChar = when (priority) { + Log.VERBOSE -> 'V' + Log.DEBUG -> 'D' + Log.INFO -> 'I' + Log.WARN -> 'W' + Log.ERROR -> 'E' + Log.ASSERT -> 'A' + else -> '?' + } + + println("$dateTime $priorityChar/$tag: $message") + } + + override fun createStackElementTag(element: StackTraceElement): String { + val className = element.className + val tag = if (anonymousClassPattern.containsMatchIn(className)) { + anonymousClassPattern.replace(className, "") + } else { + className + } + return tag.substringAfterLast('.') + } +} From 5895517100168fd456f8696ecedbfa6ebd6d36c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Fri, 12 Nov 2021 02:19:51 +0700 Subject: [PATCH 08/12] fix compiler error --- .../src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt index 3be59356..42d734fa 100644 --- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt +++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt @@ -1,5 +1,6 @@ package com.hoc.flowmvi.ui.main +import arrow.core.Either import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.mvi_base.MviIntent @@ -23,13 +24,13 @@ data class UserItem( lastName = domain.lastName.value ) - fun toDomain() = User.create( + fun toDomain(): Either = User.create( id = id, lastName = lastName, firstName = firstName, avatar = avatar, email = email - ).toEither().mapLeft(UserError::ValidationFailed) + ).toEither().mapLeft { UserError.ValidationFailed(it.toSet()) } } sealed interface ViewIntent : MviIntent { From db03a16ca7cbd635a48176a5d995da51ab5ef0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Fri, 12 Nov 2021 20:39:34 +0700 Subject: [PATCH 09/12] [skip ci] --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 15649223..4c26e9ee 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -7,7 +7,7 @@ on: pull_request: branches: [ master ] paths-ignore: [ '**.md', '**.MD' ] - + workflow_dispatch: env: CI: true From 07e6551a6eb13cc6766148b68e7aa3c60a91d2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Fri, 12 Nov 2021 20:41:52 +0700 Subject: [PATCH 10/12] Update unit-test.yml --- .github/workflows/unit-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 4c26e9ee..a460842d 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -8,6 +8,7 @@ on: branches: [ master ] paths-ignore: [ '**.md', '**.MD' ] workflow_dispatch: + env: CI: true From 51e1a6e3b52b68f5c7e13faa27a6251fb6423791 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Sat, 13 Nov 2021 01:14:20 +0700 Subject: [PATCH 11/12] up --- ...ExampleUnitTest.kt => CheckModulesTest.kt} | 3 ++- build.gradle.kts | 26 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) rename app/src/test/java/com/hoc/flowmvi/{ExampleUnitTest.kt => CheckModulesTest.kt} (90%) diff --git a/app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt similarity index 90% rename from app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt rename to app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt index 50a9efaf..fae6bb64 100644 --- a/app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt +++ b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt @@ -6,6 +6,7 @@ import io.mockk.mockkClass import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import org.junit.Rule +import org.koin.test.AutoCloseKoinTest import org.koin.test.check.checkKoinModules import org.koin.test.mock.MockProviderRule import kotlin.test.Test @@ -15,7 +16,7 @@ import kotlin.time.ExperimentalTime @FlowPreview @ExperimentalCoroutinesApi @ExperimentalTime -class ExampleUnitTest { +class CheckModulesTest : AutoCloseKoinTest() { @get:Rule val mockProvider = MockProviderRule.create { clazz -> mockkClass(clazz).also { o -> diff --git a/build.gradle.kts b/build.gradle.kts index 5a0527ad..2fd3cbf7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,6 @@ +import java.util.EnumSet +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.tasks.KotlinCompile // Top-level build file where you can add configuration options common to all sub-projects/modules. @@ -33,7 +36,9 @@ subprojects { // TODO this should all come from editorconfig https://github.com/diffplug/spotless/issues/142 mapOf( "indent_size" to "2", - "ij_kotlin_imports_layout" to "*" + "ij_kotlin_imports_layout" to "*", + "end_of_line" to "lf", + "charset" to "utf-8" ) ) @@ -56,7 +61,9 @@ subprojects { ktlint(ktlintVersion).userData( mapOf( "indent_size" to "2", - "ij_kotlin_imports_layout" to "*" + "ij_kotlin_imports_layout" to "*", + "end_of_line" to "lf", + "charset" to "utf-8" ) ) @@ -83,6 +90,21 @@ subprojects { isIncludeNoLocationClasses = true excludes = listOf("jdk.internal.*") } + + testLogging { + showExceptions = true + showCauses = true + showStackTraces = true + showStandardStreams = true + events = EnumSet.of( + TestLogEvent.PASSED, + TestLogEvent.FAILED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_OUT, + TestLogEvent.STANDARD_ERROR + ) + exceptionFormat = TestExceptionFormat.FULL + } } } } From 721f26d20fcbb62032d77d919b3281217a0d1acb Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Sat, 13 Nov 2021 01:35:32 +0700 Subject: [PATCH 12/12] up --- .../com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt | 5 ++++- domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt index 97f259d3..cd2a2b68 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt @@ -3,6 +3,7 @@ package com.hoc.flowmvi.data import android.util.Log import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers import com.hoc.flowmvi.domain.repository.UserRepository +import com.hoc.flowmvi.test_utils.getOrThrow import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -20,6 +21,7 @@ import timber.log.Timber import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.test.Test +import kotlin.test.assertTrue import kotlin.time.ExperimentalTime @ExperimentalCoroutinesApi @@ -52,7 +54,8 @@ class UserRepositoryImplRealAPITest : KoinTest { val result = userRepo .getUsers() .first() - Timber.d("result=$result") + assertTrue(result.isRight()) + assertTrue(result.getOrThrow.isNotEmpty()) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt index 1c51329e..f71a6d03 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt @@ -51,7 +51,7 @@ internal fun validateLastName(lastName: String?): ValidatedNel { - if (email == null || !EMAIL_ADDRESS.matches(email)) { + if (email == null || !EMAIL_ADDRESS_REGEX.matches(email)) { return UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNel } // more validations here @@ -60,5 +60,5 @@ internal fun validateEmail(email: String?): ValidatedNel