Loading app/src/main/java/foundation/e/apps/data/login/repository/AuthenticatorRepository.kt +96 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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() ) } } app/src/test/java/foundation/e/apps/data/login/repository/AuthenticatorRepositoryTest.kt +61 −0 Original line number Diff line number Diff line Loading @@ -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") Loading Loading @@ -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, Loading Loading
app/src/main/java/foundation/e/apps/data/login/repository/AuthenticatorRepository.kt +96 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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() ) } }
app/src/test/java/foundation/e/apps/data/login/repository/AuthenticatorRepositoryTest.kt +61 −0 Original line number Diff line number Diff line Loading @@ -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") Loading Loading @@ -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, Loading