diff --git a/app/build.gradle b/app/build.gradle index 26137f69f9d0e2f98323edc7b9f05c3dda51e6ce..81ed596b62decc731526df08368be4cff27a8a7e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,6 +133,13 @@ android { kotlin.sourceSets.all { languageSettings.optIn("kotlin.RequiresOptIn") } + + testOptions { + unitTests { + includeAndroidResources = true + returnDefaultValues = true + } + } } kapt { @@ -256,4 +263,5 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.0.0' testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' testImplementation 'org.robolectric:robolectric:4.9' + testImplementation 'org.json:json:20180813' // Added to avoid SystemInfoProvider.getAppBuildInfo() mock error } diff --git a/app/src/main/java/foundation/e/apps/di/LoginModule.kt b/app/src/main/java/foundation/e/apps/di/LoginModule.kt index f59a084b8abfbab057fc725a488be61096d43095..3df9857af423c883e61d7eaf867c8ad8a24cb7b8 100644 --- a/app/src/main/java/foundation/e/apps/di/LoginModule.kt +++ b/app/src/main/java/foundation/e/apps/di/LoginModule.kt @@ -24,6 +24,8 @@ import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.login.LoginSourceCleanApk import foundation.e.apps.data.login.LoginSourceGPlay import foundation.e.apps.data.login.LoginSourceInterface +import foundation.e.apps.domain.login.repository.LoginRepositoryImpl +import foundation.e.apps.domain.login.usecase.UserLoginUseCase @InstallIn(SingletonComponent::class) @Module @@ -36,4 +38,11 @@ object LoginModule { ): List { return listOf(gPlay, cleanApk) } + + @Provides + fun provideLoginUserCase( + loginRepositoryImpl: LoginRepositoryImpl + ): UserLoginUseCase { + return UserLoginUseCase(loginRepositoryImpl) + } } diff --git a/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt b/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt index 359d42bee804e69a3aba3d78abd56cd77a2e992c..9244b1444221359ff5d0e3536ef8f2d7a4e5c67c 100644 --- a/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/login/usecase/UserLoginUseCase.kt @@ -31,7 +31,7 @@ class UserLoginUseCase @Inject constructor( fun anonymousUser(): Flow> = flow { try { - emit(Resource.Loading) + emit(Resource.Loading()) val userResponse = loginRepository.anonymousUser() emit(Resource.Success(userResponse)) } catch (e: Exception) { diff --git a/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt b/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt new file mode 100644 index 0000000000000000000000000000000000000000..23f54bc343f977a11a681314eda371c56b9b2415 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/presentation/login/LoginState.kt @@ -0,0 +1,25 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.presentation.login + +data class LoginState( + val isLoading: Boolean = false, + val isLoggedIn: Boolean = false, + val error: String = "" +) diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt similarity index 78% rename from app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt rename to app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt index 25c54362e6912ddeb2618291cd1908ce94b35b01..18da1ca6e4b7cb650ee9dc351b9267e9dfa8c315 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/presentation/login/LoginViewModel.kt @@ -15,14 +15,21 @@ * along with this program. If not, see . */ -package foundation.e.apps.data.login +package foundation.e.apps.presentation.login +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.login.LoginSourceRepository +import foundation.e.apps.domain.login.usecase.UserLoginUseCase import foundation.e.apps.ui.parentFragment.LoadingViewModel +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -33,6 +40,7 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val loginSourceRepository: LoginSourceRepository, + private val userLoginUseCase: UserLoginUseCase ) : ViewModel() { /** @@ -131,4 +139,27 @@ class LoginViewModel @Inject constructor( authObjects.postValue(listOf()) } } + + private val _loginState: MutableLiveData = MutableLiveData() + val loginState: LiveData = _loginState + + fun authenticateAnonymousUser() { + viewModelScope.launch { + userLoginUseCase.anonymousUser().onEach { result -> + when (result) { + is Resource.Success -> { + _loginState.value = LoginState(isLoggedIn = true) + } + is Resource.Error -> { + _loginState.value = LoginState( + error = result.message ?: "An unexpected error occured" + ) + } + is Resource.Loading -> { + _loginState.value = LoginState(isLoading = true) + } + } + }.collect() + } + } } diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index a2426b41f023fb3f26770c5dbb5ef2b061c65c0b..82e5f4ce19127d657fc6c2e636702b81702616e8 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -46,15 +46,14 @@ import foundation.e.apps.R import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.install.workmanager.InstallWorkManager +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.purchase.AppPurchaseFragmentDirections import foundation.e.apps.ui.settings.SettingsFragment -import foundation.e.apps.ui.setup.signin.SignInViewModel import foundation.e.apps.utils.SystemInfoProvider import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus @@ -68,7 +67,6 @@ import java.util.UUID @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private lateinit var signInViewModel: SignInViewModel private lateinit var loginViewModel: LoginViewModel private lateinit var binding: ActivityMainBinding private val TAG = MainActivity::class.java.simpleName @@ -90,7 +88,6 @@ class MainActivity : AppCompatActivity() { var hasInternet = true viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java] - signInViewModel = ViewModelProvider(this)[SignInViewModel::class.java] loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] // navOptions and activityNavController for TOS and SignIn Fragments diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 5173da6fecf6a5efdda1fd3fb097fcb0df58dd8a..9e395ae2a9fed384b97004c74fd91d560ced8f87 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -34,13 +34,13 @@ import foundation.e.apps.R import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.LoginSourceGPlay -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.databinding.DialogErrorLogBinding +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import timber.log.Timber diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt index d3c507cad16681bb6ca6c907bcb3d24d3a156528..9865ff7cd66f5fa909381ad565a7e5ff66dc8c92 100644 --- a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt @@ -40,9 +40,9 @@ import foundation.e.apps.BuildConfig import foundation.e.apps.R import foundation.e.apps.data.enums.User import foundation.e.apps.data.fused.UpdatesDao -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.databinding.CustomPreferenceBinding import foundation.e.apps.install.updates.UpdatesWorkManager +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.utils.SystemInfoProvider import timber.log.Timber diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt index 213e84148393d2c6c1a0965bddde44ce7cb9d5c9..e9ce89eb328229d13bb9af99d39a6ad4d9d20223 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt @@ -7,9 +7,9 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.databinding.FragmentSignInBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate +import foundation.e.apps.presentation.login.LoginViewModel import foundation.e.apps.utils.showGoogleSignInAlertDialog @AndroidEntryPoint diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt index 8dc3081b306bee1790d961c2a2ed725765f8cf13..3ecc77843e4792036ea7e4bb96d11d2b30625130 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt @@ -32,9 +32,9 @@ import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.gplay.utils.AC2DMUtil -import foundation.e.apps.data.login.LoginViewModel import foundation.e.apps.databinding.FragmentGoogleSigninBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate +import foundation.e.apps.presentation.login.LoginViewModel @AndroidEntryPoint class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { diff --git a/app/src/main/java/foundation/e/apps/utils/Resource.kt b/app/src/main/java/foundation/e/apps/utils/Resource.kt index 81bee55fcab18161df9b15b1f4206a4a4b4ce8d9..f7a20a46787a04ca98f7f6b0841c00bb7e14c473 100644 --- a/app/src/main/java/foundation/e/apps/utils/Resource.kt +++ b/app/src/main/java/foundation/e/apps/utils/Resource.kt @@ -21,20 +21,20 @@ package foundation.e.apps.utils /** * Class represents the different states of a resource for user case layer */ -sealed class Resource { +sealed class Resource(val data: T? = null, val message: String? = null) { /** * Represents a successful state of the resource with data. * @param data The data associated with the resource. */ - class Success(val data: T) : Resource() + class Success(data: T) : Resource(data) /** * Represents an error state of the resource with an error message. * @param message The error message associated with the resource. */ - class Error(message: String) : Resource() + class Error(message: String, data: T? = null) : Resource(data, message) /** * Represents a loading state of the resource. */ - object Loading : Resource() + class Loading(data: T? = null) : Resource(data) } diff --git a/app/src/test/java/foundation/e/apps/presentation/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/presentation/login/LoginViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..16b2d5d77d2899e389c1e2b75f9ada7d9bf4a734 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/presentation/login/LoginViewModelTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.presentation.login + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import foundation.e.apps.data.login.LoginSourceRepository +import foundation.e.apps.domain.login.usecase.UserLoginUseCase +import foundation.e.apps.loginFailureMessage +import foundation.e.apps.testAnonymousResponseData +import foundation.e.apps.util.getOrAwaitValue +import foundation.e.apps.utils.Resource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +class LoginViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + + @get:Rule + val rule = InstantTaskExecutorRule() + + @Mock + lateinit var mockUserLoginUseCase: UserLoginUseCase + + @Mock + lateinit var loginRepository: LoginSourceRepository + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + Dispatchers.setMain(testDispatcher) + } + + @Test + fun testOnSuccessReturnLogInStateTrue() = runTest { + Mockito.`when`(mockUserLoginUseCase.anonymousUser()) + .thenReturn(flowOf(Resource.Success(testAnonymousResponseData))) + + val loginViewModel = LoginViewModel(loginRepository, mockUserLoginUseCase) + loginViewModel.authenticateAnonymousUser() + testDispatcher.scheduler.advanceUntilIdle() + val result = loginViewModel.loginState.getOrAwaitValue() + Assert.assertEquals(true, result.isLoggedIn) + Assert.assertEquals(false, result.isLoading) + } + + @Test + fun testOnFailureReturnLogInStateFalseWithError() = runTest { + Mockito.`when`(mockUserLoginUseCase.anonymousUser()) + .thenReturn(flowOf(Resource.Error(loginFailureMessage, null))) + + val loginViewModel = LoginViewModel(loginRepository, mockUserLoginUseCase) + loginViewModel.authenticateAnonymousUser() + testDispatcher.scheduler.advanceUntilIdle() + val result = loginViewModel.loginState.getOrAwaitValue() + Assert.assertEquals(false, result.isLoggedIn) + Assert.assertEquals(false, result.isLoading) + Assert.assertEquals(loginFailureMessage, result.error) + } + + @Test + fun testOnLoadingReturnLogInStateFalse() = runTest { + Mockito.`when`(mockUserLoginUseCase.anonymousUser()) + .thenReturn(flowOf(Resource.Loading(null))) + + val loginViewModel = LoginViewModel(loginRepository, mockUserLoginUseCase) + loginViewModel.authenticateAnonymousUser() + testDispatcher.scheduler.advanceUntilIdle() + val result = loginViewModel.loginState.getOrAwaitValue() + Assert.assertEquals(true, result.isLoading) + Assert.assertEquals(false, result.isLoggedIn) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } +} diff --git a/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt b/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt index 494cf0c28ef656bd244607ac5b06fcf795c42e63..d12ea1fefaf52023b3a3e2b9b1a91bd93e28c8d1 100644 --- a/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt +++ b/modules/src/androidTest/java/app/lounge/AnonymousUserAPITest.kt @@ -75,9 +75,9 @@ class AnonymousUserAPITest { ) } -private const val testUserAgent: String = "{\"package\":\"foundation.e.apps.debug\",\"version\":\"2.5.5.debug\",\"device\":\"coral\",\"api\":32,\"os_version\":\"1.11-s-20230511288805-dev-coral\",\"build_id\":\"319e25cd.20230630224839\"}" +const val testUserAgent: String = "{\"package\":\"foundation.e.apps.debug\",\"version\":\"2.5.5.debug\",\"device\":\"coral\",\"api\":32,\"os_version\":\"1.11-s-20230511288805-dev-coral\",\"build_id\":\"319e25cd.20230630224839\"}" -private val testSystemProperties = Properties().apply { +val testSystemProperties = Properties().apply { setProperty("UserReadableName", "coral-default") setProperty("Build.HARDWARE", "coral") setProperty("Build.RADIO", "g8150-00123-220402-B-8399852")