commit
961a04af57
90 changed files with 1996 additions and 131 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
22
app/src/hilt/AndroidManifest.xml
Normal file
22
app/src/hilt/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.fnives.test.showcase">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ui.splash.HiltSplashActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.home.HiltMainActivity" />
|
||||
<activity android:name=".ui.auth.HiltAuthActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.fnives.test.showcase
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class TestShowcaseApplication : Application()
|
||||
52
app/src/hilt/java/org/fnives/test/showcase/di/AppModule.kt
Normal file
52
app/src/hilt/java/org/fnives/test/showcase/di/AppModule.kt
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 <reified T : ViewModel> ViewModelStoreOwner.viewModels(): Lazy<T> =
|
||||
when (this) {
|
||||
is ComponentActivity -> androidxViewModel()
|
||||
else -> throw IllegalStateException("Only supports activity viewModel for now")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.fnives.test.showcase.ui.splash
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HiltSplashActivity : SplashActivity()
|
||||
21
app/src/koin/AndroidManifest.xml
Normal file
21
app/src/koin/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.fnives.test.showcase">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ui.splash.SplashActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.home.MainActivity" />
|
||||
<activity android:name=".ui.auth.AuthActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 <reified T : ViewModel> ViewModelStoreOwner.viewModels(): Lazy<T> =
|
||||
viewModel()
|
||||
|
|
@ -6,24 +6,13 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".TestShowcaseApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:name=".TestShowcaseApplication"
|
||||
android:theme="@style/Theme.TestShowCase"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity android:name=".ui.splash.SplashActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ui.home.MainActivity"/>
|
||||
<activity android:name=".ui.auth.AuthActivity"/>
|
||||
</application>
|
||||
tools:ignore="AllowBackup"/>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<SharedPreferencesManagerImpl, Session?> {
|
||||
private class SessionDelegate(private val key: String) :
|
||||
ReadWriteProperty<SharedPreferencesManagerImpl, Session?> {
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<List<ContentId>> =
|
||||
favouriteDao.get().map { it.map(FavouriteEntity::contentId).map(::ContentId) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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<AuthViewModel>()
|
||||
private val viewModel by viewModels<AuthViewModel>()
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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<String>()
|
||||
val username: LiveData<String> = _username
|
||||
|
|
|
|||
|
|
@ -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<MainViewModel>()
|
||||
private val viewModel by viewModels<MainViewModel>()
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<SplashViewModel>()
|
||||
private val viewModel by viewModels<SplashViewModel>()
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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<Event<NavigateTo>>()
|
||||
val navigateTo: LiveData<Event<NavigateTo>> = _navigateTo
|
||||
|
|
|
|||
|
|
@ -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<ContentId>()
|
||||
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<Unit> {
|
||||
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<Unit> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
sdk=28
|
||||
shadows=org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
|
||||
instrumentedPackages=androidx.loader.content
|
||||
application=dagger.hilt.android.testing.HiltTestApplication
|
||||
|
|
@ -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<LogoutUseCase>()
|
||||
private val loginUseCase get() = get<LoginUseCase>()
|
||||
private val isUserLoggedInUseCase get() = get<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() }
|
||||
}
|
||||
}
|
||||
// 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<LogoutUseCase>()
|
||||
// private val loginUseCase get() = get<LoginUseCase>()
|
||||
// private val isUserLoggedInUseCase get() = get<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() }
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<HiltMainActivity>
|
||||
|
||||
@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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HiltAuthActivity>
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HiltSplashActivity>? = 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@ class ReloadKoinModulesIfNecessaryTestRule : TestRule {
|
|||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
if (GlobalContext.getOrNull() == null) {
|
||||
val application = ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
|
||||
val application =
|
||||
ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
|
||||
startKoin {
|
||||
androidContext(application)
|
||||
modules(createAppModules(BaseUrlProvider.get()))
|
||||
|
|
@ -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<LogoutUseCase>()
|
||||
private val loginUseCase get() = get<LoginUseCase>()
|
||||
private val isUserLoggedInUseCase get() = get<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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
15
build.gradle
15
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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<Content>>(null))
|
||||
private val requestFlow: Flow<Resource<List<Content>>> = flow {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()) }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -7,3 +7,7 @@ java {
|
|||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "javax.inject:javax.inject:1"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.fnives.test.showcase.hilt
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class SessionLessQualifier
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.fnives.test.showcase.hilt
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class SessionQualifier
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<LoginResponse>): LoginStatusResponses =
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<Content> =
|
||||
ExceptionWrapper.wrap {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NetworkSessionLocalStorage>()
|
||||
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")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue