Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6177fa91 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix: Serialize Play auth refreshes and restore cached auth

parent 6a2faa78
Loading
Loading
Loading
Loading
+96 −19
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package foundation.e.apps.data.login.repository
import com.aurora.gplayapi.data.models.AuthData
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.login.core.AuthObject
import foundation.e.apps.data.login.core.StoreAuthResult
import foundation.e.apps.data.login.core.StoreAuthenticator
import foundation.e.apps.data.login.core.StoreType
import foundation.e.apps.data.login.exceptions.GPlayLoginException
@@ -27,6 +28,8 @@ import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.preferences.SessionRepository
import foundation.e.apps.login.PlayStoreAuthManager
import foundation.e.apps.login.StoreAuthCoordinator
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
import javax.inject.Singleton

@@ -37,43 +40,117 @@ class AuthenticatorRepository @Inject constructor(
    private val sessionRepository: SessionRepository,
    private val playStoreAuthStore: PlayStoreAuthStore,
) : PlayStoreAuthManager, StoreAuthCoordinator {
    private val playStoreAuthMutex = Mutex()

    override suspend fun getGPlayAuthOrThrow(): AuthData {
        return playStoreAuthStore.awaitAuthData()
            ?: throw GPlayLoginException(
                false,
                "AuthData is not available",
                sessionRepository.awaitUser()
            )
        return playStoreAuthMutex.withLock {
            playStoreAuthStore.awaitAuthData()
                ?: fetchMissingPlayStoreAuthLocked()
                ?: throw missingAuthDataException()
        }
    }

    override suspend fun setGPlayAuth(auth: AuthData) {
        playStoreAuthMutex.withLock {
            playStoreAuthStore.saveAuthData(auth)
        }
    }

    override suspend fun fetchAuthObjects(authTypes: List<StoreType>): List<AuthObject> {
        val authObjectsLocal = ArrayList<AuthObject>()

        for (authenticator in authenticators) {
            if (!authenticator.isStoreActive()) continue

            val authResult = if (authenticator.storeType == StoreType.PLAY_STORE) {
                playStoreAuthMutex.withLock {
                    fetchPlayStoreAuthLocked(
                        authenticator = authenticator,
                        forceRefresh = authenticator.storeType in authTypes
                    )
                }
            } else {
                if (authenticator.storeType in authTypes) {
                    authenticator.logout()
                }

            val authResult = authenticator.login()
                authenticator.login()
            }
            authObjectsLocal.add(authResult.authObject)
            authResult.authDataToPersist?.let { playStoreAuthStore.saveAuthData(it) }
        }

        return authObjectsLocal
    }

    override suspend fun getValidatedAuthData(): ResultSupreme<AuthData?> {
        val authenticator = authenticators.firstOrNull { it.storeType == StoreType.PLAY_STORE }
            ?: return ResultSupreme.Error("Play Store authenticator not available")
        authenticator.logout()
        val authResult = authenticator.login()
        authResult.authDataToPersist?.let { playStoreAuthStore.saveAuthData(it) }
        return playStoreAuthMutex.withLock {
            val authenticator = getPlayStoreAuthenticator()
                ?: return@withLock ResultSupreme.Error<AuthData?>(
                    "Play Store authenticator not available"
                )
            val authResult = fetchPlayStoreAuthLocked(
                authenticator = authenticator,
                forceRefresh = true
            )
            val authObject = authResult.authObject as? AuthObject.GPlayAuth
        return authObject?.result ?: ResultSupreme.Error("Play Store auth unavailable")
            authObject?.result ?: ResultSupreme.Error<AuthData?>("Play Store auth unavailable")
        }
    }

    private suspend fun fetchMissingPlayStoreAuthLocked(): AuthData? {
        val authenticator = getPlayStoreAuthenticator() ?: return null
        val authResult = fetchPlayStoreAuthLocked(
            authenticator = authenticator,
            forceRefresh = false
        )
        return authResult.gPlayAuthData() ?: playStoreAuthStore.awaitAuthData()
    }

    private suspend fun fetchPlayStoreAuthLocked(
        authenticator: StoreAuthenticator,
        forceRefresh: Boolean
    ): StoreAuthResult {
        val previousAuthData = playStoreAuthStore.awaitAuthData()
        if (forceRefresh) {
            authenticator.logout()
        }

        return try {
            authenticator.login().also { authResult ->
                val authData = authResult.authDataToPersist ?: authResult.gPlayAuthData()
                if (authData != null) {
                    playStoreAuthStore.saveAuthData(authData)
                } else if (forceRefresh) {
                    restorePreviousAuthData(previousAuthData)
                }
            }
        } catch (exception: Exception) {
            if (forceRefresh) {
                restorePreviousAuthData(previousAuthData)
            }
            throw exception
        }
    }

    private suspend fun restorePreviousAuthData(authData: AuthData?) {
        if (authData != null && playStoreAuthStore.awaitAuthData() == null) {
            playStoreAuthStore.saveAuthData(authData)
        }
    }

    private fun getPlayStoreAuthenticator(): StoreAuthenticator? {
        return authenticators.firstOrNull { it.storeType == StoreType.PLAY_STORE }
    }

    private fun StoreAuthResult.gPlayAuthData(): AuthData? {
        val gPlayAuthObject = authObject as? AuthObject.GPlayAuth
        return gPlayAuthObject?.result?.data
    }

    private suspend fun missingAuthDataException(): GPlayLoginException {
        return GPlayLoginException(
            false,
            "AuthData is not available",
            sessionRepository.awaitUser()
        )
    }
}
+61 −0
Original line number Diff line number Diff line
@@ -69,6 +69,34 @@ class AuthenticatorRepositoryTest {
        assertThat(result).isEqualTo(authData)
    }

    @Test
    fun getGPlayAuthOrThrow_fetchesAndPersistsPlayAuthWhenMissing() = runTest {
        val authData = AuthData(email = "anonymous@example.com", isAnonymous = true)
        val authObject = AuthObject.GPlayAuth(ResultSupreme.Success(authData), User.ANONYMOUS)
        val storeResult = StoreAuthResult(authObject, authData)
        val inMemoryStore = InMemoryAuthStore(
            currentAuthData = null,
            persistedAuthData = null,
            currentUser = User.ANONYMOUS,
            persistedUser = User.ANONYMOUS,
        )
        val playAuthenticator = mockk<StoreAuthenticator>()

        every { playAuthenticator.storeType } returns StoreType.PLAY_STORE
        coEvery { playAuthenticator.login() } returns storeResult
        val repository = AuthenticatorRepository(
            listOf(playAuthenticator),
            inMemoryStore,
            inMemoryStore
        )

        val result = repository.getGPlayAuthOrThrow()

        assertThat(result).isEqualTo(authData)
        assertThat(inMemoryStore.awaitAuthData()).isEqualTo(authData)
        coVerify(exactly = 0) { playAuthenticator.logout() }
    }

    @Test
    fun fetchAuthObjects_logsOutAndPersistsAuthData() = runTest {
        val authData = AuthData(email = "user@example.com")
@@ -138,6 +166,39 @@ class AuthenticatorRepositoryTest {
        assertThat(inMemoryStore.awaitAuthData()).isEqualTo(authData)
    }

    @Test
    fun getValidatedAuthData_restoresPreviousAuthWhenRefreshFails() = runTest {
        val previousAuthData = AuthData(email = "old@example.com")
        val authObject = AuthObject.GPlayAuth(
            ResultSupreme.Error<AuthData?>("refresh failed"),
            User.ANONYMOUS
        )
        val storeResult = StoreAuthResult(authObject)
        val inMemoryStore = InMemoryAuthStore(
            currentAuthData = previousAuthData,
            persistedAuthData = previousAuthData,
            currentUser = User.ANONYMOUS,
            persistedUser = User.ANONYMOUS,
        )
        val playAuthenticator = mockk<StoreAuthenticator>()

        every { playAuthenticator.storeType } returns StoreType.PLAY_STORE
        coEvery { playAuthenticator.logout() } coAnswers {
            inMemoryStore.saveAuthData(null)
        }
        coEvery { playAuthenticator.login() } returns storeResult
        val repository = AuthenticatorRepository(
            listOf(playAuthenticator),
            inMemoryStore,
            inMemoryStore
        )

        val result = repository.getValidatedAuthData()

        assertThat(result).isInstanceOf(ResultSupreme.Error::class.java)
        assertThat(inMemoryStore.awaitAuthData()).isEqualTo(previousAuthData)
    }

    private class InMemoryAuthStore(
        currentAuthData: AuthData? = null,
        persistedAuthData: AuthData? = currentAuthData,