Implement observing authentication statuses

This commit is contained in:
Kirill Kamakin
2021-11-14 10:27:45 +03:00
parent 1a136b6ade
commit 698d93b351
9 changed files with 84 additions and 30 deletions

View File

@@ -7,8 +7,10 @@ import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealie.data.auth.impl.AuthDataSourceImpl
import gq.kirmanak.mealie.data.auth.impl.AuthRepoImpl
import gq.kirmanak.mealie.data.auth.impl.AuthStorageImpl
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.ExperimentalSerializationApi
@ExperimentalCoroutinesApi
@ExperimentalSerializationApi
@Module
@InstallIn(SingletonComponent::class)

View File

@@ -1,11 +1,13 @@
package gq.kirmanak.mealie.data.auth
interface AuthRepo {
suspend fun isAuthenticated(): Boolean
import kotlinx.coroutines.flow.Flow
interface AuthRepo {
suspend fun authenticate(username: String, password: String, baseUrl: String)
suspend fun getBaseUrl(): String?
suspend fun getToken(): String?
fun authenticationStatuses(): Flow<Boolean>
}

View File

@@ -1,9 +1,13 @@
package gq.kirmanak.mealie.data.auth
import kotlinx.coroutines.flow.Flow
interface AuthStorage {
suspend fun storeAuthData(token: String, baseUrl: String)
suspend fun getBaseUrl(): String?
suspend fun getToken(): String?
fun tokenObservable(): Flow<String?>
}

View File

@@ -3,6 +3,8 @@ package gq.kirmanak.mealie.data.auth.impl
import gq.kirmanak.mealie.data.auth.AuthDataSource
import gq.kirmanak.mealie.data.auth.AuthRepo
import gq.kirmanak.mealie.data.auth.AuthStorage
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject
@@ -10,13 +12,6 @@ class AuthRepoImpl @Inject constructor(
private val dataSource: AuthDataSource,
private val storage: AuthStorage
) : AuthRepo {
override suspend fun isAuthenticated(): Boolean {
Timber.v("isAuthenticated")
val authenticated = getToken() != null
Timber.d("isAuthenticated() response $authenticated")
return authenticated
}
override suspend fun authenticate(
username: String,
password: String,
@@ -38,4 +33,9 @@ class AuthRepoImpl @Inject constructor(
Timber.v("getToken() called")
return storage.getToken()
}
override fun authenticationStatuses(): Flow<Boolean> {
Timber.v("authenticationStatuses() called")
return storage.tokenObservable().map { it != null }
}
}

View File

@@ -6,6 +6,12 @@ import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import gq.kirmanak.mealie.data.auth.AuthStorage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@@ -13,6 +19,7 @@ import javax.inject.Inject
private const val TOKEN_KEY = "AUTH_TOKEN"
private const val BASE_URL_KEY = "BASE_URL"
@ExperimentalCoroutinesApi
class AuthStorageImpl @Inject constructor(@ApplicationContext private val context: Context) :
AuthStorage {
private val sharedPreferences: SharedPreferences
@@ -40,6 +47,29 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex
return token
}
override fun tokenObservable(): Flow<String?> {
Timber.v("tokenObservable() called")
return callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
Timber.v("tokenObservable: listener called with key $key")
val token = when (key) {
null -> null
TOKEN_KEY -> prefs.getString(key, null)
else -> return@OnSharedPreferenceChangeListener
}
Timber.d("tokenObservable: New token: $token")
trySendBlocking(token).onFailure { Timber.e(it, "Can't send new token") }
}
Timber.v("tokenObservable: registering listener")
send(getToken())
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose {
Timber.v("tokenObservable: flow has been closed")
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
}
private suspend fun getString(key: String): String? = withContext(Dispatchers.Default) {
sharedPreferences.getString(key, null)
}

View File

@@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.textfield.TextInputLayout
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealie.databinding.FragmentAuthenticationBinding
import kotlinx.coroutines.flow.collectLatest
import timber.log.Timber
@AndroidEntryPoint
@@ -24,7 +25,17 @@ class AuthenticationFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
checkIfAuthenticatedAlready()
listenToAuthenticationStatuses()
}
private fun listenToAuthenticationStatuses() {
Timber.d("listenToAuthenticationStatuses() called")
lifecycleScope.launchWhenCreated {
viewModel.authenticationStatuses().collectLatest {
Timber.d("listenToAuthenticationStatuses: new status = $it")
if (it) navigateToRecipes()
}
}
}
override fun onCreateView(
@@ -43,13 +54,6 @@ class AuthenticationFragment : Fragment() {
binding.button.setOnClickListener { onLoginClicked() }
}
private fun checkIfAuthenticatedAlready() {
Timber.v("checkIfAuthenticatedAlready() called")
lifecycleScope.launchWhenCreated {
if (viewModel.isAuthenticated()) navigateToRecipes()
}
}
private fun navigateToRecipes() {
findNavController().navigate(AuthenticationFragmentDirections.actionAuthenticationFragmentToRecipesFragment())
}
@@ -73,8 +77,6 @@ class AuthenticationFragment : Fragment() {
lifecycleScope.launchWhenResumed {
runCatching {
viewModel.authenticate(email, pass, url)
}.onSuccess {
navigateToRecipes()
}.onFailure {
Timber.e(it, "Can't authenticate")
}

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealie.ui.auth
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealie.data.auth.AuthRepo
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
import javax.inject.Inject
@@ -14,15 +15,13 @@ class AuthenticationViewModel @Inject constructor(
Timber.v("constructor called")
}
suspend fun isAuthenticated(): Boolean {
Timber.v("isAuthenticated() called")
val result = authRepo.isAuthenticated()
Timber.d("isAuthenticated() returned: $result")
return result
}
suspend fun authenticate(username: String, password: String, baseUrl: String) {
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
authRepo.authenticate(username, password, baseUrl)
}
fun authenticationStatuses(): Flow<Boolean> {
Timber.v("authenticationStatuses() called")
return authRepo.authenticationStatuses()
}
}

View File

@@ -7,6 +7,7 @@ import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_TOKEN
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_USERNAME
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueSuccessfulAuthResponse
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueUnsuccessfulAuthResponse
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Test
import retrofit2.HttpException
@@ -18,15 +19,15 @@ class AuthRepoImplTest : MockServerTest() {
lateinit var subject: AuthRepoImpl
@Test
fun `when not authenticated then isAuthenticated false`() = runBlocking {
assertThat(subject.isAuthenticated()).isFalse()
fun `when not authenticated then first auth status is false`() = runBlocking {
assertThat(subject.authenticationStatuses().first()).isFalse()
}
@Test
fun `when authenticated then isAuthenticated true`() = runBlocking {
fun `when authenticated then first auth status is true`() = runBlocking {
mockServer.enqueueSuccessfulAuthResponse()
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
assertThat(subject.isAuthenticated()).isTrue()
assertThat(subject.authenticationStatuses().first()).isTrue()
}
@Test(expected = HttpException::class)

View File

@@ -4,10 +4,13 @@ import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_TOKEN
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_URL
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Test
import javax.inject.Inject
@ExperimentalCoroutinesApi
@HiltAndroidTest
class AuthStorageImplTest : HiltRobolectricTest() {
@Inject
@@ -39,4 +42,15 @@ class AuthStorageImplTest : HiltRobolectricTest() {
fun `when reading url without storing data then returns null`() = runBlocking {
assertThat(subject.getBaseUrl()).isNull()
}
@Test
fun `when didn't store auth data then first token is null`() = runBlocking {
assertThat(subject.tokenObservable().first()).isNull()
}
@Test
fun `when stored auth data then first token is correct`() = runBlocking {
subject.storeAuthData(TEST_TOKEN, TEST_URL)
assertThat(subject.tokenObservable().first()).isEqualTo(TEST_TOKEN)
}
}