From fb179108fdadf28bc523df713a8f018987f5599d Mon Sep 17 00:00:00 2001 From: Lukasz Macionczyk Date: Mon, 17 Jun 2024 13:04:17 +0200 Subject: [PATCH] Sign out on 401 from /subscriptions (#4650) Task/Issue URL: https://app.asana.com/0/1205648422731273/1207492658883339/f ### Description Sign user out when request to /subscriptions returns 401 response code. ### Steps to test this PR See task. ### No UI changes --- .../impl/SubscriptionsManager.kt | 22 ++++++++++++++----- .../SubscriptionMessagingInterface.kt | 2 +- .../impl/RealSubscriptionsManagerTest.kt | 20 +++++++++++++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt index 11f713a689b9..1b5eabaf98e4 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/SubscriptionsManager.kt @@ -96,7 +96,7 @@ interface SubscriptionsManager { /** * Fetches subscription and account data from the BE and stores it */ - suspend fun fetchAndStoreAllData(authToken: String? = null): Subscription? + suspend fun fetchAndStoreAllData(): Subscription? /** * Gets the subscription details from internal storage @@ -109,7 +109,7 @@ interface SubscriptionsManager { suspend fun getAccount(): Account? /** - * Exchanges the auth token for an access token and stores it + * Exchanges the auth token for an access token and stores both tokens */ suspend fun exchangeAuthToken(authToken: String): String @@ -375,15 +375,24 @@ class RealSubscriptionsManager @Inject constructor( override suspend fun exchangeAuthToken(authToken: String): String { val accessToken = authService.accessToken("Bearer $authToken").accessToken authRepository.setAccessToken(accessToken) + authRepository.saveAuthToken(authToken) return accessToken } - override suspend fun fetchAndStoreAllData(authToken: String?): Subscription? { + override suspend fun fetchAndStoreAllData(): Subscription? { try { - authToken?.let { authRepository.saveAuthToken(it) } if (!isUserAuthenticated()) return null - val token = (authToken ?: authRepository.getAccessToken()) ?: return null - val subscription = subscriptionsService.subscription("Bearer $token") + val token = checkNotNull(authRepository.getAccessToken()) { "Access token should not be null when user is authenticated." } + val subscription = try { + subscriptionsService.subscription("Bearer $token") + } catch (e: HttpException) { + if (e.code() == 401) { + logcat { "Token invalid, signing out" } + signOut() + return null + } + throw e + } val accountData = validateToken(token).account authRepository.saveExternalId(accountData.externalId) authRepository.saveSubscriptionData(subscription, accountData.entitlements.toEntitlements(), accountData.email) @@ -392,6 +401,7 @@ class RealSubscriptionsManager @Inject constructor( _isSignedIn.emit(isUserAuthenticated()) return authRepository.getSubscription() } catch (e: Exception) { + logcat { "Failed to fetch subscriptions data: ${e.stackTraceToString()}" } return null } } diff --git a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt index 179afc263894..9922728fd13d 100644 --- a/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt +++ b/subscriptions/subscriptions-impl/src/main/java/com/duckduckgo/subscriptions/impl/messaging/SubscriptionMessagingInterface.kt @@ -204,7 +204,7 @@ class SubscriptionMessagingInterface @Inject constructor( val token = jsMessage.params.getString("token") appCoroutineScope.launch(dispatcherProvider.io()) { subscriptionsManager.exchangeAuthToken(token) - subscriptionsManager.fetchAndStoreAllData(token) + subscriptionsManager.fetchAndStoreAllData() subscriptionsChecker.runChecker() pixelSender.reportRestoreUsingEmailSuccess() pixelSender.reportSubscriptionActivated() diff --git a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt index bbedebbf10cb..b923cea1bc18 100644 --- a/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt +++ b/subscriptions/subscriptions-impl/src/test/java/com/duckduckgo/subscriptions/impl/RealSubscriptionsManagerTest.kt @@ -37,6 +37,7 @@ import com.duckduckgo.subscriptions.impl.services.ValidateTokenResponse import com.duckduckgo.subscriptions.impl.store.SubscriptionsDataStore import java.lang.Exception import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -228,6 +229,21 @@ class RealSubscriptionsManagerTest { assertNull(subscriptionsManager.fetchAndStoreAllData()) } + @Test + fun whenFetchAndStoreAllDataIfSubscriptionFailsWith401ThenSignOutAndReturnNull() = runTest { + givenUserIsAuthenticated() + givenSubscriptionFails(httpResponseCode = 401) + + val subscription = subscriptionsManager.fetchAndStoreAllData() + + assertNull(subscription) + assertFalse(subscriptionsManager.isSignedIn.first()) + assertNull(subscriptionsManager.getSubscription()) + assertNull(subscriptionsManager.getAccount()) + assertNull(authRepository.getAuthToken()) + assertNull(authRepository.getAccessToken()) + } + @Test fun whenPurchaseFlowIfUserNotAuthenticatedAndNotPurchaseStoredThenCreateAccount() = runTest { givenUserIsNotAuthenticated() @@ -974,9 +990,9 @@ class RealSubscriptionsManagerTest { whenever(subscriptionsService.portal(any())).thenThrow(HttpException(Response.error(400, exception))) } - private suspend fun givenSubscriptionFails() { + private suspend fun givenSubscriptionFails(httpResponseCode: Int = 400) { val exception = "failure".toResponseBody("text/json".toMediaTypeOrNull()) - whenever(subscriptionsService.subscription(any())).thenThrow(HttpException(Response.error(400, exception))) + whenever(subscriptionsService.subscription(any())).thenThrow(HttpException(Response.error(httpResponseCode, exception))) } private suspend fun givenSubscriptionSucceedsWithoutEntitlements(status: String = "Auto-Renewable") {