diff --git a/app/build.gradle b/app/build.gradle
index a390928..1717061 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,6 +2,8 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
+ // hilt specific
+ id 'dagger.hilt.android.plugin'
}
android {
@@ -25,6 +27,18 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+ flavorDimensions 'di'
+ productFlavors {
+ hilt {
+ dimension 'di'
+ applicationId "org.fnives.test.showcase.hilt"
+ testInstrumentationRunner "org.fnives.test.showcase.testutils.configuration.HiltTestRunner"
+ }
+ koin {
+ dimension 'di'
+ applicationId "org.fnives.test.showcase.koin"
+ }
+ }
buildFeatures {
viewBinding true
@@ -34,10 +48,26 @@ android {
androidTest {
java.srcDirs += "src/sharedTest/java"
}
+ androidTestHilt {
+ java.srcDirs += "src/sharedTestHilt/java"
+ }
+ androidTestKoin {
+ java.srcDirs += "src/sharedTestKoin/java"
+ }
+
test {
java.srcDirs += "src/sharedTest/java"
java.srcDirs += "src/robolectricTest/java"
}
+ testHilt {
+ java.srcDirs += "src/sharedTestHilt/java"
+ java.srcDirs += "src/robolectricTestHilt/java"
+ resources.srcDirs += "src/robolectricTestHilt/resources"
+ }
+ testKoin {
+ java.srcDirs += "src/sharedTestKoin/java"
+ java.srcDirs += "src/robolectricTestKoin/java"
+ }
}
// needed for androidTest
@@ -49,10 +79,17 @@ android {
}
}
+hilt {
+ enableAggregatingTask = true
+ enableExperimentalClasspathAggregation = true
+}
+
afterEvaluate {
// making sure the :mockserver is assembled after :clean when running tests
- testDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
- testReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
+ testKoinDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
+ testKoinReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
+ testHiltDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
+ testHiltReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
}
dependencies {
@@ -66,7 +103,12 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
// Koin
- implementation "io.insert-koin:koin-android:$koin_version"
+ koinImplementation "io.insert-koin:koin-android:$koin_version"
+
+ // Hilt
+ implementation "com.google.dagger:hilt-android:$hilt_version"
+ kaptHilt "com.google.dagger:hilt-compiler:$hilt_version"
+ hiltImplementation "androidx.activity:activity-ktx:$activity_ktx_version"
implementation "androidx.room:room-runtime:$androidx_room_version"
kapt "androidx.room:room-compiler:$androidx_room_version"
@@ -98,6 +140,8 @@ dependencies {
testImplementation "com.jakewharton.espresso:okhttp3-idling-resource:$testing_okhttp3_idling_resource_version"
testImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
+ testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
+ kaptTest "com.google.dagger:hilt-compiler:$hilt_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_version"
@@ -111,4 +155,7 @@ dependencies {
androidTestImplementation "com.jakewharton.espresso:okhttp3-idling-resource:$testing_okhttp3_idling_resource_version"
androidTestImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_version"
+ androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
+ kaptAndroidTest "com.google.dagger:hilt-compiler:$hilt_version"
+ androidTestImplementation project(":network") // hilt needs it
}
\ No newline at end of file
diff --git a/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt b/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt
new file mode 100644
index 0000000..f538425
--- /dev/null
+++ b/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt
@@ -0,0 +1,14 @@
+package org.fnives.test.showcase.testutils.configuration
+
+import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
+
+object AndroidTestServerTypeConfiguration : ServerTypeConfiguration {
+ override val useHttps: Boolean get() = true
+
+ override val url: String get() = "${MockServerScenarioSetup.HTTPS_BASE_URL}:${MockServerScenarioSetup.PORT}/"
+
+ override fun invoke(mockServerScenarioSetup: MockServerScenarioSetup) {
+ val handshakeCertificates = mockServerScenarioSetup.clientCertificates ?: return
+ HttpsConfigurationModule.handshakeCertificates = handshakeCertificates
+ }
+}
diff --git a/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/HiltTestRunner.kt b/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/HiltTestRunner.kt
new file mode 100644
index 0000000..d2b4ee5
--- /dev/null
+++ b/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/HiltTestRunner.kt
@@ -0,0 +1,12 @@
+package org.fnives.test.showcase.testutils.configuration
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class HiltTestRunner : AndroidJUnitRunner() {
+
+ override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application =
+ super.newApplication(cl, HiltTestApplication::class.java.name, context)
+}
diff --git a/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/HttpsConfigurationModule.kt b/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/HttpsConfigurationModule.kt
new file mode 100644
index 0000000..3bf93c8
--- /dev/null
+++ b/app/src/androidTestHilt/java/org/fnives/test/showcase/testutils/configuration/HttpsConfigurationModule.kt
@@ -0,0 +1,33 @@
+package org.fnives.test.showcase.testutils.configuration
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.components.SingletonComponent
+import dagger.hilt.testing.TestInstallIn
+import okhttp3.tls.HandshakeCertificates
+import org.fnives.test.showcase.hilt.SessionLessQualifier
+import org.fnives.test.showcase.network.di.hilt.BindsBaseOkHttpClient
+import org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
+import javax.inject.Singleton
+
+@Module
+@TestInstallIn(
+ components = [SingletonComponent::class],
+ replaces = [BindsBaseOkHttpClient::class]
+)
+object HttpsConfigurationModule {
+
+ lateinit var handshakeCertificates: HandshakeCertificates
+
+ @Provides
+ @Singleton
+ @SessionLessQualifier
+ fun bindsBaseOkHttpClient(enableLogging: Boolean) =
+ HiltNetworkModule.provideSessionLessOkHttpClient(enableLogging)
+ .newBuilder()
+ .sslSocketFactory(
+ handshakeCertificates.sslSocketFactory(),
+ handshakeCertificates.trustManager
+ )
+ .build()
+}
diff --git a/app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt b/app/src/androidTestKoin/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt
similarity index 100%
rename from app/src/androidTest/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt
rename to app/src/androidTestKoin/java/org/fnives/test/showcase/testutils/configuration/AndroidTestServerTypeConfiguration.kt
diff --git a/app/src/hilt/AndroidManifest.xml b/app/src/hilt/AndroidManifest.xml
new file mode 100644
index 0000000..d65911d
--- /dev/null
+++ b/app/src/hilt/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/hilt/java/org/fnives/test/showcase/TestShowcaseApplication.kt b/app/src/hilt/java/org/fnives/test/showcase/TestShowcaseApplication.kt
new file mode 100644
index 0000000..4881f5c
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/TestShowcaseApplication.kt
@@ -0,0 +1,7 @@
+package org.fnives.test.showcase
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class TestShowcaseApplication : Application()
diff --git a/app/src/hilt/java/org/fnives/test/showcase/di/AppModule.kt b/app/src/hilt/java/org/fnives/test/showcase/di/AppModule.kt
new file mode 100644
index 0000000..3e574ee
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/di/AppModule.kt
@@ -0,0 +1,52 @@
+package org.fnives.test.showcase.di
+
+import android.content.Context
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import org.fnives.test.showcase.core.session.SessionExpirationListener
+import org.fnives.test.showcase.core.storage.UserDataLocalStorage
+import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
+import org.fnives.test.showcase.session.SessionExpirationListenerImpl
+import org.fnives.test.showcase.storage.SharedPreferencesManagerImpl
+import org.fnives.test.showcase.storage.database.DatabaseInitialization
+import org.fnives.test.showcase.storage.favourite.FavouriteContentLocalStorageImpl
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+object AppModule {
+
+ @Provides
+ fun provideBaseUrl(): String = BaseUrlProvider.get().baseUrl
+
+ @Provides
+ fun enableLogging(): Boolean = true
+
+ @Singleton
+ @Provides
+ fun provideFavouriteDao(@ApplicationContext context: Context) =
+ DatabaseInitialization.create(context).favouriteDao
+
+ @Provides
+ fun provideSharedPreferencesManagerImpl(@ApplicationContext context: Context) =
+ SharedPreferencesManagerImpl.create(context)
+
+ @Singleton
+ @Provides
+ fun provideUserDataLocalStorage(
+ sharedPreferencesManagerImpl: SharedPreferencesManagerImpl
+ ): UserDataLocalStorage = sharedPreferencesManagerImpl
+
+ @Provides
+ fun provideFavouriteContentLocalStorage(
+ favouriteContentLocalStorageImpl: FavouriteContentLocalStorageImpl
+ ): FavouriteContentLocalStorage = favouriteContentLocalStorageImpl
+
+ @Provides
+ internal fun bindSessionExpirationListener(
+ sessionExpirationListenerImpl: SessionExpirationListenerImpl
+ ): SessionExpirationListener = sessionExpirationListenerImpl
+}
diff --git a/app/src/hilt/java/org/fnives/test/showcase/ui/IntentCoordinator.kt b/app/src/hilt/java/org/fnives/test/showcase/ui/IntentCoordinator.kt
new file mode 100644
index 0000000..2abf987
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/ui/IntentCoordinator.kt
@@ -0,0 +1,14 @@
+package org.fnives.test.showcase.ui
+
+import android.content.Context
+import org.fnives.test.showcase.ui.auth.HiltAuthActivity
+import org.fnives.test.showcase.ui.home.HiltMainActivity
+
+object IntentCoordinator {
+
+ fun mainActivitygetStartIntent(context: Context) =
+ HiltMainActivity.getStartIntent(context)
+
+ fun authActivitygetStartIntent(context: Context) =
+ HiltAuthActivity.getStartIntent(context)
+}
diff --git a/app/src/hilt/java/org/fnives/test/showcase/ui/ViewModelDelegate.kt b/app/src/hilt/java/org/fnives/test/showcase/ui/ViewModelDelegate.kt
new file mode 100644
index 0000000..7ef6742
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/ui/ViewModelDelegate.kt
@@ -0,0 +1,12 @@
+package org.fnives.test.showcase.ui
+
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.activity.viewModels as androidxViewModel
+
+inline fun ViewModelStoreOwner.viewModels(): Lazy =
+ when (this) {
+ is ComponentActivity -> androidxViewModel()
+ else -> throw IllegalStateException("Only supports activity viewModel for now")
+ }
diff --git a/app/src/hilt/java/org/fnives/test/showcase/ui/auth/HiltAuthActivity.kt b/app/src/hilt/java/org/fnives/test/showcase/ui/auth/HiltAuthActivity.kt
new file mode 100644
index 0000000..2574965
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/ui/auth/HiltAuthActivity.kt
@@ -0,0 +1,12 @@
+package org.fnives.test.showcase.ui.auth
+
+import android.content.Context
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class HiltAuthActivity : AuthActivity() {
+ companion object {
+ fun getStartIntent(context: Context): Intent = Intent(context, HiltAuthActivity::class.java)
+ }
+}
diff --git a/app/src/hilt/java/org/fnives/test/showcase/ui/home/HiltMainActivity.kt b/app/src/hilt/java/org/fnives/test/showcase/ui/home/HiltMainActivity.kt
new file mode 100644
index 0000000..94b0b5f
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/ui/home/HiltMainActivity.kt
@@ -0,0 +1,12 @@
+package org.fnives.test.showcase.ui.home
+
+import android.content.Context
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class HiltMainActivity : MainActivity() {
+ companion object {
+ fun getStartIntent(context: Context): Intent = Intent(context, HiltMainActivity::class.java)
+ }
+}
diff --git a/app/src/hilt/java/org/fnives/test/showcase/ui/splash/HiltSplashActivity.kt b/app/src/hilt/java/org/fnives/test/showcase/ui/splash/HiltSplashActivity.kt
new file mode 100644
index 0000000..c3af781
--- /dev/null
+++ b/app/src/hilt/java/org/fnives/test/showcase/ui/splash/HiltSplashActivity.kt
@@ -0,0 +1,6 @@
+package org.fnives.test.showcase.ui.splash
+
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class HiltSplashActivity : SplashActivity()
diff --git a/app/src/koin/AndroidManifest.xml b/app/src/koin/AndroidManifest.xml
new file mode 100644
index 0000000..72cf478
--- /dev/null
+++ b/app/src/koin/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/test/showcase/TestShowcaseApplication.kt b/app/src/koin/java/org/fnives/test/showcase/TestShowcaseApplication.kt
similarity index 100%
rename from app/src/main/java/org/fnives/test/showcase/TestShowcaseApplication.kt
rename to app/src/koin/java/org/fnives/test/showcase/TestShowcaseApplication.kt
diff --git a/app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt b/app/src/koin/java/org/fnives/test/showcase/di/createAppModules.kt
similarity index 96%
rename from app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt
rename to app/src/koin/java/org/fnives/test/showcase/di/createAppModules.kt
index 8d05715..4c20221 100644
--- a/app/src/main/java/org/fnives/test/showcase/di/createAppModules.kt
+++ b/app/src/koin/java/org/fnives/test/showcase/di/createAppModules.kt
@@ -1,6 +1,6 @@
package org.fnives.test.showcase.di
-import org.fnives.test.showcase.core.di.createCoreModule
+import org.fnives.test.showcase.core.di.koin.createCoreModule
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.session.SessionExpirationListenerImpl
import org.fnives.test.showcase.storage.LocalDatabase
diff --git a/app/src/koin/java/org/fnives/test/showcase/ui/IntentCoordinator.kt b/app/src/koin/java/org/fnives/test/showcase/ui/IntentCoordinator.kt
new file mode 100644
index 0000000..86eec94
--- /dev/null
+++ b/app/src/koin/java/org/fnives/test/showcase/ui/IntentCoordinator.kt
@@ -0,0 +1,14 @@
+package org.fnives.test.showcase.ui
+
+import android.content.Context
+import org.fnives.test.showcase.ui.auth.AuthActivity
+import org.fnives.test.showcase.ui.home.MainActivity
+
+object IntentCoordinator {
+
+ fun mainActivitygetStartIntent(context: Context) =
+ MainActivity.getStartIntent(context)
+
+ fun authActivitygetStartIntent(context: Context) =
+ AuthActivity.getStartIntent(context)
+}
diff --git a/app/src/koin/java/org/fnives/test/showcase/ui/ViewModelDelegate.kt b/app/src/koin/java/org/fnives/test/showcase/ui/ViewModelDelegate.kt
new file mode 100644
index 0000000..6ead7ed
--- /dev/null
+++ b/app/src/koin/java/org/fnives/test/showcase/ui/ViewModelDelegate.kt
@@ -0,0 +1,8 @@
+package org.fnives.test.showcase.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelStoreOwner
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+inline fun ViewModelStoreOwner.viewModels(): Lazy =
+ viewModel()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 09b554f..f778c8e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,24 +6,13 @@
-
-
-
-
-
-
-
-
-
-
+ tools:ignore="AllowBackup"/>
\ No newline at end of file
diff --git a/app/src/main/java/org/fnives/test/showcase/session/SessionExpirationListenerImpl.kt b/app/src/main/java/org/fnives/test/showcase/session/SessionExpirationListenerImpl.kt
index 230a521..b0fb00d 100644
--- a/app/src/main/java/org/fnives/test/showcase/session/SessionExpirationListenerImpl.kt
+++ b/app/src/main/java/org/fnives/test/showcase/session/SessionExpirationListenerImpl.kt
@@ -4,15 +4,20 @@ import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
+import dagger.hilt.android.qualifiers.ApplicationContext
import org.fnives.test.showcase.core.session.SessionExpirationListener
-import org.fnives.test.showcase.ui.auth.AuthActivity
+import org.fnives.test.showcase.ui.IntentCoordinator
+import javax.inject.Inject
-class SessionExpirationListenerImpl(private val context: Context) : SessionExpirationListener {
+class SessionExpirationListenerImpl @Inject constructor(
+ @ApplicationContext
+ private val context: Context
+) : SessionExpirationListener {
override fun onSessionExpired() {
Handler(Looper.getMainLooper()).post {
context.startActivity(
- AuthActivity.getStartIntent(context)
+ IntentCoordinator.authActivitygetStartIntent(context)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
diff --git a/app/src/main/java/org/fnives/test/showcase/storage/SharedPreferencesManagerImpl.kt b/app/src/main/java/org/fnives/test/showcase/storage/SharedPreferencesManagerImpl.kt
index a62af66..6835182 100644
--- a/app/src/main/java/org/fnives/test/showcase/storage/SharedPreferencesManagerImpl.kt
+++ b/app/src/main/java/org/fnives/test/showcase/storage/SharedPreferencesManagerImpl.kt
@@ -7,13 +7,20 @@ import org.fnives.test.showcase.model.session.Session
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
-class SharedPreferencesManagerImpl(private val sharedPreferences: SharedPreferences) : UserDataLocalStorage {
+class SharedPreferencesManagerImpl(
+ private val sharedPreferences: SharedPreferences
+) : UserDataLocalStorage {
override var session: Session? by SessionDelegate(SESSION_KEY)
- private class SessionDelegate(private val key: String) : ReadWriteProperty {
+ private class SessionDelegate(private val key: String) :
+ ReadWriteProperty {
- override fun setValue(thisRef: SharedPreferencesManagerImpl, property: KProperty<*>, value: Session?) {
+ override fun setValue(
+ thisRef: SharedPreferencesManagerImpl,
+ property: KProperty<*>,
+ value: Session?
+ ) {
if (value == null) {
thisRef.sharedPreferences.edit().remove(key).apply()
} else {
@@ -25,7 +32,10 @@ class SharedPreferencesManagerImpl(private val sharedPreferences: SharedPreferen
}
}
- override fun getValue(thisRef: SharedPreferencesManagerImpl, property: KProperty<*>): Session? {
+ override fun getValue(
+ thisRef: SharedPreferencesManagerImpl,
+ property: KProperty<*>
+ ): Session? {
val values = thisRef.sharedPreferences.getStringSet(key, null)?.toList()
val accessToken = values?.firstOrNull { it.startsWith(ACCESS_TOKEN_KEY) }
?.drop(ACCESS_TOKEN_KEY.length) ?: return null
diff --git a/app/src/main/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImpl.kt b/app/src/main/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImpl.kt
index ac0f9d9..f93416a 100644
--- a/app/src/main/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImpl.kt
+++ b/app/src/main/java/org/fnives/test/showcase/storage/favourite/FavouriteContentLocalStorageImpl.kt
@@ -4,8 +4,12 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
+import javax.inject.Inject
+
+class FavouriteContentLocalStorageImpl @Inject constructor(
+ private val favouriteDao: FavouriteDao
+) : FavouriteContentLocalStorage {
-class FavouriteContentLocalStorageImpl(private val favouriteDao: FavouriteDao) : FavouriteContentLocalStorage {
override fun observeFavourites(): Flow> =
favouriteDao.get().map { it.map(FavouriteEntity::contentId).map(::ContentId) }
diff --git a/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthActivity.kt b/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthActivity.kt
index 3e4157c..9e035bc 100644
--- a/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthActivity.kt
+++ b/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthActivity.kt
@@ -9,12 +9,12 @@ import androidx.core.widget.doAfterTextChanged
import com.google.android.material.snackbar.Snackbar
import org.fnives.test.showcase.R
import org.fnives.test.showcase.databinding.ActivityAuthenticationBinding
-import org.fnives.test.showcase.ui.home.MainActivity
-import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.fnives.test.showcase.ui.IntentCoordinator
+import org.fnives.test.showcase.ui.viewModels
-class AuthActivity : AppCompatActivity() {
+open class AuthActivity : AppCompatActivity() {
- private val viewModel by viewModel()
+ private val viewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -35,7 +35,7 @@ class AuthActivity : AppCompatActivity() {
}
viewModel.navigateToHome.observe(this) {
it.consume() ?: return@observe
- startActivity(MainActivity.getStartIntent(this))
+ startActivity(IntentCoordinator.mainActivitygetStartIntent(this))
finishAffinity()
}
setContentView(binding.root)
diff --git a/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthViewModel.kt b/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthViewModel.kt
index 0221f14..b5fa26f 100644
--- a/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthViewModel.kt
+++ b/app/src/main/java/org/fnives/test/showcase/ui/auth/AuthViewModel.kt
@@ -4,14 +4,17 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.ui.shared.Event
+import javax.inject.Inject
-class AuthViewModel(private val loginUseCase: LoginUseCase) : ViewModel() {
+@HiltViewModel
+class AuthViewModel @Inject constructor(private val loginUseCase: LoginUseCase) : ViewModel() {
private val _username = MutableLiveData()
val username: LiveData = _username
diff --git a/app/src/main/java/org/fnives/test/showcase/ui/home/MainActivity.kt b/app/src/main/java/org/fnives/test/showcase/ui/home/MainActivity.kt
index 187304a..3ec95c1 100644
--- a/app/src/main/java/org/fnives/test/showcase/ui/home/MainActivity.kt
+++ b/app/src/main/java/org/fnives/test/showcase/ui/home/MainActivity.kt
@@ -9,14 +9,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
import org.fnives.test.showcase.R
import org.fnives.test.showcase.databinding.ActivityMainBinding
import org.fnives.test.showcase.model.content.ContentId
-import org.fnives.test.showcase.ui.auth.AuthActivity
+import org.fnives.test.showcase.ui.IntentCoordinator
import org.fnives.test.showcase.ui.shared.VerticalSpaceItemDecoration
import org.fnives.test.showcase.ui.shared.getThemePrimaryColor
-import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.fnives.test.showcase.ui.viewModels
-class MainActivity : AppCompatActivity() {
+open class MainActivity : AppCompatActivity() {
- private val viewModel by viewModel()
+ private val viewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -45,7 +45,7 @@ class MainActivity : AppCompatActivity() {
}
viewModel.navigateToAuth.observe(this) {
it.consume() ?: return@observe
- startActivity(AuthActivity.getStartIntent(this))
+ startActivity(IntentCoordinator.authActivitygetStartIntent(this))
finishAffinity()
}
viewModel.loading.observe(this) {
diff --git a/app/src/main/java/org/fnives/test/showcase/ui/home/MainViewModel.kt b/app/src/main/java/org/fnives/test/showcase/ui/home/MainViewModel.kt
index 326ac01..502d543 100644
--- a/app/src/main/java/org/fnives/test/showcase/ui/home/MainViewModel.kt
+++ b/app/src/main/java/org/fnives/test/showcase/ui/home/MainViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
@@ -16,8 +17,10 @@ import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.ui.shared.Event
+import javax.inject.Inject
-class MainViewModel(
+@HiltViewModel
+class MainViewModel @Inject constructor(
private val getAllContentUseCase: GetAllContentUseCase,
private val logoutUseCase: LogoutUseCase,
private val fetchContentUseCase: FetchContentUseCase,
diff --git a/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashActivity.kt b/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashActivity.kt
index ab7ee68..48e1ce1 100644
--- a/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashActivity.kt
+++ b/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashActivity.kt
@@ -3,21 +3,20 @@ package org.fnives.test.showcase.ui.splash
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.fnives.test.showcase.R
-import org.fnives.test.showcase.ui.auth.AuthActivity
-import org.fnives.test.showcase.ui.home.MainActivity
-import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.fnives.test.showcase.ui.IntentCoordinator
+import org.fnives.test.showcase.ui.viewModels
-class SplashActivity : AppCompatActivity() {
+open class SplashActivity : AppCompatActivity() {
- private val viewModel by viewModel()
+ private val viewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
viewModel.navigateTo.observe(this) {
val intent = when (it.consume()) {
- SplashViewModel.NavigateTo.HOME -> MainActivity.getStartIntent(this)
- SplashViewModel.NavigateTo.AUTHENTICATION -> AuthActivity.getStartIntent(this)
+ SplashViewModel.NavigateTo.HOME -> IntentCoordinator.mainActivitygetStartIntent(this)
+ SplashViewModel.NavigateTo.AUTHENTICATION -> IntentCoordinator.authActivitygetStartIntent(this)
null -> return@observe
}
startActivity(intent)
diff --git a/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashViewModel.kt b/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashViewModel.kt
index d9bbd43..13a1887 100644
--- a/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashViewModel.kt
+++ b/app/src/main/java/org/fnives/test/showcase/ui/splash/SplashViewModel.kt
@@ -4,12 +4,15 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.ui.shared.Event
+import javax.inject.Inject
-class SplashViewModel(isUserLoggedInUseCase: IsUserLoggedInUseCase) : ViewModel() {
+@HiltViewModel
+class SplashViewModel @Inject constructor(isUserLoggedInUseCase: IsUserLoggedInUseCase) : ViewModel() {
private val _navigateTo = MutableLiveData>()
val navigateTo: LiveData> = _navigateTo
diff --git a/app/src/robolectricTestHilt/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt b/app/src/robolectricTestHilt/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt
new file mode 100644
index 0000000..7166b68
--- /dev/null
+++ b/app/src/robolectricTestHilt/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt
@@ -0,0 +1,97 @@
+package org.fnives.test.showcase.favourite
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
+import org.fnives.test.showcase.model.content.ContentId
+import org.fnives.test.showcase.storage.database.DatabaseInitialization
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@HiltAndroidTest
+internal class FavouriteContentLocalStorageImplTest {
+
+ @get:Rule
+ val hiltRule = HiltAndroidRule(this)
+
+ @Inject
+ lateinit var sut: FavouriteContentLocalStorage
+ private lateinit var testDispatcher: TestCoroutineDispatcher
+
+ @Before
+ fun setUp() {
+ testDispatcher = TestCoroutineDispatcher()
+ DatabaseInitialization.dispatcher = testDispatcher
+ hiltRule.inject()
+ }
+
+ @Test
+ fun GIVEN_content_id_WHEN_added_to_Favourite_THEN_it_can_be_read_out() = runBlocking {
+ val expected = listOf(ContentId("a"))
+
+ sut.markAsFavourite(ContentId("a"))
+ val actual = sut.observeFavourites().first()
+
+ Assert.assertEquals(expected, actual)
+ }
+
+ @Test
+ fun GIVEN_content_id_added_WHEN_removed_to_Favourite_THEN_it_no_longer_can_be_read_out() =
+ runBlocking {
+ val expected = listOf()
+ sut.markAsFavourite(ContentId("b"))
+
+ sut.deleteAsFavourite(ContentId("b"))
+ val actual = sut.observeFavourites().first()
+
+ Assert.assertEquals(expected, actual)
+ }
+
+ @Test
+ fun GIVEN_empty_database_WHILE_observing_content_WHEN_favourite_added_THEN_change_is_emitted() =
+ runBlocking {
+ val expected = listOf(listOf(), listOf(ContentId("a")))
+
+ val testDispatcher = TestCoroutineDispatcher()
+ val actual = async(testDispatcher) {
+ sut.observeFavourites().take(2).toList()
+ }
+ testDispatcher.advanceUntilIdle()
+
+ sut.markAsFavourite(ContentId("a"))
+
+ Assert.assertEquals(expected, actual.await())
+ }
+
+ @Test
+ fun GIVEN_non_empty_database_WHILE_observing_content_WHEN_favourite_removed_THEN_change_is_emitted() =
+ runBlocking {
+ val expected = listOf(listOf(ContentId("a")), listOf())
+ sut.markAsFavourite(ContentId("a"))
+
+ val testDispatcher = TestCoroutineDispatcher()
+ val actual = async(testDispatcher) {
+ sut.observeFavourites().take(2).toList()
+ }
+ testDispatcher.advanceUntilIdle()
+
+ sut.deleteAsFavourite(ContentId("a"))
+
+ Assert.assertEquals(expected, actual.await())
+ }
+}
diff --git a/app/src/robolectricTestHilt/resources/org/fnives/test/showcase/robolectric.properties b/app/src/robolectricTestHilt/resources/org/fnives/test/showcase/robolectric.properties
new file mode 100644
index 0000000..cd922e4
--- /dev/null
+++ b/app/src/robolectricTestHilt/resources/org/fnives/test/showcase/robolectric.properties
@@ -0,0 +1,4 @@
+sdk=28
+shadows=org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
+instrumentedPackages=androidx.loader.content
+application=dagger.hilt.android.testing.HiltTestApplication
diff --git a/app/src/robolectricTest/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt b/app/src/robolectricTestKoin/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt
similarity index 100%
rename from app/src/robolectricTest/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt
rename to app/src/robolectricTestKoin/java/org/fnives/test/showcase/favourite/FavouriteContentLocalStorageImplTest.kt
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
index 4e978ee..ce25260 100644
--- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
+++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
@@ -1,31 +1,31 @@
-package org.fnives.test.showcase.testutils.statesetup
-
-import kotlinx.coroutines.runBlocking
-import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
-import org.fnives.test.showcase.core.login.LoginUseCase
-import org.fnives.test.showcase.core.login.LogoutUseCase
-import org.fnives.test.showcase.model.auth.LoginCredentials
-import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
-import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
-import org.koin.test.KoinTest
-import org.koin.test.get
-
-object SetupLoggedInState : KoinTest {
-
- private val logoutUseCase get() = get()
- private val loginUseCase get() = get()
- private val isUserLoggedInUseCase get() = get()
-
- fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
- mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
- runBlocking {
- loginUseCase.invoke(LoginCredentials("a", "b"))
- }
- }
-
- fun isLoggedIn() = isUserLoggedInUseCase.invoke()
-
- fun setupLogout() {
- runBlocking { logoutUseCase.invoke() }
- }
-}
+// package org.fnives.test.showcase.testutils.statesetup
+//
+// import kotlinx.coroutines.runBlocking
+// import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
+// import org.fnives.test.showcase.core.login.LoginUseCase
+// import org.fnives.test.showcase.core.login.LogoutUseCase
+// import org.fnives.test.showcase.model.auth.LoginCredentials
+// import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
+// import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
+// import org.koin.test.KoinTest
+// import org.koin.test.get
+//
+// object SetupLoggedInState : KoinTest {
+//
+// private val logoutUseCase get() = get()
+// private val loginUseCase get() = get()
+// private val isUserLoggedInUseCase get() = get()
+//
+// fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
+// mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
+// runBlocking {
+// loginUseCase.invoke(LoginCredentials("a", "b"))
+// }
+// }
+//
+// fun isLoggedIn() = isUserLoggedInUseCase.invoke()
+//
+// fun setupLogout() {
+// runBlocking { logoutUseCase.invoke() }
+// }
+// }
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt
index 4f6dbfa..67fbdb4 100644
--- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt
+++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/HomeRobot.kt
@@ -21,14 +21,14 @@ import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.PullToRefresh
import org.fnives.test.showcase.testutils.viewactions.WithDrawable
import org.fnives.test.showcase.testutils.viewactions.notIntended
-import org.fnives.test.showcase.ui.auth.AuthActivity
+import org.fnives.test.showcase.ui.ActivityClassHolder
import org.hamcrest.Matchers.allOf
class HomeRobot : Robot {
override fun init() {
Intents.init()
- Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
+ Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null))
}
@@ -37,11 +37,11 @@ class HomeRobot : Robot {
}
fun assertNavigatedToAuth() = apply {
- Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
+ Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
fun assertDidNotNavigateToAuth() = apply {
- notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
+ notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
fun clickSignOut() = apply {
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt
index f291677..37b0bef 100644
--- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt
+++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/LoginRobot.kt
@@ -21,7 +21,7 @@ import org.fnives.test.showcase.testutils.configuration.SpecificTestConfiguratio
import org.fnives.test.showcase.testutils.configuration.TestConfigurationsFactory
import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended
-import org.fnives.test.showcase.ui.home.MainActivity
+import org.fnives.test.showcase.ui.ActivityClassHolder
import org.hamcrest.core.IsNot.not
class LoginRobot(
@@ -37,7 +37,7 @@ class LoginRobot(
override fun init() {
Intents.init()
- intending(hasComponent(MainActivity::class.java.canonicalName))
+ intending(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
@@ -91,10 +91,10 @@ class LoginRobot(
}
fun assertNavigatedToHome() = apply {
- intended(hasComponent(MainActivity::class.java.canonicalName))
+ intended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
fun assertNotNavigatedToHome() = apply {
- notIntended(hasComponent(MainActivity::class.java.canonicalName))
+ notIntended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
}
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt
index ae89da1..c612228 100644
--- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt
+++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashRobot.kt
@@ -5,16 +5,15 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended
-import org.fnives.test.showcase.ui.auth.AuthActivity
-import org.fnives.test.showcase.ui.home.MainActivity
+import org.fnives.test.showcase.ui.ActivityClassHolder
class SplashRobot : Robot {
override fun init() {
Intents.init()
- Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
+ Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null))
- Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
+ Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null))
}
@@ -23,18 +22,18 @@ class SplashRobot : Robot {
}
fun assertHomeIsStarted() = apply {
- Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
+ Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
fun assertHomeIsNotStarted() = apply {
- notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
+ notIntended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
}
fun assertAuthIsStarted() = apply {
- Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
+ Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
fun assertAuthIsNotStarted() = apply {
- notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
+ notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
}
}
diff --git a/app/src/sharedTestHilt/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt b/app/src/sharedTestHilt/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt
new file mode 100644
index 0000000..5cef5c9
--- /dev/null
+++ b/app/src/sharedTestHilt/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt
@@ -0,0 +1,42 @@
+package org.fnives.test.showcase.testutils.idling
+
+import androidx.annotation.CheckResult
+import androidx.test.espresso.IdlingResource
+import com.jakewharton.espresso.OkHttp3IdlingResource
+import okhttp3.OkHttpClient
+import org.fnives.test.showcase.hilt.SessionLessQualifier
+import org.fnives.test.showcase.hilt.SessionQualifier
+import javax.inject.Inject
+
+class NetworkSynchronization @Inject constructor(
+ @SessionQualifier
+ private val sessionOkhttpClient: OkHttpClient,
+ @SessionLessQualifier
+ private val sessionlessOkhttpClient: OkHttpClient
+) {
+
+ @CheckResult
+ fun registerNetworkingSynchronization(): Disposable {
+ val idlingResources = OkHttpClientTypes.values()
+ .map { it to getOkHttpClient(it) }
+ .associateBy { it.second.dispatcher }
+ .values
+ .map { (key, client) -> client.asIdlingResource(key.qualifier) }
+ .map(::IdlingResourceDisposable)
+
+ return CompositeDisposable(idlingResources)
+ }
+
+ private fun getOkHttpClient(type: OkHttpClientTypes): OkHttpClient =
+ when (type) {
+ OkHttpClientTypes.SESSION -> sessionOkhttpClient
+ OkHttpClientTypes.SESSIONLESS -> sessionlessOkhttpClient
+ }
+
+ private fun OkHttpClient.asIdlingResource(name: String): IdlingResource =
+ OkHttp3IdlingResource.create(name, this)
+
+ enum class OkHttpClientTypes(val qualifier: String) {
+ SESSION("SESSION-NETWORKING"), SESSIONLESS("SESSIONLESS-NETWORKING")
+ }
+}
diff --git a/app/src/sharedTestHilt/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt b/app/src/sharedTestHilt/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
new file mode 100644
index 0000000..04d165d
--- /dev/null
+++ b/app/src/sharedTestHilt/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
@@ -0,0 +1,30 @@
+package org.fnives.test.showcase.testutils.statesetup
+
+import kotlinx.coroutines.runBlocking
+import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
+import org.fnives.test.showcase.core.login.LoginUseCase
+import org.fnives.test.showcase.core.login.LogoutUseCase
+import org.fnives.test.showcase.model.auth.LoginCredentials
+import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
+import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
+import javax.inject.Inject
+
+class SetupLoggedInState @Inject constructor(
+ private val logoutUseCase: LogoutUseCase,
+ private val loginUseCase: LoginUseCase,
+ private val isUserLoggedInUseCase: IsUserLoggedInUseCase
+) {
+
+ fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
+ mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
+ runBlocking {
+ loginUseCase.invoke(LoginCredentials("a", "b"))
+ }
+ }
+
+ fun isLoggedIn() = isUserLoggedInUseCase.invoke()
+
+ fun setupLogout() {
+ runBlocking { logoutUseCase.invoke() }
+ }
+}
diff --git a/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/ActivityClassHolder.kt b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/ActivityClassHolder.kt
new file mode 100644
index 0000000..5e1aaca
--- /dev/null
+++ b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/ActivityClassHolder.kt
@@ -0,0 +1,14 @@
+package org.fnives.test.showcase.ui
+
+import org.fnives.test.showcase.ui.auth.HiltAuthActivity
+import org.fnives.test.showcase.ui.home.HiltMainActivity
+import org.fnives.test.showcase.ui.splash.HiltSplashActivity
+
+object ActivityClassHolder {
+
+ fun authActivity() = HiltAuthActivity::class
+
+ fun mainActivity() = HiltMainActivity::class
+
+ fun splashActivity() = HiltSplashActivity::class
+}
diff --git a/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt
new file mode 100644
index 0000000..6266263
--- /dev/null
+++ b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt
@@ -0,0 +1,248 @@
+package org.fnives.test.showcase.ui.home
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.fnives.test.showcase.model.content.FavouriteContent
+import org.fnives.test.showcase.network.mockserver.ContentData
+import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
+import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
+import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule
+import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
+import org.fnives.test.showcase.testutils.idling.Disposable
+import org.fnives.test.showcase.testutils.idling.NetworkSynchronization
+import org.fnives.test.showcase.testutils.idling.loopMainThreadFor
+import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources
+import org.fnives.test.showcase.testutils.robot.RobotTestRule
+import org.fnives.test.showcase.testutils.statesetup.SetupLoggedInState
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+@RunWith(AndroidJUnit4::class)
+@HiltAndroidTest
+class MainActivityTest {
+
+ private lateinit var activityScenario: ActivityScenario
+
+ @Rule
+ @JvmField
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Rule
+ @JvmField
+ val snackbarVerificationTestRule = SpecificTestConfigurationsFactory.createSnackbarVerification()
+
+ @Rule
+ @JvmField
+ val robotRule = RobotTestRule(HomeRobot())
+ private val homeRobot get() = robotRule.robot
+
+ @Rule
+ @JvmField
+ val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
+
+ @Rule
+ @JvmField
+ val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
+
+ @Rule
+ @JvmField
+ val hiltRule = HiltAndroidRule(this)
+
+ @Inject
+ lateinit var setupLoggedInState: SetupLoggedInState
+
+ @Inject
+ lateinit var networkSynchronization: NetworkSynchronization
+
+ private lateinit var disposable: Disposable
+
+ @Before
+ fun setUp() {
+ SpecificTestConfigurationsFactory.createServerTypeConfiguration()
+ .invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
+
+ hiltRule.inject()
+ setupLoggedInState.setupLogin(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
+ disposable = networkSynchronization.registerNetworkingSynchronization()
+ }
+
+ @After
+ fun tearDown() {
+ activityScenario.moveToState(Lifecycle.State.DESTROYED)
+ disposable.dispose()
+ }
+
+ @Test
+ fun GIVEN_initialized_MainActivity_WHEN_signout_is_clicked_THEN_user_is_signed_out() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Error(false))
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ homeRobot.clickSignOut()
+ mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
+
+ homeRobot.assertNavigatedToAuth()
+ Assert.assertEquals(false, setupLoggedInState.isLoggedIn())
+ }
+
+ @Test
+ fun GIVEN_success_response_WHEN_data_is_returned_THEN_it_is_shown_on_the_ui() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Success(false))
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ ContentData.contentSuccess.forEach {
+ homeRobot.assertContainsItem(FavouriteContent(it, false))
+ }
+ homeRobot.assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_success_response_WHEN_item_is_clicked_THEN_ui_is_updated() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Success(false))
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
+ homeRobot.assertContainsItem(expectedItem)
+ .assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_success_response_WHEN_item_is_clicked_THEN_ui_is_updated_even_if_activity_is_recreated() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Success(false))
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
+
+ activityScenario.moveToState(Lifecycle.State.DESTROYED)
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ homeRobot.assertContainsItem(expectedItem)
+ .assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_success_response_WHEN_item_is_clicked_then_clicked_again_THEN_ui_is_updated() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Success(false))
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ homeRobot.clickOnContentItem(ContentData.contentSuccess.first())
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), false)
+ homeRobot.assertContainsItem(expectedItem)
+ .assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_error_response_WHEN_loaded_THEN_error_is_Shown() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Error(false))
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ homeRobot.assertContainsNoItems()
+ .assertContainsError()
+ .assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_error_response_then_success_WHEN_retried_THEN_success_is_shown() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(
+ ContentScenario.Error(false)
+ .then(ContentScenario.Success(false))
+ )
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ homeRobot.swipeRefresh()
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ loopMainThreadFor(2000L)
+
+ ContentData.contentSuccess.forEach {
+ homeRobot.assertContainsItem(FavouriteContent(it, false))
+ }
+ homeRobot.assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_success_then_error_WHEN_retried_THEN_error_is_shown() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(
+ ContentScenario.Success(false)
+ .then(ContentScenario.Error(false))
+ )
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ homeRobot.swipeRefresh()
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ loopMainThreadUntilIdleWithIdlingResources()
+ mainDispatcherTestRule.advanceTimeBy(1000L)
+ loopMainThreadFor(1000)
+
+ homeRobot
+ .assertContainsError()
+ .assertContainsNoItems()
+ .assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_unauthenticated_then_success_WHEN_loaded_THEN_success_is_shown() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(
+ ContentScenario.Unauthorized(false)
+ .then(ContentScenario.Success(true))
+ )
+ .setScenario(RefreshTokenScenario.Success)
+
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ ContentData.contentSuccess.forEach {
+ homeRobot.assertContainsItem(FavouriteContent(it, false))
+ }
+ homeRobot.assertDidNotNavigateToAuth()
+ }
+
+ @Test
+ fun GIVEN_unauthenticated_then_error_WHEN_loaded_THEN_navigated_to_auth() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup
+ .setScenario(ContentScenario.Unauthorized(false))
+ .setScenario(RefreshTokenScenario.Error)
+
+ activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+
+ homeRobot.assertNavigatedToAuth()
+ Assert.assertEquals(false, setupLoggedInState.isLoggedIn())
+ }
+}
diff --git a/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt
new file mode 100644
index 0000000..3183967
--- /dev/null
+++ b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt
@@ -0,0 +1,165 @@
+package org.fnives.test.showcase.ui.login
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.fnives.test.showcase.R
+import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
+import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule
+import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
+import org.fnives.test.showcase.testutils.idling.Disposable
+import org.fnives.test.showcase.testutils.idling.NetworkSynchronization
+import org.fnives.test.showcase.testutils.robot.RobotTestRule
+import org.fnives.test.showcase.ui.auth.HiltAuthActivity
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+@RunWith(AndroidJUnit4::class)
+@HiltAndroidTest
+class AuthActivityTest {
+
+ private lateinit var activityScenario: ActivityScenario
+
+ @Rule
+ @JvmField
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Rule
+ @JvmField
+ val snackbarVerificationTestRule = SpecificTestConfigurationsFactory.createSnackbarVerification()
+
+ @Rule
+ @JvmField
+ val robotRule = RobotTestRule(LoginRobot())
+ private val loginRobot get() = robotRule.robot
+
+ @Rule
+ @JvmField
+ val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
+
+ @Rule
+ @JvmField
+ val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
+
+ @Rule
+ @JvmField
+ val hiltRule = HiltAndroidRule(this)
+
+ @Inject
+ lateinit var networkSynchronization: NetworkSynchronization
+
+ private lateinit var disposable: Disposable
+
+ @Before
+ fun setUp() {
+ SpecificTestConfigurationsFactory.createServerTypeConfiguration()
+ .invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
+ hiltRule.inject()
+ disposable = networkSynchronization.registerNetworkingSynchronization()
+ }
+
+ @After
+ fun tearDown() {
+ activityScenario.moveToState(Lifecycle.State.DESTROYED)
+ disposable.dispose()
+ }
+
+ @Test
+ fun GIVEN_non_empty_password_and_username_and_successful_response_WHEN_signIn_THEN_no_error_is_shown_and_navigating_to_home() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
+ AuthScenario.Success(
+ password = "alma",
+ username = "banan"
+ )
+ )
+ activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
+ loginRobot
+ .setPassword("alma")
+ .setUsername("banan")
+ .assertPassword("alma")
+ .assertUsername("banan")
+ .clickOnLogin()
+ .assertLoadingBeforeRequests()
+
+ mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
+ loginRobot.assertNavigatedToHome()
+ }
+
+ @Test
+ fun GIVEN_empty_password_and_username_WHEN_signIn_THEN_error_password_is_shown() {
+ activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
+ loginRobot
+ .setUsername("banan")
+ .assertUsername("banan")
+ .clickOnLogin()
+ .assertLoadingBeforeRequests()
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ loginRobot.assertErrorIsShown(R.string.password_is_invalid)
+ .assertNotNavigatedToHome()
+ .assertNotLoading()
+ }
+
+ @Test
+ fun GIVEN_password_and_empty_username_WHEN_signIn_THEN_error_username_is_shown() {
+ activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
+ loginRobot
+ .setPassword("banan")
+ .assertPassword("banan")
+ .clickOnLogin()
+ .assertLoadingBeforeRequests()
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ loginRobot.assertErrorIsShown(R.string.username_is_invalid)
+ .assertNotNavigatedToHome()
+ .assertNotLoading()
+ }
+
+ @Test
+ fun GIVEN_password_and_username_and_invalid_credentials_response_WHEN_signIn_THEN_error_invalid_credentials_is_shown() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
+ AuthScenario.InvalidCredentials(username = "alma", password = "banan")
+ )
+ activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
+ loginRobot
+ .setUsername("alma")
+ .setPassword("banan")
+ .assertUsername("alma")
+ .assertPassword("banan")
+ .clickOnLogin()
+ .assertLoadingBeforeRequests()
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ loginRobot.assertErrorIsShown(R.string.credentials_invalid)
+ .assertNotNavigatedToHome()
+ .assertNotLoading()
+ }
+
+ @Test
+ fun GIVEN_password_and_username_and_error_response_WHEN_signIn_THEN_error_invalid_credentials_is_shown() {
+ mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
+ AuthScenario.GenericError(username = "alma", password = "banan")
+ )
+ activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
+ loginRobot
+ .setUsername("alma")
+ .setPassword("banan")
+ .assertUsername("alma")
+ .assertPassword("banan")
+ .clickOnLogin()
+ .assertLoadingBeforeRequests()
+
+ mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
+ loginRobot.assertErrorIsShown(R.string.something_went_wrong)
+ .assertNotNavigatedToHome()
+ .assertNotLoading()
+ }
+}
diff --git a/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt
new file mode 100644
index 0000000..788e0a2
--- /dev/null
+++ b/app/src/sharedTestHilt/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt
@@ -0,0 +1,99 @@
+package org.fnives.test.showcase.ui.splash
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Lifecycle
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.fnives.test.showcase.testutils.MockServerScenarioSetupTestRule
+import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
+import org.fnives.test.showcase.testutils.idling.Disposable
+import org.fnives.test.showcase.testutils.idling.NetworkSynchronization
+import org.fnives.test.showcase.testutils.robot.RobotTestRule
+import org.fnives.test.showcase.testutils.statesetup.SetupLoggedInState
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.koin.test.KoinTest
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+@RunWith(AndroidJUnit4::class)
+@HiltAndroidTest
+class SplashActivityTest : KoinTest {
+
+ private var activityScenario: ActivityScenario? = null
+
+ private val splashRobot: SplashRobot get() = robotTestRule.robot
+
+ @Rule
+ @JvmField
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ @Rule
+ @JvmField
+ val robotTestRule = RobotTestRule(SplashRobot())
+
+ @Rule
+ @JvmField
+ val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
+
+ @Rule
+ @JvmField
+ val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
+
+ @Rule
+ @JvmField
+ val hiltRule = HiltAndroidRule(this)
+
+ @Inject
+ lateinit var setupLoggedInState: SetupLoggedInState
+
+ @Inject
+ lateinit var networkSynchronization: NetworkSynchronization
+
+ var disposable: Disposable? = null
+
+ @Before
+ fun setUp() {
+ SpecificTestConfigurationsFactory.createServerTypeConfiguration()
+ .invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
+ hiltRule.inject()
+ disposable = networkSynchronization.registerNetworkingSynchronization()
+ }
+
+ @After
+ fun tearDown() {
+ activityScenario?.moveToState(Lifecycle.State.DESTROYED)
+ disposable?.dispose()
+ }
+
+ @Test
+ fun GIVEN_loggedInState_WHEN_opened_THEN_MainActivity_is_started() {
+ setupLoggedInState.setupLogin(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
+
+ activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
+
+ mainDispatcherTestRule.advanceTimeBy(500)
+
+ splashRobot.assertHomeIsStarted()
+ .assertAuthIsNotStarted()
+
+ setupLoggedInState.setupLogout()
+ }
+
+ @Test
+ fun GIVEN_loggedOffState_WHEN_opened_THEN_AuthActivity_is_started() {
+ setupLoggedInState.setupLogout()
+
+ activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
+
+ mainDispatcherTestRule.advanceTimeBy(500)
+
+ splashRobot.assertAuthIsStarted()
+ .assertHomeIsNotStarted()
+ }
+}
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt
similarity index 89%
rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt
rename to app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt
index 5e65a0f..50b1427 100644
--- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt
+++ b/app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/ReloadKoinModulesIfNecessaryTestRule.kt
@@ -17,7 +17,8 @@ class ReloadKoinModulesIfNecessaryTestRule : TestRule {
object : Statement() {
override fun evaluate() {
if (GlobalContext.getOrNull() == null) {
- val application = ApplicationProvider.getApplicationContext()
+ val application =
+ ApplicationProvider.getApplicationContext()
startKoin {
androidContext(application)
modules(createAppModules(BaseUrlProvider.get()))
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt
similarity index 100%
rename from app/src/sharedTest/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt
rename to app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/idling/NetworkSynchronization.kt
diff --git a/app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
new file mode 100644
index 0000000..4e978ee
--- /dev/null
+++ b/app/src/sharedTestKoin/java/org/fnives/test/showcase/testutils/statesetup/SetupLoggedInState.kt
@@ -0,0 +1,31 @@
+package org.fnives.test.showcase.testutils.statesetup
+
+import kotlinx.coroutines.runBlocking
+import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
+import org.fnives.test.showcase.core.login.LoginUseCase
+import org.fnives.test.showcase.core.login.LogoutUseCase
+import org.fnives.test.showcase.model.auth.LoginCredentials
+import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
+import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
+import org.koin.test.KoinTest
+import org.koin.test.get
+
+object SetupLoggedInState : KoinTest {
+
+ private val logoutUseCase get() = get()
+ private val loginUseCase get() = get()
+ private val isUserLoggedInUseCase get() = get()
+
+ fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
+ mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
+ runBlocking {
+ loginUseCase.invoke(LoginCredentials("a", "b"))
+ }
+ }
+
+ fun isLoggedIn() = isUserLoggedInUseCase.invoke()
+
+ fun setupLogout() {
+ runBlocking { logoutUseCase.invoke() }
+ }
+}
diff --git a/app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/ActivityClassHolder.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/ActivityClassHolder.kt
new file mode 100644
index 0000000..357b766
--- /dev/null
+++ b/app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/ActivityClassHolder.kt
@@ -0,0 +1,14 @@
+package org.fnives.test.showcase.ui
+
+import org.fnives.test.showcase.ui.auth.AuthActivity
+import org.fnives.test.showcase.ui.home.MainActivity
+import org.fnives.test.showcase.ui.splash.SplashActivity
+
+object ActivityClassHolder {
+
+ fun authActivity() = AuthActivity::class
+
+ fun mainActivity() = MainActivity::class
+
+ fun splashActivity() = SplashActivity::class
+}
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt
similarity index 100%
rename from app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt
rename to app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt
similarity index 100%
rename from app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt
rename to app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt
diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt b/app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt
similarity index 100%
rename from app/src/sharedTest/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt
rename to app/src/sharedTestKoin/java/org/fnives/test/showcase/ui/splash/SplashActivityTest.kt
diff --git a/app/src/test/java/org/fnives/test/showcase/di/DITest.kt b/app/src/testKoin/java/org/fnives/test/showcase/di/DITest.kt
similarity index 100%
rename from app/src/test/java/org/fnives/test/showcase/di/DITest.kt
rename to app/src/testKoin/java/org/fnives/test/showcase/di/DITest.kt
diff --git a/build.gradle b/build.gradle
index c09cd78..35fcf1d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,12 +2,14 @@
buildscript {
ext.kotlin_version = "1.5.30"
ext.detekt_version = "1.18.1"
+ ext.hilt_version = "2.38.1"
repositories {
mavenCentral()
google()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
+ classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0"
@@ -22,6 +24,15 @@ allprojects {
repositories {
mavenCentral()
google()
+ maven {
+ url "https://maven.pkg.github.com/fknives/ReloadableHiltModule"
+ credentials {
+ username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
+ password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
+ }
+ // how to get token
+ // https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token
+ }
}
}
@@ -29,12 +40,12 @@ task clean(type: Delete) {
delete rootProject.buildDir
}
-task unitTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]){
+task unitTests(dependsOn: ["app:testKoinDebugUnitTest", "app:testHiltDebugUnitTest", "core:test", "network:test"]){
group = 'Tests'
description = 'Run all unit tests'
}
-task androidTests(dependsOn: "app:connectedAndroidTest"){
+task androidTests(dependsOn: ["app:connectedKoinDebugAndroidTest", "app:connectedHiltDebugAndroidTest"]){
group = 'Tests'
description = 'Run all Android tests'
}
diff --git a/core/build.gradle b/core/build.gradle
index 745175d..5789c72 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -1,6 +1,7 @@
plugins {
id 'java-library'
id 'kotlin'
+ id 'kotlin-kapt'
}
java {
@@ -14,15 +15,27 @@ compileKotlin {
}
}
+kapt {
+ correctErrorTypes = true
+}
+
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api project(":model")
implementation project(":network")
+ // hilt
+ implementation "com.google.dagger:hilt-core:$hilt_version"
+ kapt "com.google.dagger:hilt-compiler:$hilt_version"
+ implementation "org.fnives.library.reloadable.module:annotation:$reloadable_module_version"
+ kapt "org.fnives.library.reloadable.module:annotation-processor:$reloadable_module_version"
+
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"
testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
+ kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
+ testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
}
\ No newline at end of file
diff --git a/core/src/main/java/org/fnives/test/showcase/core/content/AddContentToFavouriteUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/content/AddContentToFavouriteUseCase.kt
index c5ea010..2483d1c 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/content/AddContentToFavouriteUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/content/AddContentToFavouriteUseCase.kt
@@ -2,8 +2,9 @@ package org.fnives.test.showcase.core.content
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
+import javax.inject.Inject
-class AddContentToFavouriteUseCase internal constructor(
+class AddContentToFavouriteUseCase @Inject internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {
diff --git a/core/src/main/java/org/fnives/test/showcase/core/content/ContentRepository.kt b/core/src/main/java/org/fnives/test/showcase/core/content/ContentRepository.kt
index b243db5..6b45ad3 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/content/ContentRepository.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/content/ContentRepository.kt
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
+import org.fnives.test.showcase.core.di.hilt.LoggedInModuleInject
import org.fnives.test.showcase.core.shared.Optional
import org.fnives.test.showcase.core.shared.mapIntoResource
import org.fnives.test.showcase.core.shared.wrapIntoAnswer
@@ -14,7 +15,9 @@ import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.content.ContentRemoteSource
-internal class ContentRepository(private val contentRemoteSource: ContentRemoteSource) {
+internal class ContentRepository @LoggedInModuleInject constructor(
+ private val contentRemoteSource: ContentRemoteSource
+) {
private val mutableContentFlow = MutableStateFlow(Optional>(null))
private val requestFlow: Flow>> = flow {
diff --git a/core/src/main/java/org/fnives/test/showcase/core/content/FetchContentUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/content/FetchContentUseCase.kt
index 90c7a5b..16944f5 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/content/FetchContentUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/content/FetchContentUseCase.kt
@@ -1,6 +1,8 @@
package org.fnives.test.showcase.core.content
-class FetchContentUseCase internal constructor(private val contentRepository: ContentRepository) {
+import javax.inject.Inject
+
+class FetchContentUseCase @Inject internal constructor(private val contentRepository: ContentRepository) {
fun invoke() = contentRepository.fetch()
}
diff --git a/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt
index 24a4b63..56b7c72 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/content/GetAllContentUseCase.kt
@@ -7,8 +7,9 @@ import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.shared.Resource
+import javax.inject.Inject
-class GetAllContentUseCase internal constructor(
+class GetAllContentUseCase @Inject internal constructor(
private val contentRepository: ContentRepository,
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {
diff --git a/core/src/main/java/org/fnives/test/showcase/core/content/RemoveContentFromFavouritesUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/content/RemoveContentFromFavouritesUseCase.kt
index 494af3e..4c03f90 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/content/RemoveContentFromFavouritesUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/content/RemoveContentFromFavouritesUseCase.kt
@@ -2,8 +2,9 @@ package org.fnives.test.showcase.core.content
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
+import javax.inject.Inject
-class RemoveContentFromFavouritesUseCase internal constructor(
+class RemoveContentFromFavouritesUseCase @Inject internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {
diff --git a/core/src/main/java/org/fnives/test/showcase/core/di/hilt/CoreModule.kt b/core/src/main/java/org/fnives/test/showcase/core/di/hilt/CoreModule.kt
new file mode 100644
index 0000000..67887f9
--- /dev/null
+++ b/core/src/main/java/org/fnives/test/showcase/core/di/hilt/CoreModule.kt
@@ -0,0 +1,33 @@
+package org.fnives.test.showcase.core.di.hilt
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.fnives.test.showcase.core.login.LogoutUseCase
+import org.fnives.test.showcase.core.session.SessionExpirationAdapter
+import org.fnives.test.showcase.core.storage.NetworkSessionLocalStorageAdapter
+import org.fnives.test.showcase.core.storage.UserDataLocalStorage
+import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
+import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+
+@InstallIn(SingletonComponent::class)
+@Module
+object CoreModule {
+
+ @Provides
+ internal fun bindNetworkSessionLocalStorageAdapter(
+ networkSessionLocalStorageAdapter: NetworkSessionLocalStorageAdapter
+ ): NetworkSessionLocalStorage = networkSessionLocalStorageAdapter
+
+ @Provides
+ internal fun bindNetworkSessionExpirationListener(
+ sessionExpirationAdapter: SessionExpirationAdapter
+ ): NetworkSessionExpirationListener = sessionExpirationAdapter
+
+ @Provides
+ fun provideLogoutUseCase(
+ storage: UserDataLocalStorage,
+ reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule
+ ): LogoutUseCase = LogoutUseCase(storage, reloadLoggedInModuleInjectModule)
+}
diff --git a/core/src/main/java/org/fnives/test/showcase/core/di/hilt/LoggedInModuleInject.kt b/core/src/main/java/org/fnives/test/showcase/core/di/hilt/LoggedInModuleInject.kt
new file mode 100644
index 0000000..5032c97
--- /dev/null
+++ b/core/src/main/java/org/fnives/test/showcase/core/di/hilt/LoggedInModuleInject.kt
@@ -0,0 +1,8 @@
+package org.fnives.test.showcase.core.di.hilt
+
+import org.fnives.library.reloadable.module.annotation.ReloadableModule
+
+@ReloadableModule
+@Target(AnnotationTarget.CONSTRUCTOR)
+@Retention(AnnotationRetention.SOURCE)
+annotation class LoggedInModuleInject
diff --git a/core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt b/core/src/main/java/org/fnives/test/showcase/core/di/koin/createCoreModule.kt
similarity index 94%
rename from core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt
rename to core/src/main/java/org/fnives/test/showcase/core/di/koin/createCoreModule.kt
index d3b6bc5..5f66810 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/di/createCoreModule.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/di/koin/createCoreModule.kt
@@ -1,4 +1,4 @@
-package org.fnives.test.showcase.core.di
+package org.fnives.test.showcase.core.di.koin
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.core.content.ContentRepository
@@ -14,7 +14,7 @@ import org.fnives.test.showcase.core.storage.NetworkSessionLocalStorageAdapter
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.network.BaseUrl
-import org.fnives.test.showcase.network.di.createNetworkModules
+import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.koin.core.module.Module
import org.koin.core.scope.Scope
import org.koin.dsl.module
@@ -42,7 +42,7 @@ fun repositoryModule() = module {
fun useCaseModule() = module {
factory { LoginUseCase(get(), get()) }
- factory { LogoutUseCase(get()) }
+ factory { LogoutUseCase(get(), null) }
factory { GetAllContentUseCase(get(), get()) }
factory { AddContentToFavouriteUseCase(get()) }
factory { RemoveContentFromFavouritesUseCase(get()) }
diff --git a/core/src/main/java/org/fnives/test/showcase/core/login/IsUserLoggedInUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/login/IsUserLoggedInUseCase.kt
index fd0539a..1e21dd1 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/login/IsUserLoggedInUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/login/IsUserLoggedInUseCase.kt
@@ -1,8 +1,11 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
+import javax.inject.Inject
-class IsUserLoggedInUseCase(private val userDataLocalStorage: UserDataLocalStorage) {
+class IsUserLoggedInUseCase @Inject constructor(
+ private val userDataLocalStorage: UserDataLocalStorage
+) {
fun invoke(): Boolean = userDataLocalStorage.session != null
}
diff --git a/core/src/main/java/org/fnives/test/showcase/core/login/LoginUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/login/LoginUseCase.kt
index 2a91780..cf03c79 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/login/LoginUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/login/LoginUseCase.kt
@@ -7,8 +7,9 @@ import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
+import javax.inject.Inject
-class LoginUseCase internal constructor(
+class LoginUseCase @Inject internal constructor(
private val loginRemoteSource: LoginRemoteSource,
private val userDataLocalStorage: UserDataLocalStorage
) {
diff --git a/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt b/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt
index 158e7e0..15d6033 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/login/LogoutUseCase.kt
@@ -1,13 +1,22 @@
package org.fnives.test.showcase.core.login
-import org.fnives.test.showcase.core.di.repositoryModule
+import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
+import org.fnives.test.showcase.core.di.koin.repositoryModule
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.koin.core.context.loadKoinModules
+import org.koin.mp.KoinPlatformTools
-class LogoutUseCase(private val storage: UserDataLocalStorage) {
+class LogoutUseCase(
+ private val storage: UserDataLocalStorage,
+ private val reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule?
+) {
suspend fun invoke() {
- loadKoinModules(repositoryModule())
+ if (KoinPlatformTools.defaultContext().getOrNull() == null) {
+ reloadLoggedInModuleInjectModule?.reload()
+ } else {
+ loadKoinModules(repositoryModule())
+ }
storage.session = null
}
}
diff --git a/core/src/main/java/org/fnives/test/showcase/core/session/SessionExpirationAdapter.kt b/core/src/main/java/org/fnives/test/showcase/core/session/SessionExpirationAdapter.kt
index 1947635..3571971 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/session/SessionExpirationAdapter.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/session/SessionExpirationAdapter.kt
@@ -1,11 +1,11 @@
package org.fnives.test.showcase.core.session
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
+import javax.inject.Inject
-internal class SessionExpirationAdapter(
+internal class SessionExpirationAdapter @Inject constructor(
private val sessionExpirationListener: SessionExpirationListener
-) :
- NetworkSessionExpirationListener {
+) : NetworkSessionExpirationListener {
override fun onSessionExpired() = sessionExpirationListener.onSessionExpired()
}
diff --git a/core/src/main/java/org/fnives/test/showcase/core/storage/NetworkSessionLocalStorageAdapter.kt b/core/src/main/java/org/fnives/test/showcase/core/storage/NetworkSessionLocalStorageAdapter.kt
index a9d5d77..0d157f6 100644
--- a/core/src/main/java/org/fnives/test/showcase/core/storage/NetworkSessionLocalStorageAdapter.kt
+++ b/core/src/main/java/org/fnives/test/showcase/core/storage/NetworkSessionLocalStorageAdapter.kt
@@ -2,8 +2,9 @@ package org.fnives.test.showcase.core.storage
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+import javax.inject.Inject
-internal class NetworkSessionLocalStorageAdapter(
+internal class NetworkSessionLocalStorageAdapter @Inject constructor(
private val userDataLocalStorage: UserDataLocalStorage
) : NetworkSessionLocalStorage {
diff --git a/core/src/test/java/org/fnives/test/showcase/core/login/hilt/LogoutUseCaseTest.kt b/core/src/test/java/org/fnives/test/showcase/core/login/hilt/LogoutUseCaseTest.kt
new file mode 100644
index 0000000..20ea451
--- /dev/null
+++ b/core/src/test/java/org/fnives/test/showcase/core/login/hilt/LogoutUseCaseTest.kt
@@ -0,0 +1,56 @@
+package org.fnives.test.showcase.core.login.hilt
+
+import kotlinx.coroutines.test.runBlockingTest
+import org.fnives.test.showcase.core.content.ContentRepository
+import org.fnives.test.showcase.core.login.LogoutUseCase
+import org.fnives.test.showcase.core.storage.UserDataLocalStorage
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.verifyZeroInteractions
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+internal class LogoutUseCaseTest {
+
+ @Inject
+ lateinit var sut: LogoutUseCase
+ private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
+ private lateinit var testCoreComponent: TestCoreComponent
+ @Inject
+ lateinit var contentRepository: ContentRepository
+
+ @BeforeEach
+ fun setUp() {
+ mockUserDataLocalStorage = mock()
+ testCoreComponent = DaggerTestCoreComponent.builder()
+ .setBaseUrl("https://a.b.com")
+ .setEnableLogging(true)
+ .setSessionExpirationListener(mock())
+ .setUserDataLocalStorage(mockUserDataLocalStorage)
+ .build()
+ testCoreComponent.inject(this)
+ }
+
+ @Test
+ fun WHEN_no_call_THEN_storage_is_not_interacted() {
+ verifyZeroInteractions(mockUserDataLocalStorage)
+ }
+
+ @Test
+ fun WHEN_logout_invoked_THEN_storage_is_cleared() = runBlockingTest {
+ val repositoryBefore = contentRepository
+
+ sut.invoke()
+
+ testCoreComponent.inject(this@LogoutUseCaseTest)
+ val repositoryAfter = contentRepository
+ verify(mockUserDataLocalStorage, times(1)).session = null
+ verifyNoMoreInteractions(mockUserDataLocalStorage)
+ Assertions.assertNotSame(repositoryBefore, repositoryAfter)
+ }
+}
diff --git a/core/src/test/java/org/fnives/test/showcase/core/login/hilt/TestCoreComponent.kt b/core/src/test/java/org/fnives/test/showcase/core/login/hilt/TestCoreComponent.kt
new file mode 100644
index 0000000..f5d2997
--- /dev/null
+++ b/core/src/test/java/org/fnives/test/showcase/core/login/hilt/TestCoreComponent.kt
@@ -0,0 +1,36 @@
+package org.fnives.test.showcase.core.login.hilt
+
+import dagger.BindsInstance
+import dagger.Component
+import org.fnives.test.showcase.core.di.hilt.CoreModule
+import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModuleImpl
+import org.fnives.test.showcase.core.session.SessionExpirationListener
+import org.fnives.test.showcase.core.storage.UserDataLocalStorage
+import org.fnives.test.showcase.network.di.hilt.BindsBaseOkHttpClient
+import org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [CoreModule::class, HiltNetworkModule::class, ReloadLoggedInModuleInjectModuleImpl::class, BindsBaseOkHttpClient::class])
+internal interface TestCoreComponent {
+
+ @Component.Builder
+ interface Builder {
+
+ @BindsInstance
+ fun setBaseUrl(baseUrl: String): Builder
+
+ @BindsInstance
+ fun setEnableLogging(enableLogging: Boolean): Builder
+
+ @BindsInstance
+ fun setSessionExpirationListener(listener: SessionExpirationListener): Builder
+
+ @BindsInstance
+ fun setUserDataLocalStorage(storage: UserDataLocalStorage): Builder
+
+ fun build(): TestCoreComponent
+ }
+
+ fun inject(logoutUseCaseTest: LogoutUseCaseTest)
+}
diff --git a/core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt b/core/src/test/java/org/fnives/test/showcase/core/login/koin/LogoutUseCaseTest.kt
similarity index 89%
rename from core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt
rename to core/src/test/java/org/fnives/test/showcase/core/login/koin/LogoutUseCaseTest.kt
index ffb1900..78574fe 100644
--- a/core/src/test/java/org/fnives/test/showcase/core/login/LogoutUseCaseTest.kt
+++ b/core/src/test/java/org/fnives/test/showcase/core/login/koin/LogoutUseCaseTest.kt
@@ -1,8 +1,9 @@
-package org.fnives.test.showcase.core.login
+package org.fnives.test.showcase.core.login.koin
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.content.ContentRepository
-import org.fnives.test.showcase.core.di.createCoreModule
+import org.fnives.test.showcase.core.di.koin.createCoreModule
+import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.network.BaseUrl
import org.junit.jupiter.api.AfterEach
@@ -27,7 +28,7 @@ internal class LogoutUseCaseTest : KoinTest {
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
- sut = LogoutUseCase(mockUserDataLocalStorage)
+ sut = LogoutUseCase(mockUserDataLocalStorage, null)
startKoin {
modules(
createCoreModule(
diff --git a/gradlescripts/versions.gradle b/gradlescripts/versions.gradle
index 4f8ed25..0d5a246 100644
--- a/gradlescripts/versions.gradle
+++ b/gradlescripts/versions.gradle
@@ -6,6 +6,7 @@ project.ext {
androidx_livedata_version = "2.3.1"
androidx_swiperefreshlayout_version = "1.1.0"
androidx_room_version = "2.3.0"
+ activity_ktx_version = "1.3.1"
coroutines_version = "1.4.3"
koin_version = "3.1.2"
@@ -13,6 +14,7 @@ project.ext {
retrofit_version = "2.9.0"
okhttp_version = "4.9.1"
moshi_version = "1.12.0"
+ reloadable_module_version = "0.1.0"
testing_androidx_code_version = "1.4.0"
testing_androidx_junit_version = "1.1.3"
diff --git a/model/build.gradle b/model/build.gradle
index a1a673c..a1e605a 100644
--- a/model/build.gradle
+++ b/model/build.gradle
@@ -6,4 +6,8 @@ plugins {
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+ implementation "javax.inject:javax.inject:1"
}
\ No newline at end of file
diff --git a/model/src/main/java/org/fnives/test/showcase/hilt/SessionLessQualifier.kt b/model/src/main/java/org/fnives/test/showcase/hilt/SessionLessQualifier.kt
new file mode 100644
index 0000000..7760b2f
--- /dev/null
+++ b/model/src/main/java/org/fnives/test/showcase/hilt/SessionLessQualifier.kt
@@ -0,0 +1,6 @@
+package org.fnives.test.showcase.hilt
+
+import javax.inject.Qualifier
+
+@Qualifier
+annotation class SessionLessQualifier
diff --git a/model/src/main/java/org/fnives/test/showcase/hilt/SessionQualifier.kt b/model/src/main/java/org/fnives/test/showcase/hilt/SessionQualifier.kt
new file mode 100644
index 0000000..bba6af8
--- /dev/null
+++ b/model/src/main/java/org/fnives/test/showcase/hilt/SessionQualifier.kt
@@ -0,0 +1,6 @@
+package org.fnives.test.showcase.hilt
+
+import javax.inject.Qualifier
+
+@Qualifier
+annotation class SessionQualifier
diff --git a/network/build.gradle b/network/build.gradle
index fc17ae7..0ac9855 100644
--- a/network/build.gradle
+++ b/network/build.gradle
@@ -17,8 +17,13 @@ dependencies {
implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
+
+ // koin
api "io.insert-koin:koin-core:$koin_version"
+ // hilt
+ implementation "com.google.dagger:hilt-core:$hilt_version"
+
api project(":model")
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
@@ -28,4 +33,6 @@ dependencies {
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
+ kapt "com.google.dagger:hilt-compiler:$hilt_version"
+ kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
}
\ No newline at end of file
diff --git a/network/src/main/java/org/fnives/test/showcase/network/auth/LoginErrorConverter.kt b/network/src/main/java/org/fnives/test/showcase/network/auth/LoginErrorConverter.kt
index 49a588b..057064e 100644
--- a/network/src/main/java/org/fnives/test/showcase/network/auth/LoginErrorConverter.kt
+++ b/network/src/main/java/org/fnives/test/showcase/network/auth/LoginErrorConverter.kt
@@ -7,8 +7,9 @@ import org.fnives.test.showcase.network.shared.ExceptionWrapper
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
import retrofit2.HttpException
import retrofit2.Response
+import javax.inject.Inject
-internal class LoginErrorConverter {
+internal class LoginErrorConverter @Inject constructor() {
@Throws(ParsingException::class)
suspend fun invoke(request: suspend () -> Response): LoginStatusResponses =
diff --git a/network/src/main/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceImpl.kt b/network/src/main/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceImpl.kt
index 248ffde..0a5bf85 100644
--- a/network/src/main/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceImpl.kt
+++ b/network/src/main/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceImpl.kt
@@ -7,8 +7,9 @@ import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.network.shared.ExceptionWrapper
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
+import javax.inject.Inject
-internal class LoginRemoteSourceImpl constructor(
+internal class LoginRemoteSourceImpl @Inject constructor(
private val loginService: LoginService,
private val loginErrorConverter: LoginErrorConverter
) : LoginRemoteSource {
diff --git a/network/src/main/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImpl.kt b/network/src/main/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImpl.kt
index d1daac8..25d830d 100644
--- a/network/src/main/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImpl.kt
+++ b/network/src/main/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImpl.kt
@@ -4,8 +4,11 @@ import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.network.shared.ExceptionWrapper
+import javax.inject.Inject
-internal class ContentRemoteSourceImpl(private val contentService: ContentService) : ContentRemoteSource {
+internal class ContentRemoteSourceImpl @Inject constructor(
+ private val contentService: ContentService
+) : ContentRemoteSource {
override suspend fun get(): List =
ExceptionWrapper.wrap {
diff --git a/network/src/main/java/org/fnives/test/showcase/network/di/hilt/BindsBaseOkHttpClient.kt b/network/src/main/java/org/fnives/test/showcase/network/di/hilt/BindsBaseOkHttpClient.kt
new file mode 100644
index 0000000..260f5ad
--- /dev/null
+++ b/network/src/main/java/org/fnives/test/showcase/network/di/hilt/BindsBaseOkHttpClient.kt
@@ -0,0 +1,17 @@
+package org.fnives.test.showcase.network.di.hilt
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import org.fnives.test.showcase.hilt.SessionLessQualifier
+
+@InstallIn(SingletonComponent::class)
+@Module
+abstract class BindsBaseOkHttpClient {
+
+ @Binds
+ @SessionLessQualifier
+ abstract fun bindsSessionLess(okHttpClient: OkHttpClient): OkHttpClient
+}
diff --git a/network/src/main/java/org/fnives/test/showcase/network/di/hilt/HiltNetworkModule.kt b/network/src/main/java/org/fnives/test/showcase/network/di/hilt/HiltNetworkModule.kt
new file mode 100644
index 0000000..fe8d982
--- /dev/null
+++ b/network/src/main/java/org/fnives/test/showcase/network/di/hilt/HiltNetworkModule.kt
@@ -0,0 +1,96 @@
+package org.fnives.test.showcase.network.di.hilt
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import org.fnives.test.showcase.hilt.SessionLessQualifier
+import org.fnives.test.showcase.hilt.SessionQualifier
+import org.fnives.test.showcase.network.auth.LoginRemoteSource
+import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
+import org.fnives.test.showcase.network.auth.LoginService
+import org.fnives.test.showcase.network.content.ContentRemoteSource
+import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
+import org.fnives.test.showcase.network.content.ContentService
+import org.fnives.test.showcase.network.di.setupLogging
+import org.fnives.test.showcase.network.session.AuthenticationHeaderInterceptor
+import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
+import org.fnives.test.showcase.network.session.SessionAuthenticator
+import org.fnives.test.showcase.network.shared.PlatformInterceptor
+import retrofit2.Converter
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+object HiltNetworkModule {
+
+ @Provides
+ @Singleton
+ fun provideConverterFactory(): Converter.Factory = MoshiConverterFactory.create()
+
+ @Provides
+ @Singleton
+ fun provideSessionLessOkHttpClient(enableLogging: Boolean) =
+ OkHttpClient.Builder()
+ .addInterceptor(PlatformInterceptor())
+ .setupLogging(enableLogging)
+ .build()
+
+ @Provides
+ @Singleton
+ @SessionLessQualifier
+ fun provideSessionLessRetrofit(
+ baseUrl: String,
+ converterFactory: Converter.Factory,
+ @SessionLessQualifier okHttpClient: OkHttpClient
+ ) = Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .addConverterFactory(converterFactory)
+ .client(okHttpClient)
+ .build()
+
+ @Provides
+ @Singleton
+ @SessionQualifier
+ internal fun provideSessionOkHttpClient(
+ @SessionLessQualifier okHttpClient: OkHttpClient,
+ sessionAuthenticator: SessionAuthenticator,
+ authenticationHeaderUtils: AuthenticationHeaderUtils
+ ) =
+ okHttpClient
+ .newBuilder()
+ .authenticator(sessionAuthenticator)
+ .addInterceptor(AuthenticationHeaderInterceptor(authenticationHeaderUtils))
+ .build()
+
+ @Provides
+ @Singleton
+ @SessionQualifier
+ fun provideSessionRetrofit(
+ @SessionLessQualifier retrofit: Retrofit,
+ @SessionQualifier okHttpClient: OkHttpClient
+ ) = retrofit.newBuilder()
+ .client(okHttpClient)
+ .build()
+
+ @Provides
+ internal fun bindContentRemoteSource(
+ contentRemoteSourceImpl: ContentRemoteSourceImpl
+ ): ContentRemoteSource = contentRemoteSourceImpl
+
+ @Provides
+ internal fun bindLoginRemoteSource(
+ loginRemoteSource: LoginRemoteSourceImpl
+ ): LoginRemoteSource = loginRemoteSource
+
+ @Provides
+ internal fun provideLoginService(@SessionLessQualifier retrofit: Retrofit): LoginService =
+ retrofit.create(LoginService::class.java)
+
+ @Provides
+ internal fun provideContentService(@SessionQualifier retrofit: Retrofit): ContentService =
+ retrofit.create(ContentService::class.java)
+}
diff --git a/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt b/network/src/main/java/org/fnives/test/showcase/network/di/koin/createNetworkmodules.kt
similarity index 97%
rename from network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt
rename to network/src/main/java/org/fnives/test/showcase/network/di/koin/createNetworkmodules.kt
index b4b0e8b..2c84e77 100644
--- a/network/src/main/java/org/fnives/test/showcase/network/di/createNetworkmodules.kt
+++ b/network/src/main/java/org/fnives/test/showcase/network/di/koin/createNetworkmodules.kt
@@ -1,4 +1,4 @@
-package org.fnives.test.showcase.network.di
+package org.fnives.test.showcase.network.di.koin
import okhttp3.OkHttpClient
import org.fnives.test.showcase.model.network.BaseUrl
@@ -9,6 +9,7 @@ import org.fnives.test.showcase.network.auth.LoginService
import org.fnives.test.showcase.network.content.ContentRemoteSource
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
import org.fnives.test.showcase.network.content.ContentService
+import org.fnives.test.showcase.network.di.setupLogging
import org.fnives.test.showcase.network.session.AuthenticationHeaderInterceptor
import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
diff --git a/network/src/main/java/org/fnives/test/showcase/network/session/AuthenticationHeaderUtils.kt b/network/src/main/java/org/fnives/test/showcase/network/session/AuthenticationHeaderUtils.kt
index cfb35c6..56601b1 100644
--- a/network/src/main/java/org/fnives/test/showcase/network/session/AuthenticationHeaderUtils.kt
+++ b/network/src/main/java/org/fnives/test/showcase/network/session/AuthenticationHeaderUtils.kt
@@ -1,14 +1,18 @@
package org.fnives.test.showcase.network.session
import okhttp3.Request
+import javax.inject.Inject
-internal class AuthenticationHeaderUtils(private val networkSessionLocalStorage: NetworkSessionLocalStorage) {
+internal class AuthenticationHeaderUtils @Inject constructor(
+ private val networkSessionLocalStorage: NetworkSessionLocalStorage
+) {
fun hasToken(okhttpRequest: Request): Boolean =
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken
fun attachToken(okhttpRequest: Request): Request =
- okhttpRequest.newBuilder().header(KEY, networkSessionLocalStorage.session?.accessToken.orEmpty()).build()
+ okhttpRequest.newBuilder()
+ .header(KEY, networkSessionLocalStorage.session?.accessToken.orEmpty()).build()
companion object {
private const val KEY = "Authorization"
diff --git a/network/src/main/java/org/fnives/test/showcase/network/session/SessionAuthenticator.kt b/network/src/main/java/org/fnives/test/showcase/network/session/SessionAuthenticator.kt
index df1c411..544574c 100644
--- a/network/src/main/java/org/fnives/test/showcase/network/session/SessionAuthenticator.kt
+++ b/network/src/main/java/org/fnives/test/showcase/network/session/SessionAuthenticator.kt
@@ -6,8 +6,9 @@ import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
+import javax.inject.Inject
-internal class SessionAuthenticator(
+internal class SessionAuthenticator @Inject constructor(
private val networkSessionLocalStorage: NetworkSessionLocalStorage,
private val loginRemoteSource: LoginRemoteSourceImpl,
private val authenticationHeaderUtils: AuthenticationHeaderUtils,
diff --git a/network/src/test/java/org/fnives/test/showcase/network/TestNetworkComponent.kt b/network/src/test/java/org/fnives/test/showcase/network/TestNetworkComponent.kt
new file mode 100644
index 0000000..9300e01
--- /dev/null
+++ b/network/src/test/java/org/fnives/test/showcase/network/TestNetworkComponent.kt
@@ -0,0 +1,44 @@
+package org.fnives.test.showcase.network
+
+import dagger.BindsInstance
+import dagger.Component
+import org.fnives.test.showcase.network.auth.hilt.LoginRemoteSourceRefreshActionImplTest
+import org.fnives.test.showcase.network.auth.hilt.LoginRemoteSourceTest
+import org.fnives.test.showcase.network.content.hilt.ContentRemoteSourceImplTest
+import org.fnives.test.showcase.network.content.hilt.SessionExpirationTest
+import org.fnives.test.showcase.network.di.hilt.BindsBaseOkHttpClient
+import org.fnives.test.showcase.network.di.hilt.HiltNetworkModule
+import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
+import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [HiltNetworkModule::class, BindsBaseOkHttpClient::class])
+interface TestNetworkComponent {
+
+ @Component.Builder
+ interface Builder {
+
+ @BindsInstance
+ fun setBaseUrl(baseUrl: String): Builder
+
+ @BindsInstance
+ fun setEnableLogging(enableLogging: Boolean): Builder
+
+ @BindsInstance
+ fun setNetworkSessionLocalStorage(storage: NetworkSessionLocalStorage): Builder
+
+ @BindsInstance
+ fun setNetworkSessionExpirationListener(listener: NetworkSessionExpirationListener): Builder
+
+ fun build(): TestNetworkComponent
+ }
+
+ fun inject(contentRemoteSourceImplTest: ContentRemoteSourceImplTest)
+
+ fun inject(sessionExpirationTest: SessionExpirationTest)
+
+ fun inject(loginRemoteSourceRefreshActionImplTest: LoginRemoteSourceRefreshActionImplTest)
+
+ fun inject(loginRemoteSourceTest: LoginRemoteSourceTest)
+}
diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/hilt/LoginRemoteSourceRefreshActionImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/hilt/LoginRemoteSourceRefreshActionImplTest.kt
new file mode 100644
index 0000000..1b9e1f1
--- /dev/null
+++ b/network/src/test/java/org/fnives/test/showcase/network/auth/hilt/LoginRemoteSourceRefreshActionImplTest.kt
@@ -0,0 +1,98 @@
+package org.fnives.test.showcase.network.auth.hilt
+
+import kotlinx.coroutines.runBlocking
+import org.fnives.test.showcase.network.DaggerTestNetworkComponent
+import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
+import org.fnives.test.showcase.network.mockserver.ContentData
+import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
+import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
+import org.fnives.test.showcase.network.shared.exceptions.NetworkException
+import org.fnives.test.showcase.network.shared.exceptions.ParsingException
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.mockito.kotlin.mock
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+class LoginRemoteSourceRefreshActionImplTest {
+
+ @Inject
+ internal lateinit var sut: LoginRemoteSourceImpl
+ private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
+
+ @RegisterExtension
+ @JvmField
+ val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
+ private val mockServerScenarioSetup
+ get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
+
+ @BeforeEach
+ fun setUp() {
+ mockNetworkSessionLocalStorage = mock()
+ DaggerTestNetworkComponent.builder()
+ .setBaseUrl(mockServerScenarioSetupExtensions.url)
+ .setEnableLogging(true)
+ .setNetworkSessionLocalStorage(mockNetworkSessionLocalStorage)
+ .setNetworkSessionExpirationListener(mock())
+ .build()
+ .inject(this)
+ }
+
+ @Test
+ fun GIVEN_successful_response_WHEN_refresh_request_is_fired_THEN_session() = runBlocking {
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success)
+ val expected = ContentData.refreshSuccessResponse
+
+ val actual = sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
+
+ Assertions.assertEquals(expected, actual)
+ }
+
+ @Test
+ fun GIVEN_successful_response_WHEN_refresh_request_is_fired_THEN_the_request_is_setup_properly() =
+ runBlocking {
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
+
+ sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
+ val request = mockServerScenarioSetup.takeRequest()
+
+ Assertions.assertEquals("PUT", request.method)
+ Assertions.assertEquals("Android", request.getHeader("Platform"))
+ Assertions.assertEquals(null, request.getHeader("Authorization"))
+ Assertions.assertEquals(
+ "/login/${ContentData.refreshSuccessResponse.refreshToken}",
+ request.path
+ )
+ Assertions.assertEquals("", request.body.readUtf8())
+ }
+
+ @Test
+ fun GIVEN_internal_error_response_WHEN_refresh_request_is_fired_THEN_network_exception_is_thrown() {
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
+
+ Assertions.assertThrows(NetworkException::class.java) {
+ runBlocking { sut.refresh(ContentData.refreshSuccessResponse.refreshToken) }
+ }
+ }
+
+ @Test
+ fun GIVEN_invalid_json_response_WHEN_refresh_request_is_fired_THEN_network_exception_is_thrown() {
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.UnexpectedJsonAsSuccessResponse)
+
+ Assertions.assertThrows(ParsingException::class.java) {
+ runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
+ }
+ }
+
+ @Test
+ fun GIVEN_malformed_json_response_WHEN_refresh_request_is_fired_THEN_network_exception_is_thrown() {
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.MalformedJson)
+
+ Assertions.assertThrows(ParsingException::class.java) {
+ runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
+ }
+ }
+}
diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/hilt/LoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/hilt/LoginRemoteSourceTest.kt
new file mode 100644
index 0000000..94b848e
--- /dev/null
+++ b/network/src/test/java/org/fnives/test/showcase/network/auth/hilt/LoginRemoteSourceTest.kt
@@ -0,0 +1,120 @@
+package org.fnives.test.showcase.network.auth.hilt
+
+import kotlinx.coroutines.runBlocking
+import org.fnives.test.showcase.model.auth.LoginCredentials
+import org.fnives.test.showcase.network.DaggerTestNetworkComponent
+import org.fnives.test.showcase.network.auth.LoginRemoteSource
+import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
+import org.fnives.test.showcase.network.mockserver.ContentData
+import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson
+import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
+import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
+import org.fnives.test.showcase.network.shared.exceptions.NetworkException
+import org.fnives.test.showcase.network.shared.exceptions.ParsingException
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.mockito.kotlin.mock
+import org.skyscreamer.jsonassert.JSONAssert
+import org.skyscreamer.jsonassert.JSONCompareMode
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+class LoginRemoteSourceTest {
+
+ @Inject
+ internal lateinit var sut: LoginRemoteSource
+
+ @RegisterExtension
+ @JvmField
+ val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
+ private val mockServerScenarioSetup
+ get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
+
+ @BeforeEach
+ fun setUp() {
+ val mockNetworkSessionLocalStorage = mock()
+ DaggerTestNetworkComponent.builder()
+ .setBaseUrl(mockServerScenarioSetupExtensions.url)
+ .setEnableLogging(true)
+ .setNetworkSessionLocalStorage(mockNetworkSessionLocalStorage)
+ .setNetworkSessionExpirationListener(mock())
+ .build()
+ .inject(this)
+ }
+
+ @DisplayName("GIVEN successful response WHEN request is fired THEN login status success is returned")
+ @Test
+ fun successResponseIsParsedProperly() = runBlocking {
+ mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
+ val expected = LoginStatusResponses.Success(ContentData.loginSuccessResponse)
+
+ val actual = sut.login(LoginCredentials("a", "b"))
+
+ Assertions.assertEquals(expected, actual)
+ }
+
+ @DisplayName("GIVEN successful response WHEN request is fired THEN the request is setup properly")
+ @Test
+ fun requestProperlySetup() = runBlocking {
+ mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"), false)
+
+ sut.login(LoginCredentials("a", "b"))
+ val request = mockServerScenarioSetup.takeRequest()
+
+ Assertions.assertEquals("POST", request.method)
+ Assertions.assertEquals("Android", request.getHeader("Platform"))
+ Assertions.assertEquals(null, request.getHeader("Authorization"))
+ Assertions.assertEquals("/login", request.path)
+ val loginRequest = createExpectedLoginRequestJson("a", "b")
+ JSONAssert.assertEquals(
+ loginRequest,
+ request.body.readUtf8(),
+ JSONCompareMode.NON_EXTENSIBLE
+ )
+ }
+
+ @DisplayName("GIVEN bad request response WHEN request is fired THEN login status invalid credentials is returned")
+ @Test
+ fun badRequestMeansInvalidCredentials() = runBlocking {
+ mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials("a", "b"))
+ val expected = LoginStatusResponses.InvalidCredentials
+
+ val actual = sut.login(LoginCredentials("a", "b"))
+
+ Assertions.assertEquals(expected, actual)
+ }
+
+ @DisplayName("GIVEN_internal_error_response_WHEN_request_is_fired_THEN_network_exception_is_thrown")
+ @Test
+ fun genericErrorMeansNetworkError() {
+ mockServerScenarioSetup.setScenario(AuthScenario.GenericError("a", "b"))
+
+ Assertions.assertThrows(NetworkException::class.java) {
+ runBlocking { sut.login(LoginCredentials("a", "b")) }
+ }
+ }
+
+ @DisplayName("GIVEN invalid json response WHEN request is fired THEN network exception is thrown")
+ @Test
+ fun invalidJsonMeansParsingException() {
+ mockServerScenarioSetup.setScenario(AuthScenario.UnexpectedJsonAsSuccessResponse("a", "b"))
+
+ Assertions.assertThrows(ParsingException::class.java) {
+ runBlocking { sut.login(LoginCredentials("a", "b")) }
+ }
+ }
+
+ @DisplayName("GIVEN malformed json response WHEN request is fired THEN network exception is thrown")
+ @Test
+ fun malformedJsonMeansParsingException() {
+ mockServerScenarioSetup.setScenario(AuthScenario.MalformedJsonAsSuccessResponse("a", "b"))
+
+ Assertions.assertThrows(ParsingException::class.java) {
+ runBlocking { sut.login(LoginCredentials("a", "b")) }
+ }
+ }
+}
diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/koin/LoginRemoteSourceRefreshActionImplTest.kt
similarity index 95%
rename from network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt
rename to network/src/test/java/org/fnives/test/showcase/network/auth/koin/LoginRemoteSourceRefreshActionImplTest.kt
index fc71d92..cbf9ae7 100644
--- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt
+++ b/network/src/test/java/org/fnives/test/showcase/network/auth/koin/LoginRemoteSourceRefreshActionImplTest.kt
@@ -1,8 +1,9 @@
-package org.fnives.test.showcase.network.auth
+package org.fnives.test.showcase.network.auth.koin
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.network.BaseUrl
-import org.fnives.test.showcase.network.di.createNetworkModules
+import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
+import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/koin/LoginRemoteSourceTest.kt
similarity index 96%
rename from network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt
rename to network/src/test/java/org/fnives/test/showcase/network/auth/koin/LoginRemoteSourceTest.kt
index cd586bd..cc1b582 100644
--- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt
+++ b/network/src/test/java/org/fnives/test/showcase/network/auth/koin/LoginRemoteSourceTest.kt
@@ -1,10 +1,11 @@
-package org.fnives.test.showcase.network.auth
+package org.fnives.test.showcase.network.auth.koin
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.network.BaseUrl
+import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
-import org.fnives.test.showcase.network.di.createNetworkModules
+import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/CodeKataSessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/CodeKataSessionExpirationTest.kt
index eb2f310..625b995 100644
--- a/network/src/test/java/org/fnives/test/showcase/network/content/CodeKataSessionExpirationTest.kt
+++ b/network/src/test/java/org/fnives/test/showcase/network/content/CodeKataSessionExpirationTest.kt
@@ -3,7 +3,7 @@ package org.fnives.test.showcase.network.content
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockWebServer
import org.fnives.test.showcase.model.network.BaseUrl
-import org.fnives.test.showcase.network.di.createNetworkModules
+import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
import org.junit.jupiter.api.AfterEach
diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/hilt/ContentRemoteSourceImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/hilt/ContentRemoteSourceImplTest.kt
new file mode 100644
index 0000000..d1b669d
--- /dev/null
+++ b/network/src/test/java/org/fnives/test/showcase/network/content/hilt/ContentRemoteSourceImplTest.kt
@@ -0,0 +1,114 @@
+package org.fnives.test.showcase.network.content.hilt
+
+import kotlinx.coroutines.runBlocking
+import org.fnives.test.showcase.network.DaggerTestNetworkComponent
+import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
+import org.fnives.test.showcase.network.mockserver.ContentData
+import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
+import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
+import org.fnives.test.showcase.network.shared.exceptions.NetworkException
+import org.fnives.test.showcase.network.shared.exceptions.ParsingException
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.koin.test.inject
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+class ContentRemoteSourceImplTest {
+
+ @Inject
+ internal lateinit var sut: ContentRemoteSourceImpl
+
+ @RegisterExtension
+ @JvmField
+ val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
+ private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
+ private val mockServerScenarioSetup
+ get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
+
+ @BeforeEach
+ fun setUp() {
+ mockNetworkSessionLocalStorage = mock()
+ DaggerTestNetworkComponent.builder()
+ .setBaseUrl(mockServerScenarioSetupExtensions.url)
+ .setEnableLogging(true)
+ .setNetworkSessionLocalStorage(mockNetworkSessionLocalStorage)
+ .setNetworkSessionExpirationListener(mock())
+ .build()
+ .inject(this)
+ }
+
+ @Test
+ fun GIVEN_successful_response_WHEN_getting_content_THEN_its_parsed_and_returned_correctly() = runBlocking {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.Success(false))
+ val expected = ContentData.contentSuccess
+
+ val actual = sut.get()
+
+ Assertions.assertEquals(expected, actual)
+ }
+
+ @Test
+ fun GIVEN_successful_response_WHEN_getting_content_THEN_the_request_is_setup_properly() = runBlocking {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.Success(false), false)
+
+ sut.get()
+ val request = mockServerScenarioSetup.takeRequest()
+
+ Assertions.assertEquals("GET", request.method)
+ Assertions.assertEquals("Android", request.getHeader("Platform"))
+ Assertions.assertEquals(ContentData.loginSuccessResponse.accessToken, request.getHeader("Authorization"))
+ Assertions.assertEquals("/content", request.path)
+ Assertions.assertEquals("", request.body.readUtf8())
+ }
+
+ @Test
+ fun GIVEN_response_with_missing_Field_WHEN_getting_content_THEN_invalid_is_ignored_others_are_returned() = runBlocking {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.SuccessWithMissingFields(false))
+
+ val expected = ContentData.contentSuccessWithMissingFields
+
+ val actual = sut.get()
+
+ Assertions.assertEquals(expected, actual)
+ }
+
+ @Test
+ fun GIVEN_error_response_WHEN_getting_content_THEN_network_request_is_thrown() {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.Error(false))
+
+ Assertions.assertThrows(NetworkException::class.java) {
+ runBlocking { sut.get() }
+ }
+ }
+
+ @Test
+ fun GIVEN_unexpected_json_response_WHEN_getting_content_THEN_parsing_request_is_thrown() {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.UnexpectedJsonAsSuccessResponse(false))
+
+ Assertions.assertThrows(ParsingException::class.java) {
+ runBlocking { sut.get() }
+ }
+ }
+
+ @Test
+ fun GIVEN_malformed_json_response_WHEN_getting_content_THEN_parsing_request_is_thrown() {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.MalformedJsonAsSuccessResponse(false))
+
+ Assertions.assertThrows(ParsingException::class.java) {
+ runBlocking { sut.get() }
+ }
+ }
+}
diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/hilt/SessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/hilt/SessionExpirationTest.kt
new file mode 100644
index 0000000..9476715
--- /dev/null
+++ b/network/src/test/java/org/fnives/test/showcase/network/content/hilt/SessionExpirationTest.kt
@@ -0,0 +1,110 @@
+package org.fnives.test.showcase.network.content.hilt
+
+import kotlinx.coroutines.runBlocking
+import org.fnives.test.showcase.model.session.Session
+import org.fnives.test.showcase.network.DaggerTestNetworkComponent
+import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
+import org.fnives.test.showcase.network.mockserver.ContentData
+import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
+import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
+import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
+import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
+import org.fnives.test.showcase.network.shared.MockServerScenarioSetupExtensions
+import org.fnives.test.showcase.network.shared.exceptions.NetworkException
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.koin.test.inject
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.whenever
+import javax.inject.Inject
+
+@Suppress("TestFunctionName")
+class SessionExpirationTest {
+
+ @Inject
+ internal lateinit var sut: ContentRemoteSourceImpl
+
+ @RegisterExtension
+ @JvmField
+ val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
+ private val mockServerScenarioSetup
+ get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
+ private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
+ private lateinit var mockNetworkSessionExpirationListener: NetworkSessionExpirationListener
+
+ @BeforeEach
+ fun setUp() {
+ mockNetworkSessionLocalStorage = mock()
+ mockNetworkSessionExpirationListener = mock()
+ DaggerTestNetworkComponent.builder()
+ .setBaseUrl(mockServerScenarioSetupExtensions.url)
+ .setEnableLogging(true)
+ .setNetworkSessionLocalStorage(mockNetworkSessionLocalStorage)
+ .setNetworkSessionExpirationListener(mockNetworkSessionExpirationListener)
+ .build()
+ .inject(this)
+ }
+
+ @DisplayName("GIVEN_401_THEN_refresh_token_ok_response_WHEN_content_requested_THE_tokens_are_refreshed_and_request_retried_with_new_tokens")
+ @Test
+ fun successRefreshResultsInRequestRetry() = runBlocking {
+ var sessionToReturnByMock: Session? = ContentData.loginSuccessResponse
+ mockServerScenarioSetup.setScenario(
+ ContentScenario.Unauthorized(false)
+ .then(ContentScenario.Success(true)),
+ false
+ )
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
+ whenever(mockNetworkSessionLocalStorage.session).doAnswer { sessionToReturnByMock }
+ doAnswer { sessionToReturnByMock = it.arguments[0] as Session? }
+ .whenever(mockNetworkSessionLocalStorage).session = anyOrNull()
+
+ sut.get()
+
+ mockServerScenarioSetup.takeRequest()
+ val refreshRequest = mockServerScenarioSetup.takeRequest()
+ val retryAfterTokenRefreshRequest = mockServerScenarioSetup.takeRequest()
+
+ Assertions.assertEquals("PUT", refreshRequest.method)
+ Assertions.assertEquals(
+ "/login/${ContentData.loginSuccessResponse.refreshToken}",
+ refreshRequest.path
+ )
+ Assertions.assertEquals(null, refreshRequest.getHeader("Authorization"))
+ Assertions.assertEquals("Android", refreshRequest.getHeader("Platform"))
+ Assertions.assertEquals("", refreshRequest.body.readUtf8())
+ Assertions.assertEquals(
+ ContentData.refreshSuccessResponse.accessToken,
+ retryAfterTokenRefreshRequest.getHeader("Authorization")
+ )
+ verify(mockNetworkSessionLocalStorage, times(1)).session =
+ ContentData.refreshSuccessResponse
+ verifyZeroInteractions(mockNetworkSessionExpirationListener)
+ }
+
+ @DisplayName("GIVEN 401 THEN failing refresh WHEN content requested THE error is returned and callback is Called")
+ @Test
+ fun failingRefreshResultsInSessionExpiration() = runBlocking {
+ whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
+ mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(false))
+ mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
+
+ Assertions.assertThrows(NetworkException::class.java) {
+ runBlocking { sut.get() }
+ }
+ verify(mockNetworkSessionLocalStorage, times(3)).session
+ verify(mockNetworkSessionLocalStorage, times(1)).session = null
+ verifyNoMoreInteractions(mockNetworkSessionLocalStorage)
+ verify(mockNetworkSessionExpirationListener, times(1)).onSessionExpired()
+ }
+}
diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/koin/ContentRemoteSourceImplTest.kt
similarity index 96%
rename from network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt
rename to network/src/test/java/org/fnives/test/showcase/network/content/koin/ContentRemoteSourceImplTest.kt
index f1996d2..6edc095 100644
--- a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt
+++ b/network/src/test/java/org/fnives/test/showcase/network/content/koin/ContentRemoteSourceImplTest.kt
@@ -1,8 +1,9 @@
-package org.fnives.test.showcase.network.content
+package org.fnives.test.showcase.network.content.koin
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.network.BaseUrl
-import org.fnives.test.showcase.network.di.createNetworkModules
+import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
+import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/koin/SessionExpirationTest.kt
similarity index 96%
rename from network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt
rename to network/src/test/java/org/fnives/test/showcase/network/content/koin/SessionExpirationTest.kt
index 0f80d3e..5902153 100644
--- a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt
+++ b/network/src/test/java/org/fnives/test/showcase/network/content/koin/SessionExpirationTest.kt
@@ -1,9 +1,10 @@
-package org.fnives.test.showcase.network.content
+package org.fnives.test.showcase.network.content.koin
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.session.Session
-import org.fnives.test.showcase.network.di.createNetworkModules
+import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
+import org.fnives.test.showcase.network.di.koin.createNetworkModules
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario