Implement observing authentication statuses
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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?>
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user