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.AuthDataSourceImpl
|
||||||
import gq.kirmanak.mealie.data.auth.impl.AuthRepoImpl
|
import gq.kirmanak.mealie.data.auth.impl.AuthRepoImpl
|
||||||
import gq.kirmanak.mealie.data.auth.impl.AuthStorageImpl
|
import gq.kirmanak.mealie.data.auth.impl.AuthStorageImpl
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
@ExperimentalSerializationApi
|
@ExperimentalSerializationApi
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package gq.kirmanak.mealie.data.auth
|
package gq.kirmanak.mealie.data.auth
|
||||||
|
|
||||||
interface AuthRepo {
|
import kotlinx.coroutines.flow.Flow
|
||||||
suspend fun isAuthenticated(): Boolean
|
|
||||||
|
|
||||||
|
interface AuthRepo {
|
||||||
suspend fun authenticate(username: String, password: String, baseUrl: String)
|
suspend fun authenticate(username: String, password: String, baseUrl: String)
|
||||||
|
|
||||||
suspend fun getBaseUrl(): String?
|
suspend fun getBaseUrl(): String?
|
||||||
|
|
||||||
suspend fun getToken(): String?
|
suspend fun getToken(): String?
|
||||||
|
|
||||||
|
fun authenticationStatuses(): Flow<Boolean>
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package gq.kirmanak.mealie.data.auth
|
package gq.kirmanak.mealie.data.auth
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface AuthStorage {
|
interface AuthStorage {
|
||||||
suspend fun storeAuthData(token: String, baseUrl: String)
|
suspend fun storeAuthData(token: String, baseUrl: String)
|
||||||
|
|
||||||
suspend fun getBaseUrl(): String?
|
suspend fun getBaseUrl(): String?
|
||||||
|
|
||||||
suspend fun getToken(): 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.AuthDataSource
|
||||||
import gq.kirmanak.mealie.data.auth.AuthRepo
|
import gq.kirmanak.mealie.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealie.data.auth.AuthStorage
|
import gq.kirmanak.mealie.data.auth.AuthStorage
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -10,13 +12,6 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
private val dataSource: AuthDataSource,
|
private val dataSource: AuthDataSource,
|
||||||
private val storage: AuthStorage
|
private val storage: AuthStorage
|
||||||
) : AuthRepo {
|
) : AuthRepo {
|
||||||
override suspend fun isAuthenticated(): Boolean {
|
|
||||||
Timber.v("isAuthenticated")
|
|
||||||
val authenticated = getToken() != null
|
|
||||||
Timber.d("isAuthenticated() response $authenticated")
|
|
||||||
return authenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun authenticate(
|
override suspend fun authenticate(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
@@ -38,4 +33,9 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
Timber.v("getToken() called")
|
Timber.v("getToken() called")
|
||||||
return storage.getToken()
|
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 dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import gq.kirmanak.mealie.data.auth.AuthStorage
|
import gq.kirmanak.mealie.data.auth.AuthStorage
|
||||||
import kotlinx.coroutines.Dispatchers
|
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 kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,6 +19,7 @@ import javax.inject.Inject
|
|||||||
private const val TOKEN_KEY = "AUTH_TOKEN"
|
private const val TOKEN_KEY = "AUTH_TOKEN"
|
||||||
private const val BASE_URL_KEY = "BASE_URL"
|
private const val BASE_URL_KEY = "BASE_URL"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
class AuthStorageImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
class AuthStorageImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||||
AuthStorage {
|
AuthStorage {
|
||||||
private val sharedPreferences: SharedPreferences
|
private val sharedPreferences: SharedPreferences
|
||||||
@@ -40,6 +47,29 @@ class AuthStorageImpl @Inject constructor(@ApplicationContext private val contex
|
|||||||
return token
|
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) {
|
private suspend fun getString(key: String): String? = withContext(Dispatchers.Default) {
|
||||||
sharedPreferences.getString(key, null)
|
sharedPreferences.getString(key, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
|
|||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealie.databinding.FragmentAuthenticationBinding
|
import gq.kirmanak.mealie.databinding.FragmentAuthenticationBinding
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -24,7 +25,17 @@ class AuthenticationFragment : Fragment() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.v("onCreate() called with: savedInstanceState = $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(
|
override fun onCreateView(
|
||||||
@@ -43,13 +54,6 @@ class AuthenticationFragment : Fragment() {
|
|||||||
binding.button.setOnClickListener { onLoginClicked() }
|
binding.button.setOnClickListener { onLoginClicked() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkIfAuthenticatedAlready() {
|
|
||||||
Timber.v("checkIfAuthenticatedAlready() called")
|
|
||||||
lifecycleScope.launchWhenCreated {
|
|
||||||
if (viewModel.isAuthenticated()) navigateToRecipes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToRecipes() {
|
private fun navigateToRecipes() {
|
||||||
findNavController().navigate(AuthenticationFragmentDirections.actionAuthenticationFragmentToRecipesFragment())
|
findNavController().navigate(AuthenticationFragmentDirections.actionAuthenticationFragmentToRecipesFragment())
|
||||||
}
|
}
|
||||||
@@ -73,8 +77,6 @@ class AuthenticationFragment : Fragment() {
|
|||||||
lifecycleScope.launchWhenResumed {
|
lifecycleScope.launchWhenResumed {
|
||||||
runCatching {
|
runCatching {
|
||||||
viewModel.authenticate(email, pass, url)
|
viewModel.authenticate(email, pass, url)
|
||||||
}.onSuccess {
|
|
||||||
navigateToRecipes()
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it, "Can't authenticate")
|
Timber.e(it, "Can't authenticate")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealie.ui.auth
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealie.data.auth.AuthRepo
|
import gq.kirmanak.mealie.data.auth.AuthRepo
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -14,15 +15,13 @@ class AuthenticationViewModel @Inject constructor(
|
|||||||
Timber.v("constructor called")
|
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) {
|
suspend fun authenticate(username: String, password: String, baseUrl: String) {
|
||||||
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
|
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
|
||||||
authRepo.authenticate(username, password, 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.TEST_USERNAME
|
||||||
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueSuccessfulAuthResponse
|
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueSuccessfulAuthResponse
|
||||||
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueUnsuccessfulAuthResponse
|
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.enqueueUnsuccessfulAuthResponse
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
@@ -18,15 +19,15 @@ class AuthRepoImplTest : MockServerTest() {
|
|||||||
lateinit var subject: AuthRepoImpl
|
lateinit var subject: AuthRepoImpl
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when not authenticated then isAuthenticated false`() = runBlocking {
|
fun `when not authenticated then first auth status is false`() = runBlocking {
|
||||||
assertThat(subject.isAuthenticated()).isFalse()
|
assertThat(subject.authenticationStatuses().first()).isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when authenticated then isAuthenticated true`() = runBlocking {
|
fun `when authenticated then first auth status is true`() = runBlocking {
|
||||||
mockServer.enqueueSuccessfulAuthResponse()
|
mockServer.enqueueSuccessfulAuthResponse()
|
||||||
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
|
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
|
||||||
assertThat(subject.isAuthenticated()).isTrue()
|
assertThat(subject.authenticationStatuses().first()).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = HttpException::class)
|
@Test(expected = HttpException::class)
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
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_TOKEN
|
||||||
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_URL
|
import gq.kirmanak.mealie.data.auth.impl.AuthImplTestData.TEST_URL
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class AuthStorageImplTest : HiltRobolectricTest() {
|
class AuthStorageImplTest : HiltRobolectricTest() {
|
||||||
@Inject
|
@Inject
|
||||||
@@ -39,4 +42,15 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
|||||||
fun `when reading url without storing data then returns null`() = runBlocking {
|
fun `when reading url without storing data then returns null`() = runBlocking {
|
||||||
assertThat(subject.getBaseUrl()).isNull()
|
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