Skip to content

Commit

Permalink
Reorganize use case - repository structure
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertoMoreta committed Oct 31, 2023
1 parent 7db29bb commit b1d1696
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 74 deletions.
9 changes: 7 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ android {
viewBinding = true
}

testOptions {
unitTests.isReturnDefaultValues = true
}

}

dependencies {
Expand All @@ -96,8 +100,7 @@ dependencies {
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")

implementation("androidx.preference:preference:1.2.0")
implementation("androidx.preference:preference-ktx:1.2.1")

implementation("commons-codec:commons-codec:1.16.0")

Expand All @@ -112,11 +115,13 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("org.jacoco:org.jacoco.core:0.8.8")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")

// Kotlin Dependencies
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
}

ktlint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.digitalcampus.oppiamobile.data.config.db.AppDatabase
import org.digitalcampus.oppiamobile.data.course.remote.CourseRemoteService
import org.digitalcampus.oppiamobile.data.course.repository.CourseRemoteDataSource
import org.digitalcampus.oppiamobile.di.ApiKey
import org.digitalcampus.oppiamobile.domain.use_cases.TestApiClientUseCase
import org.digitalcampus.oppiamobile.domain.useCases.TestApiClientUseCase
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Singleton
Expand Down
14 changes: 6 additions & 8 deletions app/src/main/java/org/digitalcampus/oppiamobile/data/user/di.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import org.digitalcampus.oppiamobile.data.user.remote.auth.AuthRemoteService
import org.digitalcampus.oppiamobile.data.user.repository.UserDbDataSource
import org.digitalcampus.oppiamobile.data.user.repository.UserRemoteDataSource
import org.digitalcampus.oppiamobile.data.user.repository.UserRepository
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginLocalUseCase
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginRemoteUseCase
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginUseCase
import org.digitalcampus.oppiamobile.utils.ConnectivityUtils
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Singleton
Expand Down Expand Up @@ -41,14 +41,12 @@ class UserModule {
@Provides
fun provideAuthRepository(
authDbDataSource: UserDbDataSource,
userRemoteDataSource: UserRemoteDataSource
) = UserRepository(authDbDataSource, userRemoteDataSource)
userRemoteDataSource: UserRemoteDataSource,
connectivityUtils: ConnectivityUtils
) = UserRepository(authDbDataSource, userRemoteDataSource, connectivityUtils)

@Singleton
@Provides
fun provideUserLoginRemoteUseCase(userRepository: UserRepository) = UserLoginRemoteUseCase(userRepository)
fun provideUserLoginUseCase(userRepository: UserRepository) = UserLoginUseCase(userRepository)

@Singleton
@Provides
fun provideUserLoginLocalUseCase(userRepository: UserRepository) = UserLoginLocalUseCase(userRepository)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
package org.digitalcampus.oppiamobile.data.user.repository

import org.digitalcampus.oppiamobile.data.user.db.entity.passwordEncrypted
import org.digitalcampus.oppiamobile.domain.model.User
import org.digitalcampus.oppiamobile.utils.ConnectivityUtils
import org.digitalcampus.oppiamobile.utils.CryptoUtils
import javax.inject.Inject

class UserRepository @Inject constructor(
private val authDbDataSource: UserDbDataSource, // TODO COMENTAR: prefijo auth o user?
private val userRemoteDataSource: UserRemoteDataSource,
private val connectivityUtils: ConnectivityUtils
) {

suspend fun login(username: String, password: String): User {
val isConnected = connectivityUtils.isConnected()
return if (isConnected) {
loginRemotely(username, password)
} else {
loginLocaly(username, password)
}
}

private suspend fun loginRemotely(username: String, password: String): User {
val user = userRemoteDataSource.login(username, password)
val localUser = authDbDataSource.getByUsername(username)
localUser?.let {
Expand All @@ -27,5 +40,17 @@ class UserRepository @Inject constructor(
return user
}

suspend fun getLocalUserByUsername(username: String) = authDbDataSource.getByUsername(username)
private suspend fun loginLocaly(username: String, password: String) : User {
val localUser = authDbDataSource.getByUsername(username)
if (localUser == null) {
throw Exception("User not found")
} else {
if (localUser.passwordEncrypted == CryptoUtils.encryptLocalPassword(password)) {
// TODO pending user api key check
return localUser.toUser()
} else {
throw Exception("Wrong password")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,9 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.digitalcampus.oppiamobile.data.config.BASE_URL
import org.digitalcampus.oppiamobile.data.config.db.AppDatabase
import org.digitalcampus.oppiamobile.data.user.db.dao.UserDao
import org.digitalcampus.oppiamobile.data.user.remote.auth.AuthRemoteService
import org.digitalcampus.oppiamobile.data.user.repository.UserDbDataSource
import org.digitalcampus.oppiamobile.data.user.repository.UserRemoteDataSource
import org.digitalcampus.oppiamobile.data.user.repository.UserRepository
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginLocalUseCase
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginRemoteUseCase
import org.digitalcampus.oppiamobile.utils.ConnectivityUtils
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import javax.inject.Singleton

@Module
Expand Down Expand Up @@ -81,6 +73,5 @@ class AppModule {
@ApiKey
fun provideApiKey(prefs: SharedPreferences): String = "ApiKey jbc25:896753f4968d1af4555fb70b454a27572dafe075"


// TODO COMENTAR aquí se pueden llegar a crear muchísimos Provides, organizamos por funcionalidad o modelo?
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.digitalcampus.oppiamobile.domain.use_cases
package org.digitalcampus.oppiamobile.domain.useCases

import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import org.digitalcampus.oppiamobile.data.course.remote.CourseRemoteService
import org.digitalcampus.oppiamobile.di.ApiKey
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import org.digitalcampus.oppiamobile.data.user.repository.UserRepository
import org.digitalcampus.oppiamobile.domain.model.User
import javax.inject.Inject

class UserLoginRemoteUseCase @Inject constructor(
class UserLoginUseCase @Inject constructor(
private val userRepository: UserRepository,
) {

suspend operator fun invoke(username: String, password: String): User {
val user = userRepository.login(username, password)
Log.d("UserLoginRemoteUseCase", "doLogin: User: $user")
Log.d("UserLoginUseCase", "doLogin: User: $user")
return user
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginLocalUseCase
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginRemoteUseCase
import org.digitalcampus.oppiamobile.domain.use_cases.TestApiClientUseCase
import org.digitalcampus.oppiamobile.domain.model.User
import org.digitalcampus.oppiamobile.domain.useCases.TestApiClientUseCase
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginUseCase
import org.digitalcampus.oppiamobile.ui.common.AppViewModel
import org.digitalcampus.oppiamobile.utils.ConnectivityUtils
import javax.inject.Inject

@HiltViewModel
class LoginViewModel @Inject constructor(
private val userLoginRemoteUseCase: UserLoginRemoteUseCase,
private val userLoginLocalUseCase: UserLoginLocalUseCase,
private val connectivityUtils: ConnectivityUtils,
private val userLoginUseCase: UserLoginUseCase,
private val testApiClientUseCase: TestApiClientUseCase,
) : AppViewModel() {

Expand All @@ -29,6 +26,7 @@ class LoginViewModel @Inject constructor(
val loading: Boolean = false,
val error: String? = null,
val loginSuccess: Boolean = false,
val user: User? = null,
)

init {
Expand All @@ -43,32 +41,36 @@ class LoginViewModel @Inject constructor(
}

fun onLoginClick(username: String, password: String) {
val result = validate(username, password)
if (result){
doLogin(username, password)
}
}

private fun validate(username: String, password: String) : Boolean {
var result = true
if (username.isBlank()) {
_uiState.update { it.copy(error = "Username is emnpty") }
_uiState.update { it.copy(error = "Username is empty") }
result = false
} else if (password.isBlank()) {
_uiState.update { it.copy(error = "Password is emnpty") }
} else {
doLogin(username, password)
_uiState.update { it.copy(error = "Password is empty") }
result = false
}

return result
}

private fun doLogin(username: String, password: String) {
val isConnected = connectivityUtils.isConnected()

viewModelScope.launch {
_uiState.update { it.copy(loading = true) }

try {
val user =
if (isConnected) {
userLoginRemoteUseCase(username, password)
} else {
userLoginLocalUseCase(username, password)
}
val user = userLoginUseCase(username, password)

Log.d(TAG, "doLogin: User: $user")

_uiState.update { it.copy(error = "Login success") } // for testing
_uiState.update { it.copy(error = "Login success", user = user) } // for testing


// TODO Go to main activity
} catch (e: Exception) {
Expand All @@ -78,8 +80,6 @@ class LoginViewModel @Inject constructor(
} finally {
_uiState.update { it.copy(loading = false) }
}


}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.digitalcampus.oppiamobile.ui.auth.login

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.digitalcampus.oppiamobile.domain.model.User
import org.digitalcampus.oppiamobile.domain.useCases.TestApiClientUseCase
import org.digitalcampus.oppiamobile.domain.useCases.UserLoginUseCase
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import utils.MainCoroutineRule

internal class LoginViewModelTest {

private val userLoginUseCase = mock<UserLoginUseCase>()
private val testApiClientUseCase = mock<TestApiClientUseCase>()

val viewmodel by lazy { LoginViewModel(userLoginUseCase, testApiClientUseCase) }

@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()

@Test
fun `test login successful`() = runTest {
val expectedUser = User(
email = "testEmail@test.com",
username = "testUsername",
firstName = "Test",
lastName = "User",
apiKey = "testApiKey"
)
whenever(userLoginUseCase.invoke(ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(expectedUser)

viewmodel.onLoginClick("testUsername", "Some Password")
advanceUntilIdle()
Assert.assertEquals(expectedUser, viewmodel.uiState.value.user)
}

@Test
fun `test user is empty`() = runTest {
viewmodel.onLoginClick("", "")
Assert.assertEquals("Username is empty", viewmodel.uiState.value.error)
}

@Test
fun `test password is empty`() = runTest {
viewmodel.onLoginClick("someUsername", "")
Assert.assertEquals("Password is empty", viewmodel.uiState.value.error)
}
}
25 changes: 25 additions & 0 deletions app/src/test/java/utils/MainCoroutineRule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package utils

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@ExperimentalCoroutinesApi
class MainCoroutineRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {

override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}

override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}

0 comments on commit b1d1696

Please sign in to comment.