diff --git a/app/build.gradle b/app/build.gradle index da8da87..62e7704 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,9 +120,6 @@ dependencies { // https://github.com/Kotlin/kotlinx.serialization/releases implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" - // https://developer.android.com/jetpack/androidx/releases/preference - implementation "androidx.preference:preference-ktx:1.2.0" - // https://github.com/JakeWharton/timber/releases implementation 'com.jakewharton.timber:timber:5.0.1' @@ -145,6 +142,12 @@ dependencies { // https://github.com/square/picasso/releases implementation "com.squareup.picasso:picasso:2.8" + // https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases + implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6" + + // https://developer.android.com/topic/libraries/architecture/datastore + implementation "androidx.datastore:datastore-preferences:1.0.0" + // https://github.com/junit-team/junit4/releases testImplementation "junit:junit:4.13.2" @@ -163,9 +166,6 @@ dependencies { // https://mockk.io/ testImplementation "io.mockk:mockk:1.12.3" - // https://github.com/androidbroadcast/ViewBindingPropertyDelegate/releases - implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6" - // https://github.com/facebook/flipper/releases def flipper_version = "0.140.0" debugImplementation "com.facebook.flipper:flipper:$flipper_version" diff --git a/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt b/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt index 12b6d63..d35f6e6 100644 --- a/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt +++ b/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt @@ -25,54 +25,54 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DebugModule { - @Provides - @Singleton - @IntoSet - fun provideLoggingInterceptor(): Interceptor { - val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) } - interceptor.level = HttpLoggingInterceptor.Level.BODY - return interceptor - } + @Provides + @Singleton + @IntoSet + fun provideLoggingInterceptor(): Interceptor { + val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) } + interceptor.level = HttpLoggingInterceptor.Level.BODY + return interceptor + } - @Provides - @Singleton - @IntoSet - fun provideFlipperInterceptor(networkFlipperPlugin: NetworkFlipperPlugin): Interceptor { - return FlipperOkhttpInterceptor(networkFlipperPlugin) - } + @Provides + @Singleton + @IntoSet + fun provideFlipperInterceptor(networkFlipperPlugin: NetworkFlipperPlugin): Interceptor { + return FlipperOkhttpInterceptor(networkFlipperPlugin) + } - @Provides - @Singleton - fun networkFlipperPlugin() = NetworkFlipperPlugin() + @Provides + @Singleton + fun networkFlipperPlugin() = NetworkFlipperPlugin() - @Provides - @Singleton - @IntoSet - fun bindNetworkFlipperPlugin(plugin: NetworkFlipperPlugin): FlipperPlugin = plugin + @Provides + @Singleton + @IntoSet + fun bindNetworkFlipperPlugin(plugin: NetworkFlipperPlugin): FlipperPlugin = plugin - @Provides - @Singleton - @IntoSet - fun sharedPreferencesPlugin(@ApplicationContext context: Context): FlipperPlugin = - SharedPreferencesFlipperPlugin(context) + @Provides + @Singleton + @IntoSet + fun sharedPreferencesPlugin(@ApplicationContext context: Context): FlipperPlugin = + SharedPreferencesFlipperPlugin(context) - @Provides - @Singleton - @IntoSet - fun leakCanaryPlugin(): FlipperPlugin { - LeakCanary.config = LeakCanary.config.copy(onHeapAnalyzedListener = FlipperLeakListener()) - return LeakCanary2FlipperPlugin() - } + @Provides + @Singleton + @IntoSet + fun leakCanaryPlugin(): FlipperPlugin { + LeakCanary.config = LeakCanary.config.copy(onHeapAnalyzedListener = FlipperLeakListener()) + return LeakCanary2FlipperPlugin() + } - @Provides - @Singleton - @IntoSet - fun databasesPlugin(@ApplicationContext context: Context): FlipperPlugin = - DatabasesFlipperPlugin(context) + @Provides + @Singleton + @IntoSet + fun databasesPlugin(@ApplicationContext context: Context): FlipperPlugin = + DatabasesFlipperPlugin(context) - @Provides - @Singleton - @IntoSet - fun inspectorPlugin(@ApplicationContext context: Context): FlipperPlugin = - InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()) + @Provides + @Singleton + @IntoSet + fun inspectorPlugin(@ApplicationContext context: Context): FlipperPlugin = + InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()) } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt index 794e69c..1426dbe 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt @@ -15,5 +15,5 @@ interface AuthRepo { fun authenticationStatuses(): Flow - fun logout() + suspend fun logout() } \ No newline at end of file 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 index 88cd966..215a5da 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt @@ -3,7 +3,7 @@ package gq.kirmanak.mealient.data.auth import kotlinx.coroutines.flow.Flow interface AuthStorage { - fun storeAuthData(authHeader: String, baseUrl: String) + suspend fun storeAuthData(authHeader: String, baseUrl: String) suspend fun getBaseUrl(): String? @@ -11,5 +11,5 @@ interface AuthStorage { fun authHeaderObservable(): Flow - fun clearAuthData() + 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 d20a9e3..89f0bfd 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 @@ -46,7 +46,7 @@ class AuthRepoImpl @Inject constructor( return storage.authHeaderObservable().map { it != null } } - override fun logout() { + override suspend fun logout() { Timber.v("logout() called") storage.clearAuthData() } 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 index 5a81763..43d4618 100644 --- 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 @@ -1,55 +1,48 @@ package gq.kirmanak.mealient.data.auth.impl -import android.content.SharedPreferences -import androidx.core.content.edit import gq.kirmanak.mealient.data.auth.AuthStorage -import gq.kirmanak.mealient.extensions.changesFlow -import gq.kirmanak.mealient.extensions.getStringOrNull +import gq.kirmanak.mealient.data.storage.PreferencesStorage import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -private const val AUTH_HEADER_KEY = "AUTH_TOKEN" -private const val BASE_URL_KEY = "BASE_URL" - @Singleton class AuthStorageImpl @Inject constructor( - private val sharedPreferences: SharedPreferences + private val preferencesStorage: PreferencesStorage, ) : AuthStorage { - override fun storeAuthData(authHeader: String, baseUrl: String) { + private val authHeaderKey by preferencesStorage::authHeaderKey + private val baseUrlKey by preferencesStorage::baseUrlKey + + override suspend fun storeAuthData(authHeader: String, baseUrl: String) { Timber.v("storeAuthData() called with: authHeader = $authHeader, baseUrl = $baseUrl") - sharedPreferences.edit { - putString(AUTH_HEADER_KEY, authHeader) - putString(BASE_URL_KEY, baseUrl) - } + preferencesStorage.storeValues( + Pair(authHeaderKey, authHeader), + Pair(baseUrlKey, baseUrl), + ) } override suspend fun getBaseUrl(): String? { - val baseUrl = sharedPreferences.getStringOrNull(BASE_URL_KEY) + val baseUrl = preferencesStorage.getValue(baseUrlKey) Timber.d("getBaseUrl: base url is $baseUrl") return baseUrl } override suspend fun getAuthHeader(): String? { Timber.v("getAuthHeader() called") - val token = sharedPreferences.getStringOrNull(AUTH_HEADER_KEY) + val token = preferencesStorage.getValue(authHeaderKey) Timber.d("getAuthHeader: header is \"$token\"") return token } override fun authHeaderObservable(): Flow { Timber.v("authHeaderObservable() called") - return sharedPreferences.changesFlow().map { it.first.getStringOrNull(AUTH_HEADER_KEY) } + return preferencesStorage.valueUpdates(authHeaderKey) } - override fun clearAuthData() { + override suspend fun clearAuthData() { Timber.v("clearAuthData() called") - sharedPreferences.edit { - remove(AUTH_HEADER_KEY) - remove(BASE_URL_KEY) - } + preferencesStorage.removeValues(authHeaderKey, baseUrlKey) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt index 7ace2de..22ffbb4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt @@ -3,5 +3,5 @@ package gq.kirmanak.mealient.data.disclaimer interface DisclaimerStorage { suspend fun isDisclaimerAccepted(): Boolean - fun acceptDisclaimer() + suspend fun acceptDisclaimer() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt index 41f8139..f7e313b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt @@ -1,29 +1,26 @@ package gq.kirmanak.mealient.data.disclaimer -import android.content.SharedPreferences -import gq.kirmanak.mealient.extensions.getBooleanOrFalse +import gq.kirmanak.mealient.data.storage.PreferencesStorage import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -private const val IS_DISCLAIMER_ACCEPTED_KEY = "IS_DISCLAIMER_ACCEPTED" - @Singleton class DisclaimerStorageImpl @Inject constructor( - private val sharedPreferences: SharedPreferences + private val preferencesStorage: PreferencesStorage, ) : DisclaimerStorage { + private val isDisclaimerAcceptedKey by preferencesStorage::isDisclaimerAcceptedKey + override suspend fun isDisclaimerAccepted(): Boolean { Timber.v("isDisclaimerAccepted() called") - val isAccepted = sharedPreferences.getBooleanOrFalse(IS_DISCLAIMER_ACCEPTED_KEY) + val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false Timber.v("isDisclaimerAccepted() returned: $isAccepted") return isAccepted } - override fun acceptDisclaimer() { + override suspend fun acceptDisclaimer() { Timber.v("acceptDisclaimer() called") - sharedPreferences.edit() - .putBoolean(IS_DISCLAIMER_ACCEPTED_KEY, true) - .apply() + preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true)) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt new file mode 100644 index 0000000..f63dc9b --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt @@ -0,0 +1,23 @@ +package gq.kirmanak.mealient.data.storage + +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.flow.Flow + +interface PreferencesStorage { + + val baseUrlKey: Preferences.Key + + val authHeaderKey: Preferences.Key + + val isDisclaimerAcceptedKey: Preferences.Key + + suspend fun getValue(key: Preferences.Key): T? + + suspend fun requireValue(key: Preferences.Key): T + + suspend fun storeValues(vararg pairs: Pair, T>) + + fun valueUpdates(key: Preferences.Key): Flow + + suspend fun removeValues(vararg keys: Preferences.Key) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt new file mode 100644 index 0000000..a382682 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt @@ -0,0 +1,60 @@ +package gq.kirmanak.mealient.data.storage + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PreferencesStorageImpl @Inject constructor( + private val dataStore: DataStore +) : PreferencesStorage { + + override val baseUrlKey = stringPreferencesKey("baseUrl") + + override val authHeaderKey = stringPreferencesKey("authHeader") + + override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted") + + override suspend fun getValue(key: Preferences.Key): T? { + val value = dataStore.data.first()[key] + Timber.v("getValue() returned: $value for $key") + return value + } + + override suspend fun requireValue(key: Preferences.Key): T = + checkNotNull(getValue(key)) { "Value at $key is null when it was required" } + + override suspend fun storeValues(vararg pairs: Pair, T>) { + Timber.v("storeValues() called with: pairs = ${pairs.contentToString()}") + dataStore.edit { preferences -> + pairs.forEach { preferences += it.toPreferencesPair() } + } + } + + override fun valueUpdates(key: Preferences.Key): Flow { + Timber.v("valueUpdates() called with: key = $key") + return dataStore.data + .map { it[key] } + .distinctUntilChanged() + .onEach { Timber.d("valueUpdates: new value at $key is $it") } + .onCompletion { Timber.i(it, "valueUpdates: finished") } + } + + override suspend fun removeValues(vararg keys: Preferences.Key) { + Timber.v("removeValues() called with: key = ${keys.contentToString()}") + dataStore.edit { preferences -> + keys.forEach { preferences -= it } + } + } +} + +private fun Pair, T>.toPreferencesPair(): Preferences.Pair { + val (key, value) = this + return key to value +} diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AppModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AppModule.kt index 3418f3e..43b19a5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AppModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AppModule.kt @@ -1,27 +1,39 @@ package gq.kirmanak.mealient.di import android.content.Context -import android.content.SharedPreferences -import androidx.preference.PreferenceManager +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile import androidx.room.Room +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.data.AppDb +import gq.kirmanak.mealient.data.storage.PreferencesStorage +import gq.kirmanak.mealient.data.storage.PreferencesStorageImpl import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object AppModule { - @Provides - @Singleton - fun createDb(@ApplicationContext context: Context): AppDb = - Room.databaseBuilder(context, AppDb::class.java, "app.db").build() +interface AppModule { + companion object { - @Provides + @Provides + @Singleton + fun createDb(@ApplicationContext context: Context): AppDb = + Room.databaseBuilder(context, AppDb::class.java, "app.db").build() + + @Provides + @Singleton + fun provideDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create { context.preferencesDataStoreFile("settings") } + } + + @Binds @Singleton - fun createSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context) + fun bindPreferencesStorage(preferencesStorage: PreferencesStorageImpl): PreferencesStorage } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/SharedPrefExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/SharedPrefExtensions.kt deleted file mode 100644 index edd88ca..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/SharedPrefExtensions.kt +++ /dev/null @@ -1,36 +0,0 @@ -package gq.kirmanak.mealient.extensions - -import android.content.SharedPreferences -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.onClosed -import kotlinx.coroutines.channels.onFailure -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.withContext -import timber.log.Timber - -suspend fun SharedPreferences.getStringOrNull(key: String) = - withContext(Dispatchers.IO) { getString(key, null) } - -suspend fun SharedPreferences.getBooleanOrFalse(key: String) = - withContext(Dispatchers.IO) { getBoolean(key, false) } - -@OptIn(ExperimentalCoroutinesApi::class) -fun SharedPreferences.changesFlow(): Flow> = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key -> - Timber.v("watchChanges: listener called with key $key") - trySend(prefs to key) - .onFailure { Timber.e(it, "watchChanges: can't send preference change, key $key") } - .onClosed { Timber.e(it, "watchChanges: flow has been closed") } - } - Timber.v("watchChanges: registering listener") - registerOnSharedPreferenceChangeListener(listener) - send(this@changesFlow to null) - awaitClose { - Timber.v("watchChanges: flow has been closed") - unregisterOnSharedPreferenceChangeListener(listener) - } -} - 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 9a846cd..f630531 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 @@ -38,7 +38,9 @@ class AuthenticationViewModel @Inject constructor( fun logout() { Timber.v("logout() called") - authRepo.logout() - viewModelScope.launch { recipeRepo.clearLocalData() } + viewModelScope.launch { + authRepo.logout() + recipeRepo.clearLocalData() + } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt index ea056d1..9f3535a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt @@ -36,8 +36,10 @@ class DisclaimerViewModel @Inject constructor( fun acceptDisclaimer() { Timber.v("acceptDisclaimer() called") - disclaimerStorage.acceptDisclaimer() - _isAccepted.value = true + viewModelScope.launch { + disclaimerStorage.acceptDisclaimer() + _isAccepted.value = true + } } fun startCountDown() { 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 66b0457..f7677a1 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 @@ -13,8 +13,8 @@ import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME import gq.kirmanak.mealient.test.RobolectricTest import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -102,12 +102,12 @@ class AuthRepoImplTest : RobolectricTest() { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL)) } returns TEST_TOKEN subject.authenticate(TEST_USERNAME, TEST_PASSWORD, TEST_BASE_URL) - verify { storage.storeAuthData(TEST_AUTH_HEADER, TEST_BASE_URL) } + coVerify { storage.storeAuthData(TEST_AUTH_HEADER, TEST_BASE_URL) } } @Test fun `when logout then clearAuthData is called`() = runTest { subject.logout() - verify { storage.clearAuthData() } + coVerify { storage.clearAuthData() } } } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImplTest.kt new file mode 100644 index 0000000..b250045 --- /dev/null +++ b/app/src/test/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImplTest.kt @@ -0,0 +1,47 @@ +package gq.kirmanak.mealient.data.storage + +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidTest +import gq.kirmanak.mealient.test.HiltRobolectricTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Test +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +class PreferencesStorageImplTest : HiltRobolectricTest() { + + @Inject + lateinit var subject: PreferencesStorage + + @Test + fun `when getValue without writes then null`() = runTest { + assertThat(subject.getValue(subject.authHeaderKey)).isNull() + } + + @Test(expected = IllegalStateException::class) + fun `when requireValue without writes then throws IllegalStateException`() = runTest { + subject.requireValue(subject.authHeaderKey) + } + + @Test + fun `when getValue after write then returns value`() = runTest { + subject.storeValues(Pair(subject.authHeaderKey, "test")) + assertThat(subject.getValue(subject.authHeaderKey)).isEqualTo("test") + } + + @Test + fun `when storeValue then valueUpdates emits`() = runTest { + subject.storeValues(Pair(subject.authHeaderKey, "test")) + assertThat(subject.valueUpdates(subject.authHeaderKey).first()).isEqualTo("test") + } + + @Test + fun `when remove value then getValue returns null`() = runTest { + subject.storeValues(Pair(subject.authHeaderKey, "test")) + subject.removeValues(subject.authHeaderKey) + assertThat(subject.getValue(subject.authHeaderKey)).isNull() + } +} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/extensions/RoomTypeConvertersTest.kt b/app/src/test/java/gq/kirmanak/mealient/extensions/RoomTypeConvertersTest.kt index 1068a18..240d593 100644 --- a/app/src/test/java/gq/kirmanak/mealient/extensions/RoomTypeConvertersTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/extensions/RoomTypeConvertersTest.kt @@ -1,6 +1,6 @@ package gq.kirmanak.mealient.extensions -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import org.junit.Test @@ -10,27 +10,27 @@ class RoomTypeConvertersTest { fun `when localDateTimeToTimestamp then correctly converts`() { val input = LocalDateTime.parse("2021-11-13T15:56:33") val actual = RoomTypeConverters.localDateTimeToTimestamp(input) - Truth.assertThat(actual).isEqualTo(1636818993000) + assertThat(actual).isEqualTo(1636818993000) } @Test fun `when timestampToLocalDateTime then correctly converts`() { val expected = LocalDateTime.parse("2021-11-13T15:58:38") val actual = RoomTypeConverters.timestampToLocalDateTime(1636819118000) - Truth.assertThat(actual).isEqualTo(expected) + assertThat(actual).isEqualTo(expected) } @Test fun `when localDateToTimeStamp then correctly converts`() { val input = LocalDate.parse("2021-11-13") val actual = RoomTypeConverters.localDateToTimeStamp(input) - Truth.assertThat(actual).isEqualTo(1636761600000) + assertThat(actual).isEqualTo(1636761600000) } @Test fun `when timestampToLocalDate then correctly converts`() { val expected = LocalDate.parse("2021-11-13") val actual = RoomTypeConverters.timestampToLocalDate(1636761600000) - Truth.assertThat(actual).isEqualTo(expected) + assertThat(actual).isEqualTo(expected) } } \ No newline at end of file