Skip to content

Commit

Permalink
Sign out on 401 from /subscriptions (#4650)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
lmac012 authored Jun 17, 2024
1 parent 702c589 commit fb17910
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -974,9 +990,9 @@ class RealSubscriptionsManagerTest {
whenever(subscriptionsService.portal(any())).thenThrow(HttpException(Response.error<String>(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<String>(400, exception)))
whenever(subscriptionsService.subscription(any())).thenThrow(HttpException(Response.error<String>(httpResponseCode, exception)))
}

private suspend fun givenSubscriptionSucceedsWithoutEntitlements(status: String = "Auto-Renewable") {
Expand Down

0 comments on commit fb17910

Please sign in to comment.