diff --git a/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/RemoteAuthRepository.kt b/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/RemoteAuthRepository.kt index 2a6c7c8..af20a87 100644 --- a/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/RemoteAuthRepository.kt +++ b/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/RemoteAuthRepository.kt @@ -2,7 +2,6 @@ package nl.jovmit.androiddevs.domain.auth import nl.jovmit.androiddevs.core.network.AuthService import nl.jovmit.androiddevs.core.network.LoginData -import nl.jovmit.androiddevs.core.network.SignUpData import nl.jovmit.androiddevs.domain.auth.data.AuthResult import nl.jovmit.androiddevs.domain.auth.data.toDomain import javax.inject.Inject @@ -17,7 +16,7 @@ internal class RemoteAuthRepository @Inject constructor( val response = authService.login(loginData) AuthResult.Success(response.token, response.userData.toDomain()) } catch (exception: Exception) { - AuthResult.Error + AuthResult.ExistingUserError } } diff --git a/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/data/AuthResult.kt b/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/data/AuthResult.kt index d10dcb8..4237afc 100644 --- a/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/data/AuthResult.kt +++ b/domain/auth/src/main/java/nl/jovmit/androiddevs/domain/auth/data/AuthResult.kt @@ -7,5 +7,9 @@ sealed class AuthResult { val user: User ) : AuthResult() - data object Error : AuthResult() + data object BackendError : AuthResult() + + data object ExistingUserError : AuthResult() + + data object OfflineError : AuthResult() } diff --git a/domain/auth/src/test/java/nl/jovmit/androiddevs/base/auth/AuthContractTest.kt b/domain/auth/src/test/java/nl/jovmit/androiddevs/base/auth/AuthContractTest.kt index f5044d8..f479eec 100644 --- a/domain/auth/src/test/java/nl/jovmit/androiddevs/base/auth/AuthContractTest.kt +++ b/domain/auth/src/test/java/nl/jovmit/androiddevs/base/auth/AuthContractTest.kt @@ -30,7 +30,7 @@ abstract class AuthContractTest { val result = usersCatalog.login(bob.email, bobPassword) - assertThat(result).isEqualTo(AuthResult.Error) + assertThat(result).isEqualTo(AuthResult.ExistingUserError) } @Test @@ -39,7 +39,7 @@ abstract class AuthContractTest { val result = usersCatalog.login(bob.email, bobPassword) - assertThat(result).isEqualTo(AuthResult.Error) + assertThat(result).isEqualTo(AuthResult.ExistingUserError) } abstract fun usersCatalogWith(authToken: String, password: String, users: List): AuthRepository diff --git a/feature/signup/build.gradle.kts b/feature/signup/build.gradle.kts index bfbc4a4..87d7c0a 100644 --- a/feature/signup/build.gradle.kts +++ b/feature/signup/build.gradle.kts @@ -55,8 +55,8 @@ android { dependencies { implementation(projects.core.view) + implementation(projects.domain.auth) implementation(libs.bundles.hilt) - testImplementation(project(":testutils")) kapt(libs.hilt.compiler) diff --git a/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpScreen.kt b/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpScreen.kt index 12ad841..f063895 100644 --- a/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpScreen.kt +++ b/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpScreen.kt @@ -176,7 +176,7 @@ private fun SignUpScreenContent( private fun PreviewLoginScreen() { AppTheme { SignUpScreenContent( - signUpScreenState = SignUpScreenState(), + signUpScreenState = SignUpScreenState(incorrectEmailFormat = true), onEmailChanged = {}, onPasswordChanged = {}, onAboutChanged = {}, diff --git a/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpViewModel.kt b/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpViewModel.kt index 11ddfcb..031c31e 100644 --- a/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpViewModel.kt +++ b/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/SignUpViewModel.kt @@ -2,13 +2,23 @@ package nl.jovmit.androiddevs.feature.signup import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import nl.jovmit.androiddevs.core.view.extensions.update +import nl.jovmit.androiddevs.core.view.validation.EmailValidator +import nl.jovmit.androiddevs.core.view.validation.PasswordValidator +import nl.jovmit.androiddevs.domain.auth.AuthRepository +import nl.jovmit.androiddevs.domain.auth.data.AuthResult import nl.jovmit.androiddevs.feature.signup.state.SignUpScreenState -class SignUpViewModel( - private val savedStateHandle: SavedStateHandle +class SignUpViewModel ( + private val savedStateHandle: SavedStateHandle, + private val authRepository: AuthRepository ) : ViewModel() { + private val emailValidator = EmailValidator() + private val passwordValidator = PasswordValidator() + val screenState = savedStateHandle.getStateFlow(SIGN_UP, SignUpScreenState()) fun updateEmail(value: String) { @@ -29,6 +39,74 @@ class SignUpViewModel( } } + fun signUp() { + val email = screenState.value.email + val password = screenState.value.password + val about = screenState.value.about + + val isEmailValid = emailValidator.validateEmail(email) + val isPasswordValid = passwordValidator.validatePassword(password) + + if (!isEmailValid) { setIncorrectEmailFormatError() } + if (!isPasswordValid) { setIncorrectPasswordFormatError() } + + if (isEmailValid && isPasswordValid) { + performSignUp(email, password, about) + } + } + + private fun setIncorrectEmailFormatError() { + savedStateHandle.update(SIGN_UP) { + it.copy(incorrectEmailFormat = true) + } + } + + private fun setIncorrectPasswordFormatError() { + savedStateHandle.update(SIGN_UP) { + it.copy(incorrectPasswordFormat = true) + } + } + + private fun performSignUp(email: String, password: String, about: String) { + viewModelScope.launch { //TODO (we need to offload from main thread) + val result = authRepository.signUp(email, password, about) + onAuthResults(result) + } + } + + private fun onAuthResults(result: AuthResult) { + when (result) { + is AuthResult.Success -> onSignedUp() + is AuthResult.BackendError -> onBackendError() + is AuthResult.ExistingUserError -> onExistingUserError() + is AuthResult.OfflineError -> onOfflineError() + } + } + + private fun onSignedUp() { + savedStateHandle.update(SIGN_UP) { + it.copy(isSignedUp = true) + } + } + + private fun onBackendError() { + savedStateHandle.update(SIGN_UP) { + it.copy(isBackendError = true) + } + } + + private fun onExistingUserError() { + savedStateHandle.update(SIGN_UP) { + it.copy(isExistingEmail = true) + } + } + + private fun onOfflineError() { + savedStateHandle.update(SIGN_UP) { + it.copy(isOfflineError = true) + } + } + companion object { private const val SIGN_UP = "signUpKey" } diff --git a/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/state/SignUpScreenState.kt b/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/state/SignUpScreenState.kt index 6a40793..55c0001 100644 --- a/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/state/SignUpScreenState.kt +++ b/feature/signup/src/main/java/nl/jovmit/androiddevs/feature/signup/state/SignUpScreenState.kt @@ -7,5 +7,11 @@ import kotlinx.parcelize.Parcelize data class SignUpScreenState( val email: String = "", val password: String = "", - val about: String = "" + val about: String = "", + val incorrectEmailFormat: Boolean = false, + val incorrectPasswordFormat: Boolean = false, + val isSignedUp: Boolean = false, + val isExistingEmail: Boolean = false, + val isBackendError: Boolean = false, + val isOfflineError: Boolean = false ) : Parcelable \ No newline at end of file diff --git a/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpScreenStateTest.kt b/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpScreenStateTest.kt index 25a0e36..9928383 100644 --- a/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpScreenStateTest.kt +++ b/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpScreenStateTest.kt @@ -3,17 +3,19 @@ package nl.jovmit.androiddevs.feature.signup import androidx.lifecycle.SavedStateHandle import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import nl.jovmit.androiddevs.domain.auth.InMemoryAuthRepository import nl.jovmit.androiddevs.feature.signup.state.SignUpScreenState import org.junit.jupiter.api.Test class SignUpScreenStateTest { + private val authRepository = InMemoryAuthRepository() private val savedStateHandle = SavedStateHandle() @Test fun `email value updating`() { val newEmailValue = "email@" - val viewModel = SignUpViewModel(savedStateHandle) + val viewModel = SignUpViewModel(savedStateHandle, authRepository) viewModel.updateEmail(newEmailValue) @@ -25,7 +27,7 @@ class SignUpScreenStateTest { @Test fun `password value updating`() = runTest { val newValue = ":irrelevant:" - val viewModel = SignUpViewModel(savedStateHandle) + val viewModel = SignUpViewModel(savedStateHandle, authRepository) viewModel.updatePassword(newValue) @@ -37,7 +39,7 @@ class SignUpScreenStateTest { @Test fun `about value updating`() { val newValue = ":dunno:" - val viewModel = SignUpViewModel(savedStateHandle) + val viewModel = SignUpViewModel(savedStateHandle, authRepository) viewModel.updateAbout(newValue) diff --git a/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpTest.kt b/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpTest.kt new file mode 100644 index 0000000..49371a5 --- /dev/null +++ b/feature/signup/src/test/java/nl/jovmit/androiddevs/feature/signup/SignUpTest.kt @@ -0,0 +1,138 @@ +package nl.jovmit.androiddevs.feature.signup + +import androidx.lifecycle.SavedStateHandle +import com.google.common.truth.Truth.assertThat +import nl.jovmit.androiddevs.domain.auth.InMemoryAuthRepository +import nl.jovmit.androiddevs.domain.auth.data.User +import nl.jovmit.androiddevs.feature.signup.state.SignUpScreenState +import nl.jovmit.androiddevs.testutils.CoroutineTestExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class SignUpTest { + + // - we don't want long running operations in the main thread + // - state delivery in order + // - contract test to make sure prod code is aligned with the fake + + private val validEmail = "email@email.com" + private val validPassword = "ValidP@ssword1" + + private val authRepository = InMemoryAuthRepository() + private val savedStateHandle = SavedStateHandle() + + @Test + fun invalidEmail() { + val email = "invalid email format" + val viewModel = SignUpViewModel(savedStateHandle, authRepository) + + viewModel.signUp(email = email) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState( + email = email, + incorrectEmailFormat = true, + incorrectPasswordFormat = true + ) + ) + } + + @Test + fun invalidPassword() { + val viewModel = SignUpViewModel(savedStateHandle, authRepository) + + viewModel.signUp(email = validEmail) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState(email = validEmail, incorrectPasswordFormat = true) + ) + } + + @Test + fun invalidEmailWithValidPassword() { + val viewModel = SignUpViewModel(savedStateHandle, authRepository) + + viewModel.signUp(password = validPassword) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState(password = validPassword, incorrectEmailFormat = true) + ) + } + + @Test + fun signedUpSuccessfully() { + val viewModel = SignUpViewModel(savedStateHandle, authRepository) + + viewModel.signUp(validEmail, validPassword) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState( + email = validEmail, + password = validPassword, + isSignedUp = true + ) + ) + } + + @Test + fun attemptToSignUpWithKnownEmail() { + val newPassword = "another$validPassword" + val repository = InMemoryAuthRepository( + usersForPassword = mapOf(validPassword to listOf(User("userId", validEmail, ""))) + ) + val viewModel = SignUpViewModel(savedStateHandle, repository) + + viewModel.signUp(validEmail, newPassword) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState( + email = validEmail, + password = newPassword, + isExistingEmail = true + ) + ) + } + + @Test + fun errorCreatingNewAccount() { + val unavailableAuthRepository = InMemoryAuthRepository().apply { + setUnavailable() + } + val viewModel = SignUpViewModel(savedStateHandle, unavailableAuthRepository) + + viewModel.signUp(validEmail, validPassword) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState( + email = validEmail, + password = validPassword, + isBackendError = true + ) + ) + } + + @Test + fun attemptToSignUpWhenOffline() { + val offlineAuthRepository = InMemoryAuthRepository().apply { + setOffline() + } + val viewModel = SignUpViewModel(savedStateHandle, offlineAuthRepository) + + viewModel.signUp(validEmail, validPassword) + + assertThat(viewModel.screenState.value).isEqualTo( + SignUpScreenState( + email = validEmail, + password = validPassword, + isOfflineError = true + ) + ) + } + + private fun SignUpViewModel.signUp(email: String = "", password: String = "") { + updateEmail(email) + updatePassword(password) + signUp() + } +} \ No newline at end of file diff --git a/testutils/src/main/java/nl/jovmit/androiddevs/domain/auth/InMemoryAuthRepository.kt b/testutils/src/main/java/nl/jovmit/androiddevs/domain/auth/InMemoryAuthRepository.kt index ae343d8..801bcab 100644 --- a/testutils/src/main/java/nl/jovmit/androiddevs/domain/auth/InMemoryAuthRepository.kt +++ b/testutils/src/main/java/nl/jovmit/androiddevs/domain/auth/InMemoryAuthRepository.kt @@ -2,12 +2,16 @@ package nl.jovmit.androiddevs.domain.auth import nl.jovmit.androiddevs.domain.auth.data.AuthResult import nl.jovmit.androiddevs.domain.auth.data.User +import java.util.UUID class InMemoryAuthRepository( private val authToken: String = "", - usersForPassword: Map> + usersForPassword: Map> = emptyMap() ) : AuthRepository { + private var isUnavailable = false + private var isOffline = false + private val _usersForPassword = usersForPassword.toMutableMap() override suspend fun login(email: String, password: String): AuthResult { @@ -16,14 +20,43 @@ class InMemoryAuthRepository( found?.let { user -> return AuthResult.Success(authToken, user) } - return AuthResult.Error + return AuthResult.ExistingUserError + } + + override suspend fun signUp( + email: String, + password: String, + about: String + ): AuthResult { + if (isUnavailable) return AuthResult.BackendError + if (isOffline) return AuthResult.OfflineError + if (isKnownUser(email)) return AuthResult.ExistingUserError + val user = User(UUID.randomUUID().toString(), email, about) + saveUserData(password, user) + return AuthResult.Success(authToken, user) + } + + private fun isKnownUser(email: String) = _usersForPassword.values.flatten().any { + it.email == email } - override suspend fun signUp(email: String, password: String, about: String): AuthResult { - TODO("Not yet implemented") + private fun saveUserData(password: String, user: User) { + val currentUsers = _usersForPassword.getOrElse(password) { emptyList() } + currentUsers.toMutableList().apply { + add(user) + } + _usersForPassword[password] = currentUsers } fun setLoggedInUsers(usersForPassword: Map>) { _usersForPassword.putAll(usersForPassword) } + + fun setUnavailable() { + isUnavailable = true + } + + fun setOffline() { + isOffline = true + } } \ No newline at end of file