Loading app/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +2 −2 Original line number Diff line number Diff line Loading @@ -52,8 +52,8 @@ class MicrogLoginManager @Inject constructor( override suspend fun login(): AuthData? { val oldToken = playStoreAuthStore.awaitOauthToken() val shouldRefresh = hasMicrogAccount() && oldToken.startsWith(MICROG_TOKEN_PREFIX) val oauthToken = if (shouldRefresh) { val shouldRefreshToken = hasMicrogAccount() && oldToken.startsWith(MICROG_TOKEN_PREFIX) val oauthToken = if (shouldRefreshToken) { fetchRefreshedToken(oldToken) } else { oldToken Loading app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +25 −20 Original line number Diff line number Diff line Loading @@ -209,7 +209,7 @@ class PlayStoreRepository @Inject constructor( throw exception } if (!isEmulator() && appDetails.versionCode == 0L && isAnonymousUser()) { if (!isEmulator() && appDetails.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 for ${appDetails.packageName}.") Loading Loading @@ -242,14 +242,18 @@ class PlayStoreRepository @Inject constructor( } private suspend fun <T> retryOnUnauthorized(block: suspend () -> T): T { return try { return withContext(gPlayHttpClient.requestResponseCodeContext) { try { gPlayHttpClient.resetResponseCode() block() } catch (exception: Exception) { val requestResponseCode = gPlayHttpClient.getRequestResponseCode() val isUnauthorized: Boolean = when (exception) { is GplayHttpRequestException -> exception.status == HttpURLConnection.HTTP_UNAUTHORIZED exception.status == HttpURLConnection.HTTP_UNAUTHORIZED || requestResponseCode == HttpURLConnection.HTTP_UNAUTHORIZED is InternalException.AppNotFound -> gPlayHttpClient.responseCode.value == HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode == HttpURLConnection.HTTP_UNAUTHORIZED else -> false } Loading @@ -266,6 +270,7 @@ class PlayStoreRepository @Inject constructor( block() } } } suspend fun getAppDetailsWeb(packageName: String): Application? { val webAppDetailsHelper = WebAppDetailsHelper().using(gPlayHttpClient) Loading app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +21 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ 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 import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow Loading @@ -46,6 +47,7 @@ import java.net.SocketTimeoutException import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.coroutines.CoroutineContext class GPlayHttpClient @Inject constructor( loggingInterceptor: HttpLoggingInterceptor Loading @@ -65,6 +67,9 @@ class GPlayHttpClient @Inject constructor( } private val _responseCode = MutableStateFlow(INITIAL_RESPONSE_CODE) private val requestResponseCode = ThreadLocal.withInitial { RequestResponseCodeState(INITIAL_RESPONSE_CODE) } override val responseCode: StateFlow<Int> get() = _responseCode.asStateFlow() Loading Loading @@ -168,9 +173,21 @@ class GPlayHttpClient @Inject constructor( return headersWithLocale } fun resetResponseCode() { _responseCode.value = 0 checkNotNull(requestResponseCode.get()).value = 0 } fun getRequestResponseCode(): Int { return checkNotNull(requestResponseCode.get()).value } val requestResponseCodeContext: CoroutineContext get() = requestResponseCode.asContextElement(RequestResponseCodeState(0)) private fun processRequest(request: Request): PlayResponse { // Reset response code as flow doesn't sends the same value twice _responseCode.value = 0 resetResponseCode() var response: Response? = null return try { val call = okHttpClient.newCall(request) Loading Loading @@ -237,8 +254,11 @@ class GPlayHttpClient @Inject constructor( responseBytes = responseBytes, ).withErrorString(errorMessage).apply { _responseCode.value = response.code checkNotNull(requestResponseCode.get()).value = response.code } } } private data class RequestResponseCodeState(var value: Int) class GplayHttpRequestException(val status: Int, message: String) : Exception(message) app/src/test/java/foundation/e/apps/data/login/MicrogLoginManagerTest.kt +24 −0 Original line number Diff line number Diff line Loading @@ -244,6 +244,30 @@ class MicrogLoginManagerTest { org.mockito.kotlin.verify(playStoreAuthStore).saveAasToken("") } @Test fun `login uses existing oauth token when microg account exists but token is not a microg token`() = runBlocking { val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) val accountManager = mock<AccountManager>() whenever(accountManager.getAccountsByType(eq(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE))) .thenReturn(arrayOf(account)) val oauthAuthDataBuilder = mock<OauthAuthDataBuilder>() val playStoreAuthStore = mock<PlayStoreAuthStore>() whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("non-ya29-stale-token") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) val authData = com.aurora.gplayapi.data.models.AuthData(email = account.name) whenever(oauthAuthDataBuilder.build("non-ya29-stale-token")).thenReturn(authData) val result = buildMicrogLoginManager(accountManager, oauthAuthDataBuilder, playStoreAuthStore).login() assertEquals(authData, result) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) .saveGoogleLogin(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) .saveAasToken(any()) } @Test fun `login throws when token missing`() { val accountManager = mock<AccountManager>() Loading app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt +124 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.web.WebTopChartsHelper import com.google.common.truth.Truth.assertThat import foundation.e.apps.R import java.net.HttpURLConnection import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.ApplicationRepository Loading Loading @@ -46,6 +47,7 @@ import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.mockito.kotlin.verify as mockitoVerify import kotlin.coroutines.EmptyCoroutineContext @RunWith(RobolectricTestRunner::class) @Config(sdk = [30]) Loading @@ -55,12 +57,23 @@ class PlayStoreRepositoryTest { private val gPlayHttpClient = mockk<GPlayHttpClient>() private val applicationDataManager = mockk<ApplicationDataManager>(relaxed = true) private val playStoreSearchHelper = mockk<PlayStoreSearchHelper>() private lateinit var responseCodeFlow: MutableStateFlow<Int> private var requestResponseCode: Int = 0 private lateinit var repository: PlayStoreRepository @Before fun setUp() { mockkObject(SystemInfoProvider) every { SystemInfoProvider.getSystemProperty("ro.boot.qemu") } returns "0" responseCodeFlow = MutableStateFlow(0) requestResponseCode = 0 every { gPlayHttpClient.responseCode } returns responseCodeFlow every { gPlayHttpClient.resetResponseCode() } answers { responseCodeFlow.value = 0 requestResponseCode = 0 } every { gPlayHttpClient.getRequestResponseCode() } answers { requestResponseCode } every { gPlayHttpClient.requestResponseCodeContext } returns EmptyCoroutineContext mockkConstructor(AppDetailsHelper::class) mockkConstructor(WebTopChartsHelper::class) every { anyConstructed<AppDetailsHelper>().using(gPlayHttpClient) } answers { Loading Loading @@ -133,6 +146,94 @@ class PlayStoreRepositoryTest { verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails refreshes auth for google user when version code is zero`() = runTest { val authData = AuthData(email = "user@gmail.com", isAnonymous = false) val staleApp = App(packageName = "pkg.test", versionCode = 0) val refreshedApp = App(packageName = "pkg.test", versionCode = 4) val playStoreAuthManager = mock<PlayStoreAuthManager>() val storeAuthCoordinator = mock<StoreAuthCoordinator>() 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<AppDetailsHelper>().getAppByPackageName("pkg.test") } returns staleApp andThen refreshedApp val result = repository.getAppDetails("pkg.test") assertThat(result.latest_version_code).isEqualTo(4) mockitoVerify(storeAuthCoordinator).fetchAuthObjects(listOf(StoreType.PLAY_STORE)) verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails retries after auth refresh when gplay returns 404 with underlying 401`() = runTest { val authData = AuthData(email = "user@gmail.com") val app = App(packageName = "pkg.test", versionCode = 2) val playStoreAuthManager = mock<PlayStoreAuthManager>() var requestCount = 0 repository = createRepository(playStoreAuthManager) whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Success(authData)) every { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } answers { requestCount += 1 if (requestCount == 1) { responseCodeFlow.value = HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode = HttpURLConnection.HTTP_UNAUTHORIZED throw GplayHttpRequestException(404, "not found") } app } val result = repository.getAppDetails("pkg.test") assertThat(result.package_name).isEqualTo("pkg.test") mockitoVerify(playStoreAuthManager).getValidatedAuthData() verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails retries after auth refresh when gplay returns 404 with underlying 401 for anonymous user`() = runTest { val authData = AuthData(email = "anon@example.com", isAnonymous = true) val app = App(packageName = "pkg.test", versionCode = 2) val playStoreAuthManager = mock<PlayStoreAuthManager>() var requestCount = 0 repository = createRepository(playStoreAuthManager) whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Success(authData)) every { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } answers { requestCount += 1 if (requestCount == 1) { responseCodeFlow.value = HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode = HttpURLConnection.HTTP_UNAUTHORIZED throw GplayHttpRequestException(404, "not found") } app } val result = repository.getAppDetails("pkg.test") assertThat(result.package_name).isEqualTo("pkg.test") mockitoVerify(playStoreAuthManager).getValidatedAuthData() verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails throws AppNotFound on 404`() = runTest { val authData = AuthData(email = "user@gmail.com") Loading @@ -152,6 +253,29 @@ class PlayStoreRepositoryTest { assertThat(exception).isInstanceOf(InternalException.AppNotFound::class.java) } @Test fun `getAppDetails does not retry on true 404 when a previous call left 401 in state`() = runTest { val authData = AuthData(email = "user@gmail.com") val playStoreAuthManager = mock<PlayStoreAuthManager>() repository = createRepository(playStoreAuthManager) whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) responseCodeFlow.value = HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode = 0 every { anyConstructed<AppDetailsHelper>().getAppByPackageName("missing.pkg") } throws GplayHttpRequestException(404, "not found") val exception = kotlin.test.assertFailsWith<InternalException.AppNotFound> { repository.getAppDetails("missing.pkg") } assertThat(exception).isInstanceOf(InternalException.AppNotFound::class.java) mockitoVerify(playStoreAuthManager, org.mockito.kotlin.never()).getValidatedAuthData() verify(exactly = 1) { anyConstructed<AppDetailsHelper>().getAppByPackageName("missing.pkg") } } @Test fun `getAppsDetails refreshes auth when anonymous and all versions zero`() = runTest { val authData = AuthData(email = "anon@example.com", isAnonymous = true) Loading Loading
app/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +2 −2 Original line number Diff line number Diff line Loading @@ -52,8 +52,8 @@ class MicrogLoginManager @Inject constructor( override suspend fun login(): AuthData? { val oldToken = playStoreAuthStore.awaitOauthToken() val shouldRefresh = hasMicrogAccount() && oldToken.startsWith(MICROG_TOKEN_PREFIX) val oauthToken = if (shouldRefresh) { val shouldRefreshToken = hasMicrogAccount() && oldToken.startsWith(MICROG_TOKEN_PREFIX) val oauthToken = if (shouldRefreshToken) { fetchRefreshedToken(oldToken) } else { oldToken Loading
app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +25 −20 Original line number Diff line number Diff line Loading @@ -209,7 +209,7 @@ class PlayStoreRepository @Inject constructor( throw exception } if (!isEmulator() && appDetails.versionCode == 0L && isAnonymousUser()) { if (!isEmulator() && appDetails.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 for ${appDetails.packageName}.") Loading Loading @@ -242,14 +242,18 @@ class PlayStoreRepository @Inject constructor( } private suspend fun <T> retryOnUnauthorized(block: suspend () -> T): T { return try { return withContext(gPlayHttpClient.requestResponseCodeContext) { try { gPlayHttpClient.resetResponseCode() block() } catch (exception: Exception) { val requestResponseCode = gPlayHttpClient.getRequestResponseCode() val isUnauthorized: Boolean = when (exception) { is GplayHttpRequestException -> exception.status == HttpURLConnection.HTTP_UNAUTHORIZED exception.status == HttpURLConnection.HTTP_UNAUTHORIZED || requestResponseCode == HttpURLConnection.HTTP_UNAUTHORIZED is InternalException.AppNotFound -> gPlayHttpClient.responseCode.value == HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode == HttpURLConnection.HTTP_UNAUTHORIZED else -> false } Loading @@ -266,6 +270,7 @@ class PlayStoreRepository @Inject constructor( block() } } } suspend fun getAppDetailsWeb(packageName: String): Application? { val webAppDetailsHelper = WebAppDetailsHelper().using(gPlayHttpClient) Loading
app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +21 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ 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 import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow Loading @@ -46,6 +47,7 @@ import java.net.SocketTimeoutException import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.coroutines.CoroutineContext class GPlayHttpClient @Inject constructor( loggingInterceptor: HttpLoggingInterceptor Loading @@ -65,6 +67,9 @@ class GPlayHttpClient @Inject constructor( } private val _responseCode = MutableStateFlow(INITIAL_RESPONSE_CODE) private val requestResponseCode = ThreadLocal.withInitial { RequestResponseCodeState(INITIAL_RESPONSE_CODE) } override val responseCode: StateFlow<Int> get() = _responseCode.asStateFlow() Loading Loading @@ -168,9 +173,21 @@ class GPlayHttpClient @Inject constructor( return headersWithLocale } fun resetResponseCode() { _responseCode.value = 0 checkNotNull(requestResponseCode.get()).value = 0 } fun getRequestResponseCode(): Int { return checkNotNull(requestResponseCode.get()).value } val requestResponseCodeContext: CoroutineContext get() = requestResponseCode.asContextElement(RequestResponseCodeState(0)) private fun processRequest(request: Request): PlayResponse { // Reset response code as flow doesn't sends the same value twice _responseCode.value = 0 resetResponseCode() var response: Response? = null return try { val call = okHttpClient.newCall(request) Loading Loading @@ -237,8 +254,11 @@ class GPlayHttpClient @Inject constructor( responseBytes = responseBytes, ).withErrorString(errorMessage).apply { _responseCode.value = response.code checkNotNull(requestResponseCode.get()).value = response.code } } } private data class RequestResponseCodeState(var value: Int) class GplayHttpRequestException(val status: Int, message: String) : Exception(message)
app/src/test/java/foundation/e/apps/data/login/MicrogLoginManagerTest.kt +24 −0 Original line number Diff line number Diff line Loading @@ -244,6 +244,30 @@ class MicrogLoginManagerTest { org.mockito.kotlin.verify(playStoreAuthStore).saveAasToken("") } @Test fun `login uses existing oauth token when microg account exists but token is not a microg token`() = runBlocking { val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) val accountManager = mock<AccountManager>() whenever(accountManager.getAccountsByType(eq(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE))) .thenReturn(arrayOf(account)) val oauthAuthDataBuilder = mock<OauthAuthDataBuilder>() val playStoreAuthStore = mock<PlayStoreAuthStore>() whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("non-ya29-stale-token") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) val authData = com.aurora.gplayapi.data.models.AuthData(email = account.name) whenever(oauthAuthDataBuilder.build("non-ya29-stale-token")).thenReturn(authData) val result = buildMicrogLoginManager(accountManager, oauthAuthDataBuilder, playStoreAuthStore).login() assertEquals(authData, result) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) .saveGoogleLogin(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) .saveAasToken(any()) } @Test fun `login throws when token missing`() { val accountManager = mock<AccountManager>() Loading
app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt +124 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.web.WebTopChartsHelper import com.google.common.truth.Truth.assertThat import foundation.e.apps.R import java.net.HttpURLConnection import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.ApplicationRepository Loading Loading @@ -46,6 +47,7 @@ import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.mockito.kotlin.verify as mockitoVerify import kotlin.coroutines.EmptyCoroutineContext @RunWith(RobolectricTestRunner::class) @Config(sdk = [30]) Loading @@ -55,12 +57,23 @@ class PlayStoreRepositoryTest { private val gPlayHttpClient = mockk<GPlayHttpClient>() private val applicationDataManager = mockk<ApplicationDataManager>(relaxed = true) private val playStoreSearchHelper = mockk<PlayStoreSearchHelper>() private lateinit var responseCodeFlow: MutableStateFlow<Int> private var requestResponseCode: Int = 0 private lateinit var repository: PlayStoreRepository @Before fun setUp() { mockkObject(SystemInfoProvider) every { SystemInfoProvider.getSystemProperty("ro.boot.qemu") } returns "0" responseCodeFlow = MutableStateFlow(0) requestResponseCode = 0 every { gPlayHttpClient.responseCode } returns responseCodeFlow every { gPlayHttpClient.resetResponseCode() } answers { responseCodeFlow.value = 0 requestResponseCode = 0 } every { gPlayHttpClient.getRequestResponseCode() } answers { requestResponseCode } every { gPlayHttpClient.requestResponseCodeContext } returns EmptyCoroutineContext mockkConstructor(AppDetailsHelper::class) mockkConstructor(WebTopChartsHelper::class) every { anyConstructed<AppDetailsHelper>().using(gPlayHttpClient) } answers { Loading Loading @@ -133,6 +146,94 @@ class PlayStoreRepositoryTest { verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails refreshes auth for google user when version code is zero`() = runTest { val authData = AuthData(email = "user@gmail.com", isAnonymous = false) val staleApp = App(packageName = "pkg.test", versionCode = 0) val refreshedApp = App(packageName = "pkg.test", versionCode = 4) val playStoreAuthManager = mock<PlayStoreAuthManager>() val storeAuthCoordinator = mock<StoreAuthCoordinator>() 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<AppDetailsHelper>().getAppByPackageName("pkg.test") } returns staleApp andThen refreshedApp val result = repository.getAppDetails("pkg.test") assertThat(result.latest_version_code).isEqualTo(4) mockitoVerify(storeAuthCoordinator).fetchAuthObjects(listOf(StoreType.PLAY_STORE)) verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails retries after auth refresh when gplay returns 404 with underlying 401`() = runTest { val authData = AuthData(email = "user@gmail.com") val app = App(packageName = "pkg.test", versionCode = 2) val playStoreAuthManager = mock<PlayStoreAuthManager>() var requestCount = 0 repository = createRepository(playStoreAuthManager) whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Success(authData)) every { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } answers { requestCount += 1 if (requestCount == 1) { responseCodeFlow.value = HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode = HttpURLConnection.HTTP_UNAUTHORIZED throw GplayHttpRequestException(404, "not found") } app } val result = repository.getAppDetails("pkg.test") assertThat(result.package_name).isEqualTo("pkg.test") mockitoVerify(playStoreAuthManager).getValidatedAuthData() verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails retries after auth refresh when gplay returns 404 with underlying 401 for anonymous user`() = runTest { val authData = AuthData(email = "anon@example.com", isAnonymous = true) val app = App(packageName = "pkg.test", versionCode = 2) val playStoreAuthManager = mock<PlayStoreAuthManager>() var requestCount = 0 repository = createRepository(playStoreAuthManager) whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) whenever(playStoreAuthManager.getValidatedAuthData()).thenReturn(ResultSupreme.Success(authData)) every { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } answers { requestCount += 1 if (requestCount == 1) { responseCodeFlow.value = HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode = HttpURLConnection.HTTP_UNAUTHORIZED throw GplayHttpRequestException(404, "not found") } app } val result = repository.getAppDetails("pkg.test") assertThat(result.package_name).isEqualTo("pkg.test") mockitoVerify(playStoreAuthManager).getValidatedAuthData() verify(exactly = 2) { anyConstructed<AppDetailsHelper>().getAppByPackageName("pkg.test") } } @Test fun `getAppDetails throws AppNotFound on 404`() = runTest { val authData = AuthData(email = "user@gmail.com") Loading @@ -152,6 +253,29 @@ class PlayStoreRepositoryTest { assertThat(exception).isInstanceOf(InternalException.AppNotFound::class.java) } @Test fun `getAppDetails does not retry on true 404 when a previous call left 401 in state`() = runTest { val authData = AuthData(email = "user@gmail.com") val playStoreAuthManager = mock<PlayStoreAuthManager>() repository = createRepository(playStoreAuthManager) whenever(playStoreAuthManager.getGPlayAuthOrThrow()).thenReturn(authData) responseCodeFlow.value = HttpURLConnection.HTTP_UNAUTHORIZED requestResponseCode = 0 every { anyConstructed<AppDetailsHelper>().getAppByPackageName("missing.pkg") } throws GplayHttpRequestException(404, "not found") val exception = kotlin.test.assertFailsWith<InternalException.AppNotFound> { repository.getAppDetails("missing.pkg") } assertThat(exception).isInstanceOf(InternalException.AppNotFound::class.java) mockitoVerify(playStoreAuthManager, org.mockito.kotlin.never()).getValidatedAuthData() verify(exactly = 1) { anyConstructed<AppDetailsHelper>().getAppByPackageName("missing.pkg") } } @Test fun `getAppsDetails refreshes auth when anonymous and all versions zero`() = runTest { val authData = AuthData(email = "anon@example.com", isAnonymous = true) Loading