From 57f4ec4e22318e0fc62c104c4401dcdbdf136a99 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Tue, 5 Apr 2022 16:51:53 +0500 Subject: [PATCH] Implement the simplest account manager authentication --- .../java/gq/kirmanak/mealient/MainActivity.kt | 1 + .../mealient/data/auth/AuthStorage.kt | 14 ---- .../mealient/data/auth/impl/AuthRepoImpl.kt | 52 +++++++++++--- .../data/auth/impl/AuthStorageImpl.kt | 52 -------------- .../gq/kirmanak/mealient/di/AuthModule.kt | 15 ++-- .../service/auth/AccountAuthenticatorImpl.kt | 72 ++++++++----------- .../service/auth/AccountManagerInteractor.kt | 4 +- .../auth/AccountManagerInteractorImpl.kt | 39 +++++----- .../mealient/service/auth/AuthExtensions.kt | 15 ++-- .../service/auth/AuthenticatorException.kt | 11 ++- .../ui/auth/AuthenticationViewModel.kt | 15 ++-- .../main/res/xml/account_authenticator.xml | 1 - .../data/auth/impl/AuthRepoImplTest.kt | 66 ++--------------- 13 files changed, 124 insertions(+), 233 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt index 1446e96..41b809d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/MainActivity.kt @@ -74,6 +74,7 @@ class MainActivity : AppCompatActivity() { R.id.logout, R.id.login -> { // When user clicks logout they don't want to be authorized authViewModel.authRequested = item.itemId == R.id.login + authViewModel.logout() true } else -> super.onOptionsItemSelected(item) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt deleted file mode 100644 index 5955e7f..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt +++ /dev/null @@ -1,14 +0,0 @@ -package gq.kirmanak.mealient.data.auth - -import kotlinx.coroutines.flow.Flow - -interface AuthStorage { - - val authHeaderFlow: Flow - - suspend fun storeAuthData(authHeader: String) - - suspend fun getAuthHeader(): String? - - suspend fun clearAuthData() -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt index 6e26772..790ee4e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt @@ -1,8 +1,9 @@ package gq.kirmanak.mealient.data.auth.impl -import gq.kirmanak.mealient.data.auth.AuthDataSource +import android.accounts.Account import gq.kirmanak.mealient.data.auth.AuthRepo -import gq.kirmanak.mealient.data.auth.AuthStorage +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.service.auth.AccountManagerInteractor import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber @@ -11,28 +12,59 @@ import javax.inject.Singleton @Singleton class AuthRepoImpl @Inject constructor( - private val dataSource: AuthDataSource, - private val storage: AuthStorage, + private val accountManagerInteractor: AccountManagerInteractor, ) : AuthRepo { override val isAuthorizedFlow: Flow - get() = storage.authHeaderFlow.map { it != null } + get() = accountManagerInteractor.accountUpdatesFlow() + .map { it.firstOrNull() } + .map { account -> + runCatchingExceptCancel { getAuthToken(account) } + .onFailure { Timber.e(it, "authHeaderObservable: can't get token") } + .getOrNull() + }.map { it != null } override suspend fun authenticate(username: String, password: String) { Timber.v("authenticate() called with: username = $username, password = $password") - val accessToken = dataSource.authenticate(username, password) - Timber.d("authenticate result is \"$accessToken\"") - storage.storeAuthData(AUTH_HEADER_FORMAT.format(accessToken)) + val account = accountManagerInteractor.addAccount(username, password) + runCatchingExceptCancel { + getAuthToken(account) // Try to get token to check if password is correct + }.onFailure { + Timber.e(it, "authenticate: can't authorize") + removeAccount(account) // Remove account with incorrect password + }.onSuccess { + Timber.d("authenticate: successfully authorized") + }.getOrThrow() // Throw error to show it to user } - override suspend fun getAuthHeader(): String? = storage.getAuthHeader() + override suspend fun getAuthHeader(): String? { + Timber.v("getAuthHeader() called") + return currentAccount() + ?.let { getAuthToken(it) } + ?.let { AUTH_HEADER_FORMAT.format(it) } + } + + private suspend fun getAuthToken(account: Account?): String? { + return account?.let { accountManagerInteractor.getAuthToken(it) } + } + + private fun currentAccount(): Account? { + val account = accountManagerInteractor.getAccounts().firstOrNull() + Timber.v("currentAccount() returned: $account") + return account + } override suspend fun requireAuthHeader(): String = checkNotNull(getAuthHeader()) { "Auth header is null when it was required" } override suspend fun logout() { Timber.v("logout() called") - storage.clearAuthData() + currentAccount()?.let { removeAccount(it) } + } + + private suspend fun removeAccount(account: Account) { + Timber.v("removeAccount() called with: account = $account") + accountManagerInteractor.removeAccount(account) } companion object { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt deleted file mode 100644 index a15ac30..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt +++ /dev/null @@ -1,52 +0,0 @@ -package gq.kirmanak.mealient.data.auth.impl - -import androidx.datastore.preferences.core.Preferences -import dagger.hilt.android.scopes.ActivityScoped -import gq.kirmanak.mealient.data.auth.AuthStorage -import gq.kirmanak.mealient.data.storage.PreferencesStorage -import gq.kirmanak.mealient.service.auth.AccountManagerInteractor -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject - -@ActivityScoped -class AuthStorageImpl @Inject constructor( - private val accountManagerInteractorImpl: AccountManagerInteractor, - private val preferencesStorage: PreferencesStorage, -) : AuthStorage { - - private val authHeaderKey: Preferences.Key - get() = preferencesStorage.authHeaderKey - override val authHeaderFlow: Flow - get() = preferencesStorage.valueUpdates(authHeaderKey) - - override suspend fun storeAuthData(authHeader: String) { - Timber.v("storeAuthData() called with: authHeader = $authHeader") - preferencesStorage.storeValues(Pair(authHeaderKey, authHeader)) - } - - override suspend fun getAuthHeader(): String? { - Timber.v("getAuthHeader() called") - val token = preferencesStorage.getValue(authHeaderKey) - Timber.d("getAuthHeader: header is \"$token\"") - return token - } - - override fun authHeaderObservable(): Flow { - Timber.v("authHeaderObservable() called") - return accountManagerInteractorImpl.accountUpdatesFlow() - .map { it.firstOrNull() } - .map { account -> - account ?: return@map null - runCatching { accountManagerInteractorImpl.getAuthToken(account) } - .onFailure { Timber.e(it, "authHeaderObservable: can't get token") } - .getOrNull() - } - } - - override suspend fun clearAuthData() { - Timber.v("clearAuthData() called") - preferencesStorage.removeValues(authHeaderKey) - } -} diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt index 66f556d..7a5a272 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt @@ -1,22 +1,25 @@ package gq.kirmanak.mealient.di +import android.accounts.AccountManager +import android.content.Context import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthRepo -import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl import gq.kirmanak.mealient.data.auth.impl.AuthService -import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory +import gq.kirmanak.mealient.service.auth.AccountManagerInteractor +import gq.kirmanak.mealient.service.auth.AccountManagerInteractorImpl import gq.kirmanak.mealient.service.auth.AccountParameters import javax.inject.Singleton @@ -26,8 +29,6 @@ interface AuthModule { companion object { - const val ACCOUNT_TYPE = "Mealient" - @Provides @Singleton fun provideAuthServiceFactory(retrofitBuilder: RetrofitBuilder, @@ -54,9 +55,11 @@ interface AuthModule { @Binds @Singleton - fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage + fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo @Binds @Singleton - fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo + fun bindAccountManagerInteractor( + accountManagerInteractorImpl: AccountManagerInteractorImpl + ): AccountManagerInteractor } diff --git a/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountAuthenticatorImpl.kt b/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountAuthenticatorImpl.kt index bde58f0..74920ca 100644 --- a/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountAuthenticatorImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountAuthenticatorImpl.kt @@ -2,12 +2,12 @@ package gq.kirmanak.mealient.service.auth import android.accounts.* import android.content.Context -import android.content.Intent import android.os.Bundle import dagger.hilt.android.qualifiers.ApplicationContext import gq.kirmanak.mealient.data.auth.AuthDataSource -import gq.kirmanak.mealient.data.auth.impl.AuthenticationError -import gq.kirmanak.mealient.ui.auth.AuthenticatorActivity +import gq.kirmanak.mealient.data.network.NetworkError +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.service.auth.AuthenticatorException.* import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject @@ -21,55 +21,30 @@ class AccountAuthenticatorImpl @Inject constructor( private val accountManager: AccountManager, ) : AbstractAccountAuthenticator(context) { - override fun addAccount( - response: AccountAuthenticatorResponse, - accountType: String, - authTokenType: String, - requiredFeatures: Array?, - options: Bundle, - ): Bundle { - Timber.v("addAccount() called with: response = $response, accountType = $accountType, authTokenType = $authTokenType, requiredFeatures = $requiredFeatures, options = $options") - - try { - checkAccountType(accountType) - checkAuthTokenType(authTokenType) - } catch (e: AuthenticatorException) { - Timber.e(e, "addAccount: validation failure") - return e.bundle - } - - val intent = Intent(context, AuthenticatorActivity::class.java) - return Bundle().apply { putParcelable(AccountManager.KEY_INTENT, intent) } - } - override fun getAuthToken( response: AccountAuthenticatorResponse, account: Account, authTokenType: String, - options: Bundle + options: Bundle? ): Bundle { Timber.v("getAuthToken() called with: response = $response, account = $account, authTokenType = $authTokenType, options = $options") - val password: String? - val baseUrl: String? - try { + val password = try { checkAccountType(account.type) checkAuthTokenType(authTokenType) - password = accountManager.getPassword(account) - ?: throw AuthenticatorException.AccountNotFound(account) - baseUrl = options.getString(KEY_BASE_URL) ?: throw AuthenticatorException.NoBaseUrl + accountManager.getPassword(account) ?: throw AccountNotFound(account) } catch (e: AuthenticatorException) { Timber.e(e, "getAuthToken: validation failure") return e.bundle } - val token = try { - runBlocking { authDataSource.authenticate(account.name, password, baseUrl) } - } catch (e: RuntimeException) { - return when (e) { - is AuthenticationError.NotMealie -> AuthenticatorException.NotMealie.bundle - is AuthenticationError.Unauthorized -> AuthenticatorException.Unauthorized.bundle - else -> throw NetworkErrorException(e) + val token = runCatchingExceptCancel { + runBlocking { authDataSource.authenticate(account.name, password) } + }.getOrElse { + return when (it) { + is NetworkError.NotMealie -> NotMealie.bundle + is NetworkError.Unauthorized -> Unauthorized.bundle + else -> throw NetworkErrorException(it) } } @@ -87,7 +62,18 @@ class AccountAuthenticatorImpl @Inject constructor( options: Bundle? ): Bundle { Timber.v("confirmCredentials() called with: response = $response, account = $account, options = $options") - return AuthenticatorException.UnsupportedOperation("confirmCredentials").bundle + return UnsupportedOperation("confirmCredentials").bundle + } + + override fun addAccount( + response: AccountAuthenticatorResponse, + accountType: String, + authTokenType: String, + requiredFeatures: Array?, + options: Bundle?, + ): Bundle { + Timber.v("addAccount() called with: response = $response, accountType = $accountType, authTokenType = $authTokenType, requiredFeatures = $requiredFeatures, options = $options") + return UnsupportedOperation("addAccount").bundle } override fun editProperties( @@ -114,7 +100,7 @@ class AccountAuthenticatorImpl @Inject constructor( options: Bundle? ): Bundle { Timber.v("updateCredentials() called with: response = $response, account = $account, authTokenType = $authTokenType, options = $options") - return AuthenticatorException.UnsupportedOperation("updateCredentials").bundle + return UnsupportedOperation("updateCredentials").bundle } override fun hasFeatures( @@ -129,13 +115,13 @@ class AccountAuthenticatorImpl @Inject constructor( private fun checkAccountType(accountType: String) { if (accountType != accountParameters.accountType) { - throw AuthenticatorException.UnsupportedAccountType(accountType) + throw UnsupportedAccountType(accountType) } } private fun checkAuthTokenType(authTokenType: String) { - if (authTokenType != accountParameters.accountType) { - throw AuthenticatorException.UnsupportedAccountType(authTokenType) + if (authTokenType != accountParameters.authTokenType) { + throw UnsupportedAuthTokenType(authTokenType) } } } diff --git a/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractor.kt b/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractor.kt index 8bfaa37..4cd2eea 100644 --- a/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractor.kt +++ b/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractor.kt @@ -7,9 +7,11 @@ interface AccountManagerInteractor { fun getAccounts(): Array - suspend fun addAccount(): Account + suspend fun addAccount(email: String, password: String): Account suspend fun getAuthToken(account: Account): String fun accountUpdatesFlow(): Flow> + + suspend fun removeAccount(account: Account) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractorImpl.kt b/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractorImpl.kt index 03d92e1..3782e73 100644 --- a/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractorImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/service/auth/AccountManagerInteractorImpl.kt @@ -2,38 +2,30 @@ package gq.kirmanak.mealient.service.auth import android.accounts.Account import android.accounts.AccountManager -import android.app.Activity -import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.flow.Flow import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton -@ActivityScoped +@Singleton class AccountManagerInteractorImpl @Inject constructor( private val accountManager: AccountManager, private val accountParameters: AccountParameters, - private val activity: Activity ) : AccountManagerInteractor { override fun getAccounts(): Array { Timber.v("getAccounts() called") val accounts = accountManager.getAccountsByType(accountParameters.accountType) - Timber.v("getAccounts() returned: $accounts") + Timber.v("getAccounts() returned: ${accounts.contentToString()}") return accounts } - override suspend fun addAccount(): Account { - Timber.v("addAccount() called") - val bundle = accountManager.addAccount( - accountParameters.accountType, - accountParameters.authTokenType, - null, - null, - activity, - null, - null - ).await() - return bundle.toAccount() + override suspend fun addAccount(email: String, password: String): Account { + Timber.v("addAccount() called with: email = $email, password = $password") + val account = Account(email, accountParameters.accountType) + removeAccount(account) // Remove account if it was created earlier + accountManager.addAccountExplicitly(account, password, null) + return account } override suspend fun getAuthToken(account: Account): String { @@ -42,18 +34,27 @@ class AccountManagerInteractorImpl @Inject constructor( account, accountParameters.authTokenType, null, - activity, + null, null, null ).await() val receivedAccount = bundle.toAccount() - check(account == receivedAccount) { "Received account ($receivedAccount) differs from requested ($account)" } + check(account == receivedAccount) { + "Received account ($receivedAccount) differs from requested ($account)" + } val token = bundle.authToken() Timber.v("getAuthToken() returned: $token") return token } override fun accountUpdatesFlow(): Flow> { + Timber.v("accountUpdatesFlow() called") return accountManager.accountUpdatesFlow(accountParameters.accountType) } + + override suspend fun removeAccount(account: Account) { + Timber.v("removeAccount() called with: account = $account") + val bundle = accountManager.removeAccount(account, null, null, null).await() + Timber.d("removeAccount: result is ${bundle.result()}") + } } diff --git a/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthExtensions.kt index f14fb3e..4fb9fc4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthExtensions.kt @@ -7,19 +7,16 @@ import android.accounts.AccountManagerFuture import android.accounts.OnAccountsUpdateListener import android.os.Build import android.os.Bundle +import gq.kirmanak.mealient.extensions.logErrors import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.onFailure -import kotlinx.coroutines.channels.onSuccess import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.withContext import timber.log.Timber -internal const val KEY_BASE_URL = "mealientBaseUrl" - internal suspend fun AccountManagerFuture.await(): T = withContext(Dispatchers.IO) { result } internal fun Bundle.toAccount(): Account = Account(accountName(), accountType()) @@ -30,6 +27,8 @@ internal fun Bundle.accountName(): String = string(KEY_ACCOUNT_NAME) { "Account internal fun Bundle.authToken(): String = string(KEY_AUTHTOKEN) { "Auth token is null" } +internal fun Bundle.result(): Boolean = getBoolean(KEY_BOOLEAN_RESULT) + private fun Bundle.string(key: String, error: () -> String) = checkNotNull(getString(key), error) @OptIn(ExperimentalCoroutinesApi::class) @@ -37,12 +36,10 @@ internal fun AccountManager.accountUpdatesFlow(vararg types: String): Flow - Timber.d("accountUpdatesFlow: updated accounts = $accounts") + Timber.d("accountUpdatesFlow: updated accounts = ${accounts.contentToString()}") val filtered = accounts.filter { types.contains(it.type) }.toTypedArray() - Timber.d("accountUpdatesFlow: filtered accounts = $filtered") - trySendBlocking(filtered) - .onSuccess { Timber.d("accountUpdatesFlow: sent accounts update") } - .onFailure { Timber.e(it, "accountUpdatesFlow: failed to send update") } + Timber.d("accountUpdatesFlow: filtered accounts = ${filtered.contentToString()}") + trySendBlocking(filtered).logErrors("accountUpdatesFlow") } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { addOnAccountsUpdatedListener(listener, null, true, types) diff --git a/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthenticatorException.kt b/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthenticatorException.kt index 3b06440..a22dfea 100644 --- a/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthenticatorException.kt +++ b/app/src/main/java/gq/kirmanak/mealient/service/auth/AuthenticatorException.kt @@ -35,18 +35,15 @@ sealed class AuthenticatorException( errorMessage = "$account not found" ) - object NoBaseUrl : AuthenticatorException( - errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS, - errorMessage = "Base URL was not provided" - ) - object Unauthorized : AuthenticatorException( - errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS, + errorCode = ErrorCode.Unauthorized.ordinal, errorMessage = "E-mail or password weren't correct" ) object NotMealie : AuthenticatorException( - errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS, + errorCode = ErrorCode.NotMealie.ordinal, errorMessage = "Base URL must be pointing at a non Mealie server" ) + + enum class ErrorCode { NotMealie, Unauthorized; } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index b411758..0826a92 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import timber.log.Timber @@ -32,18 +31,14 @@ class AuthenticationViewModel @Inject constructor( var authRequested: Boolean by authRequestsFlow::value var showLoginButton: Boolean by showLoginButtonFlow::value - init { - viewModelScope.launch { - authRequestsFlow.collect { isRequested -> - // Clear auth token on logout request - if (!isRequested) authRepo.logout() - } - } - } - suspend fun authenticate(username: String, password: String) = runCatchingExceptCancel { authRepo.authenticate(username, password) }.onFailure { Timber.e(it, "authenticate: can't authenticate") } + + fun logout() { + Timber.v("logout() called") + viewModelScope.launch { authRepo.logout() } + } } \ No newline at end of file diff --git a/app/src/main/res/xml/account_authenticator.xml b/app/src/main/res/xml/account_authenticator.xml index 95d0037..cdbd336 100644 --- a/app/src/main/res/xml/account_authenticator.xml +++ b/app/src/main/res/xml/account_authenticator.xml @@ -1,6 +1,5 @@ \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt index 466a26c..50fac9a 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt @@ -1,78 +1,22 @@ package gq.kirmanak.mealient.data.auth.impl -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.auth.AuthDataSource -import gq.kirmanak.mealient.data.auth.AuthStorage -import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME -import gq.kirmanak.mealient.test.RobolectricTest +import gq.kirmanak.mealient.service.auth.AccountManagerInteractor import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) -class AuthRepoImplTest : RobolectricTest() { +class AuthRepoImplTest { @MockK - lateinit var dataSource: AuthDataSource - - @MockK(relaxUnitFun = true) - lateinit var storage: AuthStorage + lateinit var accountManagerInteractor: AccountManagerInteractor lateinit var subject: AuthRepoImpl @Before fun setUp() { MockKAnnotations.init(this) - subject = AuthRepoImpl(dataSource, storage) + subject = AuthRepoImpl(accountManagerInteractor) } - @Test - fun `when not authenticated then first auth status is false`() = runTest { - coEvery { storage.authHeaderFlow } returns flowOf(null) - assertThat(subject.isAuthorizedFlow.first()).isFalse() - } - - @Test - fun `when authenticated then first auth status is true`() = runTest { - coEvery { storage.authHeaderFlow } returns flowOf(TEST_AUTH_HEADER) - assertThat(subject.isAuthorizedFlow.first()).isTrue() - } - - @Test(expected = Unauthorized::class) - fun `when authentication fails then authenticate throws`() = runTest { - coEvery { - dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) - } throws Unauthorized(RuntimeException()) - subject.authenticate(TEST_USERNAME, TEST_PASSWORD) - } - - @Test - fun `when authenticated then getToken returns token`() = runTest { - coEvery { storage.getAuthHeader() } returns TEST_AUTH_HEADER - assertThat(subject.getAuthHeader()).isEqualTo(TEST_AUTH_HEADER) - } - - @Test - fun `when authenticated successfully then stores token and url`() = runTest { - coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN - subject.authenticate(TEST_USERNAME, TEST_PASSWORD) - coVerify { storage.storeAuthData(TEST_AUTH_HEADER) } - } - - @Test - fun `when logout then clearAuthData is called`() = runTest { - subject.logout() - coVerify { storage.clearAuthData() } - } + // TODO write the actual tests }