diff --git a/app/src/main/java/foundation/e/apps/data/event/AppEvent.kt b/app/src/main/java/foundation/e/apps/data/event/AppEvent.kt index b12a12ea81131fdc98c0cfbf819508621060be88..0bc9160735f36cd869c50bb9dbadd92fc7d09f37 100644 --- a/app/src/main/java/foundation/e/apps/data/event/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/data/event/AppEvent.kt @@ -30,7 +30,6 @@ sealed class AppEvent(val data: Any) { class SignatureMissMatchError(packageName: String) : AppEvent(packageName) class UpdateEvent(result: ResultSupreme.WorkError) : AppEvent(result) - class InvalidAuthEvent(authName: String) : AppEvent(authName) class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId) class ErrorMessageDialogEvent(stringResourceId: Int) : AppEvent(stringResourceId) class AppPurchaseEvent(appInstall: AppInstall) : AppEvent(appInstall) diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 1c4e777d85d88d2120309e40afb4c90d43213ad5..c262c2cd87d8ede1c5f1f72cf1676efda5483482 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -176,7 +176,7 @@ class PlayStoreRepository @Inject constructor( getAppDetailsHelper().getAppByPackageName(packageNames) } - if (!isEmulator() && appDetails.all { it.versionCode == 0L } && isAnonymousUser()) { + if (!isEmulator() && appDetails.all { it.versionCode == 0L }) { // Google Play returns limited result ( i.e. version code being 0) with a stale token, // so we need to refresh authentication to get a new token. Timber.i("Version code is 0.") diff --git a/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt index caad7213ff40b7a6ed25d5a3fa0245316981acd6..bf54b65f79a4f7c6ee94fa7602c8813ccd5c0358 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt @@ -23,7 +23,6 @@ import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.network.IHttpClient import foundation.e.apps.data.event.AppEvent import foundation.e.apps.data.event.EventBus -import foundation.e.apps.data.login.core.AuthObject import foundation.e.apps.data.system.SystemInfoProvider import kotlinx.coroutines.MainScope import kotlinx.coroutines.asContextElement @@ -219,12 +218,6 @@ class GPlayHttpClient @Inject constructor( val responseBytes = response.body?.bytes() ?: byteArrayOf() when (code) { - STATUS_CODE_UNAUTHORIZED -> MainScope().launch { - EventBus.invokeEvent( - AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName) - ) - } - STATUS_CODE_TOO_MANY_REQUESTS -> MainScope().launch { if (url.toString().contains(SEARCH_SUGGEST)) { return@launch diff --git a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt index 3d44f6cdc3039b66805cb4621702486e363ed7dd..add8a202aee38341b3ac9cdd337e23bc00dc02b4 100644 --- a/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/LoginViewModel.kt @@ -36,7 +36,6 @@ import foundation.e.apps.login.MicrogAccountFetchResult import foundation.e.apps.login.MicrogAccountFetcher import foundation.e.apps.login.StoreAuthCoordinator import kotlinx.coroutines.launch -import okhttp3.Cache import javax.inject.Inject /** @@ -53,7 +52,6 @@ class LoginViewModel @Inject constructor( private val googleLoginUseCase: GoogleLoginUseCase, private val noGoogleLoginUseCase: NoGoogleLoginUseCase, private val logoutUseCase: LogoutUseCase, - private val cache: Cache, @ApplicationContext private val context: Context ) : ViewModel() { @@ -163,29 +161,6 @@ class LoginViewModel @Inject constructor( } } - /** - * Once an AuthObject is marked as invalid, it will be refreshed - * automatically by LoadingViewModel. - * If GPlay auth is invalid, LoadingViewModel.onLoadData has a retry block, - * this block will clear existing GPlay AuthData and freshly start the login flow. - */ - fun markInvalidAuthObject(authObjectName: String) { - val authObjectsLocal = authObjects.value?.toMutableList() - val invalidObject = authObjectsLocal?.find { it::class.java.simpleName == authObjectName } - - val replacedObject = invalidObject?.createInvalidAuthObject() - - authObjectsLocal?.apply { - if (invalidObject != null && replacedObject != null) { - remove(invalidObject) - add(replacedObject) - } - } - - authObjects.postValue(authObjectsLocal) - cache.evictAll() - } - /** * Clears all saved data and logs out the user to the sign in screen. */ 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 e972acd6d60a374a102360d914d562f90749deb2..f6e6472a32c93bda46ca9b45e06297b1dfd6ffaa 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -23,7 +23,6 @@ import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Bundle import android.view.View -import android.widget.Toast import android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts @@ -261,10 +260,6 @@ class MainActivity : AppCompatActivity() { private fun observeEvents() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - observeInvalidAuth() - } - launch { observeAppUnavailable() } @@ -520,19 +515,6 @@ class MainActivity : AppCompatActivity() { } } - private suspend fun observeInvalidAuth() { - EventBus.events.filter { appEvent -> - appEvent is AppEvent.InvalidAuthEvent - }.distinctUntilChanged { old, new -> - ((old.data is String) && (new.data is String) && old.data == new.data) - }.collectLatest { - if (BuildConfig.DEBUG) { - Toast.makeText(this, "Refreshing token...", Toast.LENGTH_SHORT).show() - } - validatedAuthObject(it) - } - } - private suspend fun observeAppUnavailable() { EventBus.events.filterIsInstance() .collectLatest { event -> @@ -542,13 +524,6 @@ class MainActivity : AppCompatActivity() { } } - private fun validatedAuthObject(appEvent: AppEvent) { - val data = appEvent.data as String - if (data.isNotBlank()) { - loginViewModel.markInvalidAuthObject(data) - } - } - private suspend fun observeTooManyRequests() { EventBus.events.filter { appEvent -> appEvent is AppEvent.TooManyRequests diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt index de36a579a46b40e7635b45feb4eae949e3a790ab..090e24edcf0c3e776b944d5cfe2ae6568e2b1b97 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt @@ -148,12 +148,6 @@ class ApplicationViewModel @Inject constructor( updateShareVisibilityState(app.shareUri.toString()) updateAppContentRatingState(packageName, app.contentRating) - - if (status != ResultStatus.OK) { - EventBus.invokeEvent( - AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName) - ) - } } catch (e: InternalException.AppNotFound) { _errorMessageLiveData.postValue(R.string.app_not_found) scheduleAutoRedirect() diff --git a/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt b/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt index 84d0b5ec8d2c7e37a9ee5e06c5ef2d5cac446538..44e9f26107da7f41dacd13e2e4b0be32135d4cc6 100644 --- a/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt +++ b/app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt @@ -304,6 +304,252 @@ class PlayStoreRepositoryTest { } } + @Test + fun `getAppDetails does not retry when token refresh fails`() = runTest { + val authData = AuthData(email = "user@gmail.com") + val playStoreAuthManager = mock() + + repository = createRepository(playStoreAuthManager) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(playStoreAuthManager.getValidatedAuthData()) + .thenReturn(ResultSupreme.Error("auth refresh failed")) + every { + anyConstructed().getAppByPackageName("pkg.test") + } throws GplayHttpRequestException(401, "unauthorized") + + kotlin.test.assertFailsWith { + repository.getAppDetails("pkg.test") + } + + mockitoVerify(playStoreAuthManager).getValidatedAuthData() + verify(exactly = 1) { anyConstructed().getAppByPackageName("pkg.test") } + } + + @Test + fun `getAppDetails does not retry when token refresh returns null auth data`() = runTest { + val authData = AuthData(email = "user@gmail.com") + val playStoreAuthManager = mock() + + repository = createRepository(playStoreAuthManager) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(playStoreAuthManager.getValidatedAuthData()) + .thenReturn(ResultSupreme.Success(null)) + every { + anyConstructed().getAppByPackageName("pkg.test") + } throws GplayHttpRequestException(401, "unauthorized") + + kotlin.test.assertFailsWith { + repository.getAppDetails("pkg.test") + } + + mockitoVerify(playStoreAuthManager).getValidatedAuthData() + verify(exactly = 1) { anyConstructed().getAppByPackageName("pkg.test") } + } + + @Test + fun `getAppDetails does not retry on non-auth errors`() = runTest { + val authData = AuthData(email = "user@gmail.com") + val playStoreAuthManager = mock() + + repository = createRepository(playStoreAuthManager) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + every { + anyConstructed().getAppByPackageName("pkg.test") + } throws GplayHttpRequestException(500, "server error") + + kotlin.test.assertFailsWith { + repository.getAppDetails("pkg.test") + } + + mockitoVerify(playStoreAuthManager, org.mockito.kotlin.never()).getValidatedAuthData() + verify(exactly = 1) { anyConstructed().getAppByPackageName("pkg.test") } + } + + @Test + fun `getAppDetails throws when version code is still zero after token refresh`() = runTest { + val authData = AuthData(email = "user@gmail.com") + val staleApp = App(packageName = "pkg.test", versionCode = 0) + val playStoreAuthManager = mock() + val storeAuthCoordinator = mock() + val playStoreAuthStore = createPlayStoreAuthStore(authData) + + repository = createRepository(playStoreAuthManager, playStoreAuthStore, storeAuthCoordinator) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(storeAuthCoordinator.fetchAuthObjects(listOf(StoreType.PLAY_STORE))) + .thenReturn(emptyList()) + every { + anyConstructed().getAppByPackageName("pkg.test") + } returns staleApp + + kotlin.test.assertFailsWith { + repository.getAppDetails("pkg.test") + } + + mockitoVerify(storeAuthCoordinator).fetchAuthObjects(listOf(StoreType.PLAY_STORE)) + } + + @Test + fun `getAppsDetails retries after 401`() = runTest { + val authData = AuthData(email = "user@gmail.com") + val apps = listOf(App(packageName = "pkg.test", versionCode = 2)) + val playStoreAuthManager = mock() + + repository = createRepository(playStoreAuthManager) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Success(authData)) + every { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } throws GplayHttpRequestException(401, "unauthorized") andThen apps + + val result = repository.getAppsDetails(listOf("pkg.test")) + + assertThat(result).hasSize(1) + mockitoVerify(playStoreAuthManager).getValidatedAuthData() + verify(exactly = 2) { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } + } + + @Test + fun `getAppsDetails does not retry when token refresh fails`() = runTest { + val authData = AuthData(email = "user@gmail.com") + val playStoreAuthManager = mock() + + repository = createRepository(playStoreAuthManager) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(playStoreAuthManager.getValidatedAuthData()) + .thenReturn(ResultSupreme.Error("auth refresh failed")) + every { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } throws GplayHttpRequestException(401, "unauthorized") + + kotlin.test.assertFailsWith { + repository.getAppsDetails(listOf("pkg.test")) + } + + mockitoVerify(playStoreAuthManager).getValidatedAuthData() + verify(exactly = 1) { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } + } + + @Test + fun `getAppsDetails refreshes auth for non-anonymous user when all version codes are zero`() = runTest { + val authData = AuthData(email = "user@gmail.com", isAnonymous = false) + val staleApps = listOf(App(packageName = "pkg.test", versionCode = 0)) + val refreshedApps = listOf(App(packageName = "pkg.test", versionCode = 5)) + val playStoreAuthManager = mock() + val storeAuthCoordinator = mock() + val playStoreAuthStore = createPlayStoreAuthStore(authData) + + repository = createRepository(playStoreAuthManager, playStoreAuthStore, storeAuthCoordinator) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(storeAuthCoordinator.fetchAuthObjects(listOf(StoreType.PLAY_STORE))) + .thenReturn(emptyList()) + every { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } returns staleApps andThen refreshedApps + + val result = repository.getAppsDetails(listOf("pkg.test")) + + assertThat(result).hasSize(1) + assertThat(result.first().package_name).isEqualTo("pkg.test") + mockitoVerify(storeAuthCoordinator).fetchAuthObjects(listOf(StoreType.PLAY_STORE)) + verify(exactly = 2) { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } + } + + @Test + fun `getAppsDetails does not refresh auth on emulator when all version codes are zero`() = runTest { + val authData = AuthData(email = "user@gmail.com", isAnonymous = false) + val staleApps = listOf(App(packageName = "pkg.test", versionCode = 0)) + val playStoreAuthManager = mock() + val storeAuthCoordinator = mock() + + every { SystemInfoProvider.getSystemProperty("ro.boot.qemu") } returns "1" + repository = createRepository( + playStoreAuthManager = playStoreAuthManager, + storeAuthCoordinator = storeAuthCoordinator + ) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + every { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } returns staleApps + + val result = repository.getAppsDetails(listOf("pkg.test")) + + assertThat(result).isEmpty() + mockitoVerify(storeAuthCoordinator, org.mockito.kotlin.never()) + .fetchAuthObjects(org.mockito.kotlin.any()) + verify(exactly = 1) { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } + } + + @Test + fun `getAppsDetails does not refresh auth when not all version codes are zero`() = runTest { + val authData = AuthData(email = "anon@example.com", isAnonymous = true) + val apps = listOf( + App(packageName = "pkg.a", versionCode = 0), + App(packageName = "pkg.b", versionCode = 5) + ) + val playStoreAuthManager = mock() + val storeAuthCoordinator = mock() + val playStoreAuthStore = createPlayStoreAuthStore(authData) + + repository = createRepository(playStoreAuthManager, playStoreAuthStore, storeAuthCoordinator) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + every { + anyConstructed().getAppByPackageName(listOf("pkg.a", "pkg.b")) + } returns apps + + val result = repository.getAppsDetails(listOf("pkg.a", "pkg.b")) + + assertThat(result).hasSize(1) + assertThat(result.first().package_name).isEqualTo("pkg.b") + mockitoVerify(storeAuthCoordinator, org.mockito.kotlin.never()) + .fetchAuthObjects(org.mockito.kotlin.any()) + verify(exactly = 1) { + anyConstructed().getAppByPackageName(listOf("pkg.a", "pkg.b")) + } + } + + @Test + fun `getAppsDetails returns empty result when version codes still zero after token refresh`() = runTest { + val authData = AuthData(email = "anon@example.com", isAnonymous = true) + val staleApps = listOf(App(packageName = "pkg.test", versionCode = 0)) + val playStoreAuthManager = mock() + val storeAuthCoordinator = mock() + val playStoreAuthStore = createPlayStoreAuthStore(authData) + + repository = createRepository(playStoreAuthManager, playStoreAuthStore, storeAuthCoordinator) + + whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) + whenever(storeAuthCoordinator.fetchAuthObjects(listOf(StoreType.PLAY_STORE))) + .thenReturn(emptyList()) + every { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } returns staleApps + + val result = repository.getAppsDetails(listOf("pkg.test")) + + assertThat(result).isEmpty() + mockitoVerify(storeAuthCoordinator).fetchAuthObjects(listOf(StoreType.PLAY_STORE)) + verify(exactly = 2) { + anyConstructed().getAppByPackageName(listOf("pkg.test")) + } + } + @Test fun `getAppsDetails filters invalid apps`() = runTest { val authData = AuthData(email = "user@gmail.com") diff --git a/app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt b/app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt index a846fe5450123034c0c23a9df79b9517bbfafd25..93b8662114eb303dc5e84bfbcc4986c3ea612362 100644 --- a/app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt +++ b/app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt @@ -19,14 +19,13 @@ package foundation.e.apps.gplay import com.aurora.gplayapi.data.models.PlayResponse -import foundation.e.apps.data.login.core.AuthObject +import foundation.e.apps.data.event.AppEvent +import foundation.e.apps.data.event.EventBus import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException +import foundation.e.apps.data.system.SystemInfoProvider import foundation.e.apps.util.FakeCall import foundation.e.apps.util.MainCoroutineRule -import foundation.e.apps.data.system.SystemInfoProvider -import foundation.e.apps.data.event.AppEvent -import foundation.e.apps.data.event.EventBus import io.mockk.every import io.mockk.mockkObject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -167,12 +166,8 @@ class GPlayHttpClientTest { Mockito.`when`(okHttpClient.newCall(any())).thenReturn(call) } - private suspend fun assertResponse(response: PlayResponse, statusValue: Int = 401) { + private fun assertResponse(response: PlayResponse, statusValue: Int = 401) { assertFalse(response.isSuccessful) assertTrue(response.code == statusValue) - val event = EventBus.events.first() - assertTrue(event is AppEvent.InvalidAuthEvent) - assertTrue(event.data is String) - assertTrue(event.data == AuthObject.GPlayAuth::class.java.simpleName) } } diff --git a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt index 0761fd5d92a72febde28b3ebd518e6b8a2b9d216..33dc2dcf76eb25218974fae786b4394fd92185a8 100644 --- a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt +++ b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt @@ -21,10 +21,7 @@ package foundation.e.apps.login import android.content.Context import android.content.Intent import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.R -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.login.core.AuthObject import foundation.e.apps.domain.login.AnonymousLoginUseCase import foundation.e.apps.domain.login.GoogleLoginUseCase import foundation.e.apps.domain.login.MicrogLoginUseCase @@ -38,7 +35,6 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import okhttp3.Cache import org.junit.After import org.junit.Before import org.junit.Rule @@ -68,8 +64,6 @@ class LoginViewModelTest { @Mock private lateinit var logoutUseCase: LogoutUseCase @Mock - private lateinit var cache: Cache - @Mock private lateinit var context: Context private lateinit var loginViewModel: LoginViewModel @@ -90,7 +84,6 @@ class LoginViewModelTest { googleLoginUseCase, noGoogleLoginUseCase, logoutUseCase, - cache, context ) whenever(context.getString(R.string.sign_in_microg_login_failed)).thenReturn("fallback") @@ -101,23 +94,6 @@ class LoginViewModelTest { Dispatchers.resetMain() } - @Test - fun testMarkInvalidAuthObject() { - val authObjectList = mutableListOf( - AuthObject.GPlayAuth( - ResultSupreme.Success(AuthData("aa@aa.com", "feri4234")), User.GOOGLE - ) - ) - loginViewModel.authObjects.value = authObjectList - - loginViewModel.markInvalidAuthObject(AuthObject.GPlayAuth::class.java.simpleName) - val currentAuthObjectList = loginViewModel.authObjects.value as List - val invalidGplayAuth = currentAuthObjectList.find { it is AuthObject.GPlayAuth } - - assert(invalidGplayAuth != null) - assert((invalidGplayAuth as AuthObject.GPlayAuth).result.isUnknownError()) - } - @Test fun `initialMicrogLogin saves user and starts flow on success`() = runTest { val result = MicrogAccountFetchResult.Success("user@gmail.com", "token")