Loading data/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +15 −3 Original line number Diff line number Diff line Loading @@ -32,10 +32,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.data.login.api.MicrogAccountFetcher import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.domain.auth.PlayStoreLoginMode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton Loading Loading @@ -100,7 +103,7 @@ class MicrogLoginManager @Inject constructor( false, null, null ).result ).getResult(MICROG_TOKEN_FETCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) val intent = bundle.parcelableCompat(AccountManager.KEY_INTENT, Intent::class.java) if (intent != null) { Loading Loading @@ -154,11 +157,19 @@ class MicrogLoginManager @Inject constructor( playStoreAuthStore.saveAasToken("") result.oauthToken } is MicrogAccountFetchResult.RequiresUserAction -> error(MICROG_TOKEN_REFRESH_FAILURE) is MicrogAccountFetchResult.Error -> error(MICROG_TOKEN_REFRESH_FAILURE) is MicrogAccountFetchResult.RequiresUserAction -> throwMicrogRefreshFailure() is MicrogAccountFetchResult.Error -> throwMicrogRefreshFailure() } } private fun throwMicrogRefreshFailure(): Nothing { throw GPlayLoginException( isTimeout = false, message = MICROG_TOKEN_REFRESH_FAILURE, loginMode = PlayStoreLoginMode.MICROG, ) } private fun resolveAccount(accounts: Array<Account>, accountName: String): Account { return if (accountName.isNotBlank()) { accounts.firstOrNull { it.name == accountName } ?: accounts.first() Loading Loading @@ -189,5 +200,6 @@ class MicrogLoginManager @Inject constructor( const val MICROG_TOKEN_PREFIX = "ya29." const val MICROG_TOKEN_MISSING = "MicroG token is missing" const val MICROG_TOKEN_REFRESH_FAILURE = "MicroG refresh failed" const val MICROG_TOKEN_FETCH_TIMEOUT_MILLIS = 10_000L } } data/src/test/java/foundation/e/apps/data/login/MicrogLoginManagerTest.kt +83 −4 Original line number Diff line number Diff line Loading @@ -21,13 +21,16 @@ package foundation.e.apps.data.login import android.accounts.Account import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.accounts.OperationCanceledException import android.content.Intent import android.os.Bundle import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.microg.MicrogCertUtil import foundation.e.apps.data.login.microg.MicrogLoginManager import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.domain.auth.PlayStoreLoginMode import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse Loading Loading @@ -195,6 +198,30 @@ class MicrogLoginManagerTest { assertTrue(result is MicrogAccountFetchResult.Error) } @Test fun `returns error when auth token request times out`() = runBlocking { val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) val accountManager = mock<AccountManager>() val future = TimeoutAccountManagerFuture() whenever(accountManager.getAccountsByType(eq(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE))) .thenReturn(arrayOf(account)) whenever( accountManager.getAuthToken( eq(account), eq(MicrogCertUtil.PLAY_AUTH_SCOPE), any<Bundle>(), eq(false), isNull(), isNull() ) ).thenReturn(future) val result = buildMicrogLoginManager(accountManager).fetchAccount() assertTrue(result is MicrogAccountFetchResult.Error) assertEquals(MicrogLoginManager.MICROG_TOKEN_FETCH_TIMEOUT_MILLIS, future.timeoutMillis) } @Test fun `login uses existing oauth token when refresh not needed`() = runBlocking { val accountManager = mock<AccountManager>() Loading Loading @@ -282,13 +309,15 @@ class MicrogLoginManagerTest { whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) assertThrows(IllegalStateException::class.java) { val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore) .login() } } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) Loading Loading @@ -317,13 +346,15 @@ class MicrogLoginManagerTest { whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) assertThrows(IllegalStateException::class.java) { val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore) .login() } } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) Loading @@ -332,6 +363,39 @@ class MicrogLoginManagerTest { .saveAasToken(any()) } @Test fun `login routes token fetch timeout to microg login required`() = 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)) whenever( accountManager.getAuthToken( eq(account), eq(MicrogCertUtil.PLAY_AUTH_SCOPE), any<Bundle>(), eq(false), isNull(), isNull() ) ).thenReturn(TimeoutAccountManagerFuture()) val playStoreAuthStore = mock<PlayStoreAuthStore>() whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore) .login() } } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) } @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) Loading Loading @@ -398,7 +462,7 @@ class MicrogLoginManagerTest { val oauthAuthDataBuilder = mock<OauthAuthDataBuilder>() val playStoreAuthStore = mock<PlayStoreAuthStore>() val exception = assertThrows(IllegalStateException::class.java) { val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) Loading @@ -407,6 +471,7 @@ class MicrogLoginManagerTest { } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) } private fun buildMicrogLoginManager( Loading Loading @@ -436,4 +501,18 @@ class MicrogLoginManagerTest { override fun getResult(): Bundle = throw error override fun getResult(timeout: Long, unit: TimeUnit): Bundle = throw error } private class TimeoutAccountManagerFuture : AccountManagerFuture<Bundle> { var timeoutMillis: Long = 0 private set override fun cancel(mayInterruptIfRunning: Boolean): Boolean = false override fun isCancelled(): Boolean = false override fun isDone(): Boolean = false override fun getResult(): Bundle = error("Unbounded token fetch must not be used") override fun getResult(timeout: Long, unit: TimeUnit): Bundle { timeoutMillis = unit.toMillis(timeout) throw OperationCanceledException("Timed out") } } } Loading
data/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +15 −3 Original line number Diff line number Diff line Loading @@ -32,10 +32,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.data.login.api.MicrogAccountFetcher import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.domain.auth.PlayStoreLoginMode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton Loading Loading @@ -100,7 +103,7 @@ class MicrogLoginManager @Inject constructor( false, null, null ).result ).getResult(MICROG_TOKEN_FETCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) val intent = bundle.parcelableCompat(AccountManager.KEY_INTENT, Intent::class.java) if (intent != null) { Loading Loading @@ -154,11 +157,19 @@ class MicrogLoginManager @Inject constructor( playStoreAuthStore.saveAasToken("") result.oauthToken } is MicrogAccountFetchResult.RequiresUserAction -> error(MICROG_TOKEN_REFRESH_FAILURE) is MicrogAccountFetchResult.Error -> error(MICROG_TOKEN_REFRESH_FAILURE) is MicrogAccountFetchResult.RequiresUserAction -> throwMicrogRefreshFailure() is MicrogAccountFetchResult.Error -> throwMicrogRefreshFailure() } } private fun throwMicrogRefreshFailure(): Nothing { throw GPlayLoginException( isTimeout = false, message = MICROG_TOKEN_REFRESH_FAILURE, loginMode = PlayStoreLoginMode.MICROG, ) } private fun resolveAccount(accounts: Array<Account>, accountName: String): Account { return if (accountName.isNotBlank()) { accounts.firstOrNull { it.name == accountName } ?: accounts.first() Loading Loading @@ -189,5 +200,6 @@ class MicrogLoginManager @Inject constructor( const val MICROG_TOKEN_PREFIX = "ya29." const val MICROG_TOKEN_MISSING = "MicroG token is missing" const val MICROG_TOKEN_REFRESH_FAILURE = "MicroG refresh failed" const val MICROG_TOKEN_FETCH_TIMEOUT_MILLIS = 10_000L } }
data/src/test/java/foundation/e/apps/data/login/MicrogLoginManagerTest.kt +83 −4 Original line number Diff line number Diff line Loading @@ -21,13 +21,16 @@ package foundation.e.apps.data.login import android.accounts.Account import android.accounts.AccountManager import android.accounts.AccountManagerFuture import android.accounts.OperationCanceledException import android.content.Intent import android.os.Bundle import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.microg.MicrogCertUtil import foundation.e.apps.data.login.microg.MicrogLoginManager import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder import foundation.e.apps.data.preference.PlayStoreAuthStore import foundation.e.apps.data.login.api.MicrogAccountFetchResult import foundation.e.apps.domain.auth.PlayStoreLoginMode import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse Loading Loading @@ -195,6 +198,30 @@ class MicrogLoginManagerTest { assertTrue(result is MicrogAccountFetchResult.Error) } @Test fun `returns error when auth token request times out`() = runBlocking { val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) val accountManager = mock<AccountManager>() val future = TimeoutAccountManagerFuture() whenever(accountManager.getAccountsByType(eq(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE))) .thenReturn(arrayOf(account)) whenever( accountManager.getAuthToken( eq(account), eq(MicrogCertUtil.PLAY_AUTH_SCOPE), any<Bundle>(), eq(false), isNull(), isNull() ) ).thenReturn(future) val result = buildMicrogLoginManager(accountManager).fetchAccount() assertTrue(result is MicrogAccountFetchResult.Error) assertEquals(MicrogLoginManager.MICROG_TOKEN_FETCH_TIMEOUT_MILLIS, future.timeoutMillis) } @Test fun `login uses existing oauth token when refresh not needed`() = runBlocking { val accountManager = mock<AccountManager>() Loading Loading @@ -282,13 +309,15 @@ class MicrogLoginManagerTest { whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) assertThrows(IllegalStateException::class.java) { val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore) .login() } } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) Loading Loading @@ -317,13 +346,15 @@ class MicrogLoginManagerTest { whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) assertThrows(IllegalStateException::class.java) { val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore) .login() } } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never()) Loading @@ -332,6 +363,39 @@ class MicrogLoginManagerTest { .saveAasToken(any()) } @Test fun `login routes token fetch timeout to microg login required`() = 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)) whenever( accountManager.getAuthToken( eq(account), eq(MicrogCertUtil.PLAY_AUTH_SCOPE), any<Bundle>(), eq(false), isNull(), isNull() ) ).thenReturn(TimeoutAccountManagerFuture()) val playStoreAuthStore = mock<PlayStoreAuthStore>() whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore) .login() } } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never()) .invalidateAuthToken(any(), any()) } @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) Loading Loading @@ -398,7 +462,7 @@ class MicrogLoginManagerTest { val oauthAuthDataBuilder = mock<OauthAuthDataBuilder>() val playStoreAuthStore = mock<PlayStoreAuthStore>() val exception = assertThrows(IllegalStateException::class.java) { val exception = assertThrows(GPlayLoginException::class.java) { runBlocking { whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old") whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name) Loading @@ -407,6 +471,7 @@ class MicrogLoginManagerTest { } assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message) assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode) } private fun buildMicrogLoginManager( Loading Loading @@ -436,4 +501,18 @@ class MicrogLoginManagerTest { override fun getResult(): Bundle = throw error override fun getResult(timeout: Long, unit: TimeUnit): Bundle = throw error } private class TimeoutAccountManagerFuture : AccountManagerFuture<Bundle> { var timeoutMillis: Long = 0 private set override fun cancel(mayInterruptIfRunning: Boolean): Boolean = false override fun isCancelled(): Boolean = false override fun isDone(): Boolean = false override fun getResult(): Bundle = error("Unbounded token fetch must not be used") override fun getResult(timeout: Long, unit: TimeUnit): Bundle { timeoutMillis = unit.toMillis(timeout) throw OperationCanceledException("Timed out") } } }