commit
961a04af57
90 changed files with 1996 additions and 131 deletions
|
|
@ -2,6 +2,8 @@ plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
|
// hilt specific
|
||||||
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -25,6 +27,18 @@ android {
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
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 {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
|
@ -34,10 +48,26 @@ android {
|
||||||
androidTest {
|
androidTest {
|
||||||
java.srcDirs += "src/sharedTest/java"
|
java.srcDirs += "src/sharedTest/java"
|
||||||
}
|
}
|
||||||
|
androidTestHilt {
|
||||||
|
java.srcDirs += "src/sharedTestHilt/java"
|
||||||
|
}
|
||||||
|
androidTestKoin {
|
||||||
|
java.srcDirs += "src/sharedTestKoin/java"
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
java.srcDirs += "src/sharedTest/java"
|
java.srcDirs += "src/sharedTest/java"
|
||||||
java.srcDirs += "src/robolectricTest/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
|
// needed for androidTest
|
||||||
|
|
@ -49,10 +79,17 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hilt {
|
||||||
|
enableAggregatingTask = true
|
||||||
|
enableExperimentalClasspathAggregation = true
|
||||||
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
// making sure the :mockserver is assembled after :clean when running tests
|
// making sure the :mockserver is assembled after :clean when running tests
|
||||||
testDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
|
testKoinDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
|
||||||
testReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
|
testKoinReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
|
||||||
|
testHiltDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
|
||||||
|
testHiltReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -66,7 +103,12 @@ dependencies {
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
|
||||||
|
|
||||||
// Koin
|
// 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"
|
implementation "androidx.room:room-runtime:$androidx_room_version"
|
||||||
kapt "androidx.room:room-compiler:$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 "com.jakewharton.espresso:okhttp3-idling-resource:$testing_okhttp3_idling_resource_version"
|
||||||
testImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
|
testImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
|
||||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_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 "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
||||||
androidTestImplementation "io.insert-koin:koin-test-junit4:$koin_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 "com.jakewharton.espresso:okhttp3-idling-resource:$testing_okhttp3_idling_resource_version"
|
||||||
androidTestImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
|
androidTestImplementation "androidx.arch.core:core-testing:$testing_androidx_arch_core_version"
|
||||||
androidTestRuntimeOnly "org.junit.vintage:junit-vintage-engine:$testing_junit5_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
|
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.model.network.BaseUrl
|
||||||
import org.fnives.test.showcase.session.SessionExpirationListenerImpl
|
import org.fnives.test.showcase.session.SessionExpirationListenerImpl
|
||||||
import org.fnives.test.showcase.storage.LocalDatabase
|
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" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".TestShowcaseApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:name=".TestShowcaseApplication"
|
|
||||||
android:theme="@style/Theme.TestShowCase"
|
android:theme="@style/Theme.TestShowCase"
|
||||||
tools:ignore="AllowBackup">
|
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>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -4,15 +4,20 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import org.fnives.test.showcase.core.session.SessionExpirationListener
|
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() {
|
override fun onSessionExpired() {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
AuthActivity.getStartIntent(context)
|
IntentCoordinator.authActivitygetStartIntent(context)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_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.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
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)
|
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) {
|
if (value == null) {
|
||||||
thisRef.sharedPreferences.edit().remove(key).apply()
|
thisRef.sharedPreferences.edit().remove(key).apply()
|
||||||
} else {
|
} 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 values = thisRef.sharedPreferences.getStringSet(key, null)?.toList()
|
||||||
val accessToken = values?.firstOrNull { it.startsWith(ACCESS_TOKEN_KEY) }
|
val accessToken = values?.firstOrNull { it.startsWith(ACCESS_TOKEN_KEY) }
|
||||||
?.drop(ACCESS_TOKEN_KEY.length) ?: return null
|
?.drop(ACCESS_TOKEN_KEY.length) ?: return null
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
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>> =
|
override fun observeFavourites(): Flow<List<ContentId>> =
|
||||||
favouriteDao.get().map { it.map(FavouriteEntity::contentId).map(::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 com.google.android.material.snackbar.Snackbar
|
||||||
import org.fnives.test.showcase.R
|
import org.fnives.test.showcase.R
|
||||||
import org.fnives.test.showcase.databinding.ActivityAuthenticationBinding
|
import org.fnives.test.showcase.databinding.ActivityAuthenticationBinding
|
||||||
import org.fnives.test.showcase.ui.home.MainActivity
|
import org.fnives.test.showcase.ui.IntentCoordinator
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -35,7 +35,7 @@ class AuthActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
viewModel.navigateToHome.observe(this) {
|
viewModel.navigateToHome.observe(this) {
|
||||||
it.consume() ?: return@observe
|
it.consume() ?: return@observe
|
||||||
startActivity(MainActivity.getStartIntent(this))
|
startActivity(IntentCoordinator.mainActivitygetStartIntent(this))
|
||||||
finishAffinity()
|
finishAffinity()
|
||||||
}
|
}
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,17 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.fnives.test.showcase.core.login.LoginUseCase
|
import org.fnives.test.showcase.core.login.LoginUseCase
|
||||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||||
import org.fnives.test.showcase.model.auth.LoginStatus
|
import org.fnives.test.showcase.model.auth.LoginStatus
|
||||||
import org.fnives.test.showcase.model.shared.Answer
|
import org.fnives.test.showcase.model.shared.Answer
|
||||||
import org.fnives.test.showcase.ui.shared.Event
|
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>()
|
private val _username = MutableLiveData<String>()
|
||||||
val username: LiveData<String> = _username
|
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.R
|
||||||
import org.fnives.test.showcase.databinding.ActivityMainBinding
|
import org.fnives.test.showcase.databinding.ActivityMainBinding
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
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.VerticalSpaceItemDecoration
|
||||||
import org.fnives.test.showcase.ui.shared.getThemePrimaryColor
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -45,7 +45,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
viewModel.navigateToAuth.observe(this) {
|
viewModel.navigateToAuth.observe(this) {
|
||||||
it.consume() ?: return@observe
|
it.consume() ?: return@observe
|
||||||
startActivity(AuthActivity.getStartIntent(this))
|
startActivity(IntentCoordinator.authActivitygetStartIntent(this))
|
||||||
finishAffinity()
|
finishAffinity()
|
||||||
}
|
}
|
||||||
viewModel.loading.observe(this) {
|
viewModel.loading.observe(this) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
|
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.content.FavouriteContent
|
||||||
import org.fnives.test.showcase.model.shared.Resource
|
import org.fnives.test.showcase.model.shared.Resource
|
||||||
import org.fnives.test.showcase.ui.shared.Event
|
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 getAllContentUseCase: GetAllContentUseCase,
|
||||||
private val logoutUseCase: LogoutUseCase,
|
private val logoutUseCase: LogoutUseCase,
|
||||||
private val fetchContentUseCase: FetchContentUseCase,
|
private val fetchContentUseCase: FetchContentUseCase,
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,20 @@ package org.fnives.test.showcase.ui.splash
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import org.fnives.test.showcase.R
|
import org.fnives.test.showcase.R
|
||||||
import org.fnives.test.showcase.ui.auth.AuthActivity
|
import org.fnives.test.showcase.ui.IntentCoordinator
|
||||||
import org.fnives.test.showcase.ui.home.MainActivity
|
import org.fnives.test.showcase.ui.viewModels
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity() {
|
open class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val viewModel by viewModel<SplashViewModel>()
|
private val viewModel by viewModels<SplashViewModel>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_splash)
|
setContentView(R.layout.activity_splash)
|
||||||
viewModel.navigateTo.observe(this) {
|
viewModel.navigateTo.observe(this) {
|
||||||
val intent = when (it.consume()) {
|
val intent = when (it.consume()) {
|
||||||
SplashViewModel.NavigateTo.HOME -> MainActivity.getStartIntent(this)
|
SplashViewModel.NavigateTo.HOME -> IntentCoordinator.mainActivitygetStartIntent(this)
|
||||||
SplashViewModel.NavigateTo.AUTHENTICATION -> AuthActivity.getStartIntent(this)
|
SplashViewModel.NavigateTo.AUTHENTICATION -> IntentCoordinator.authActivitygetStartIntent(this)
|
||||||
null -> return@observe
|
null -> return@observe
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
||||||
import org.fnives.test.showcase.ui.shared.Event
|
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>>()
|
private val _navigateTo = MutableLiveData<Event<NavigateTo>>()
|
||||||
val navigateTo: LiveData<Event<NavigateTo>> = _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
|
// package org.fnives.test.showcase.testutils.statesetup
|
||||||
|
//
|
||||||
import kotlinx.coroutines.runBlocking
|
// import kotlinx.coroutines.runBlocking
|
||||||
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
// import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
|
||||||
import org.fnives.test.showcase.core.login.LoginUseCase
|
// import org.fnives.test.showcase.core.login.LoginUseCase
|
||||||
import org.fnives.test.showcase.core.login.LogoutUseCase
|
// import org.fnives.test.showcase.core.login.LogoutUseCase
|
||||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
// import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
// import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
// import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||||
import org.koin.test.KoinTest
|
// import org.koin.test.KoinTest
|
||||||
import org.koin.test.get
|
// import org.koin.test.get
|
||||||
|
//
|
||||||
object SetupLoggedInState : KoinTest {
|
// object SetupLoggedInState : KoinTest {
|
||||||
|
//
|
||||||
private val logoutUseCase get() = get<LogoutUseCase>()
|
// private val logoutUseCase get() = get<LogoutUseCase>()
|
||||||
private val loginUseCase get() = get<LoginUseCase>()
|
// private val loginUseCase get() = get<LoginUseCase>()
|
||||||
private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>()
|
// private val isUserLoggedInUseCase get() = get<IsUserLoggedInUseCase>()
|
||||||
|
//
|
||||||
fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
|
// fun setupLogin(mockServerScenarioSetup: MockServerScenarioSetup) {
|
||||||
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
|
// mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
|
||||||
runBlocking {
|
// runBlocking {
|
||||||
loginUseCase.invoke(LoginCredentials("a", "b"))
|
// loginUseCase.invoke(LoginCredentials("a", "b"))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
fun isLoggedIn() = isUserLoggedInUseCase.invoke()
|
// fun isLoggedIn() = isUserLoggedInUseCase.invoke()
|
||||||
|
//
|
||||||
fun setupLogout() {
|
// fun setupLogout() {
|
||||||
runBlocking { logoutUseCase.invoke() }
|
// 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.PullToRefresh
|
||||||
import org.fnives.test.showcase.testutils.viewactions.WithDrawable
|
import org.fnives.test.showcase.testutils.viewactions.WithDrawable
|
||||||
import org.fnives.test.showcase.testutils.viewactions.notIntended
|
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
|
import org.hamcrest.Matchers.allOf
|
||||||
|
|
||||||
class HomeRobot : Robot {
|
class HomeRobot : Robot {
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
|
||||||
.respondWith(Instrumentation.ActivityResult(0, null))
|
.respondWith(Instrumentation.ActivityResult(0, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,11 +37,11 @@ class HomeRobot : Robot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertNavigatedToAuth() = apply {
|
fun assertNavigatedToAuth() = apply {
|
||||||
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertDidNotNavigateToAuth() = apply {
|
fun assertDidNotNavigateToAuth() = apply {
|
||||||
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clickSignOut() = apply {
|
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.configuration.TestConfigurationsFactory
|
||||||
import org.fnives.test.showcase.testutils.robot.Robot
|
import org.fnives.test.showcase.testutils.robot.Robot
|
||||||
import org.fnives.test.showcase.testutils.viewactions.notIntended
|
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
|
import org.hamcrest.core.IsNot.not
|
||||||
|
|
||||||
class LoginRobot(
|
class LoginRobot(
|
||||||
|
|
@ -37,7 +37,7 @@ class LoginRobot(
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
intending(hasComponent(MainActivity::class.java.canonicalName))
|
intending(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,10 +91,10 @@ class LoginRobot(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertNavigatedToHome() = apply {
|
fun assertNavigatedToHome() = apply {
|
||||||
intended(hasComponent(MainActivity::class.java.canonicalName))
|
intended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertNotNavigatedToHome() = apply {
|
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 androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
import org.fnives.test.showcase.testutils.robot.Robot
|
import org.fnives.test.showcase.testutils.robot.Robot
|
||||||
import org.fnives.test.showcase.testutils.viewactions.notIntended
|
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.fnives.test.showcase.ui.home.MainActivity
|
|
||||||
|
|
||||||
class SplashRobot : Robot {
|
class SplashRobot : Robot {
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
|
Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
|
||||||
.respondWith(Instrumentation.ActivityResult(0, null))
|
.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))
|
.respondWith(Instrumentation.ActivityResult(0, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,18 +22,18 @@ class SplashRobot : Robot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertHomeIsStarted() = apply {
|
fun assertHomeIsStarted() = apply {
|
||||||
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
|
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertHomeIsNotStarted() = apply {
|
fun assertHomeIsNotStarted() = apply {
|
||||||
notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
|
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertAuthIsStarted() = apply {
|
fun assertAuthIsStarted() = apply {
|
||||||
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
|
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assertAuthIsNotStarted() = apply {
|
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() {
|
object : Statement() {
|
||||||
override fun evaluate() {
|
override fun evaluate() {
|
||||||
if (GlobalContext.getOrNull() == null) {
|
if (GlobalContext.getOrNull() == null) {
|
||||||
val application = ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
|
val application =
|
||||||
|
ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
|
||||||
startKoin {
|
startKoin {
|
||||||
androidContext(application)
|
androidContext(application)
|
||||||
modules(createAppModules(BaseUrlProvider.get()))
|
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 {
|
buildscript {
|
||||||
ext.kotlin_version = "1.5.30"
|
ext.kotlin_version = "1.5.30"
|
||||||
ext.detekt_version = "1.18.1"
|
ext.detekt_version = "1.18.1"
|
||||||
|
ext.hilt_version = "2.38.1"
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
maven { url "https://plugins.gradle.org/m2/" }
|
maven { url "https://plugins.gradle.org/m2/" }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
||||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0"
|
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0"
|
||||||
|
|
@ -22,6 +24,15 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
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
|
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'
|
group = 'Tests'
|
||||||
description = 'Run all unit tests'
|
description = 'Run all unit tests'
|
||||||
}
|
}
|
||||||
|
|
||||||
task androidTests(dependsOn: "app:connectedAndroidTest"){
|
task androidTests(dependsOn: ["app:connectedKoinDebugAndroidTest", "app:connectedHiltDebugAndroidTest"]){
|
||||||
group = 'Tests'
|
group = 'Tests'
|
||||||
description = 'Run all Android tests'
|
description = 'Run all Android tests'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'java-library'
|
id 'java-library'
|
||||||
id 'kotlin'
|
id 'kotlin'
|
||||||
|
id 'kotlin-kapt'
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|
@ -14,15 +15,27 @@ compileKotlin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
|
|
||||||
api project(":model")
|
api project(":model")
|
||||||
implementation project(":network")
|
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 "io.insert-koin:koin-test-junit5:$koin_version"
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
||||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:$testing_kotlin_mockito_version"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
|
testImplementation "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_version"
|
||||||
testRuntimeOnly "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.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
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
|
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
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.Optional
|
||||||
import org.fnives.test.showcase.core.shared.mapIntoResource
|
import org.fnives.test.showcase.core.shared.mapIntoResource
|
||||||
import org.fnives.test.showcase.core.shared.wrapIntoAnswer
|
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.model.shared.Resource
|
||||||
import org.fnives.test.showcase.network.content.ContentRemoteSource
|
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 mutableContentFlow = MutableStateFlow(Optional<List<Content>>(null))
|
||||||
private val requestFlow: Flow<Resource<List<Content>>> = flow {
|
private val requestFlow: Flow<Resource<List<Content>>> = flow {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package org.fnives.test.showcase.core.content
|
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()
|
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.ContentId
|
||||||
import org.fnives.test.showcase.model.content.FavouriteContent
|
import org.fnives.test.showcase.model.content.FavouriteContent
|
||||||
import org.fnives.test.showcase.model.shared.Resource
|
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 contentRepository: ContentRepository,
|
||||||
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
|
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.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.content.ContentId
|
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
|
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.AddContentToFavouriteUseCase
|
||||||
import org.fnives.test.showcase.core.content.ContentRepository
|
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.UserDataLocalStorage
|
||||||
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
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.module.Module
|
||||||
import org.koin.core.scope.Scope
|
import org.koin.core.scope.Scope
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
@ -42,7 +42,7 @@ fun repositoryModule() = module {
|
||||||
|
|
||||||
fun useCaseModule() = module {
|
fun useCaseModule() = module {
|
||||||
factory { LoginUseCase(get(), get()) }
|
factory { LoginUseCase(get(), get()) }
|
||||||
factory { LogoutUseCase(get()) }
|
factory { LogoutUseCase(get(), null) }
|
||||||
factory { GetAllContentUseCase(get(), get()) }
|
factory { GetAllContentUseCase(get(), get()) }
|
||||||
factory { AddContentToFavouriteUseCase(get()) }
|
factory { AddContentToFavouriteUseCase(get()) }
|
||||||
factory { RemoveContentFromFavouritesUseCase(get()) }
|
factory { RemoveContentFromFavouritesUseCase(get()) }
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package org.fnives.test.showcase.core.login
|
package org.fnives.test.showcase.core.login
|
||||||
|
|
||||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
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
|
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.model.shared.Answer
|
||||||
import org.fnives.test.showcase.network.auth.LoginRemoteSource
|
import org.fnives.test.showcase.network.auth.LoginRemoteSource
|
||||||
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
|
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 loginRemoteSource: LoginRemoteSource,
|
||||||
private val userDataLocalStorage: UserDataLocalStorage
|
private val userDataLocalStorage: UserDataLocalStorage
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
package org.fnives.test.showcase.core.login
|
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.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||||
import org.koin.core.context.loadKoinModules
|
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() {
|
suspend fun invoke() {
|
||||||
loadKoinModules(repositoryModule())
|
if (KoinPlatformTools.defaultContext().getOrNull() == null) {
|
||||||
|
reloadLoggedInModuleInjectModule?.reload()
|
||||||
|
} else {
|
||||||
|
loadKoinModules(repositoryModule())
|
||||||
|
}
|
||||||
storage.session = null
|
storage.session = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package org.fnives.test.showcase.core.session
|
package org.fnives.test.showcase.core.session
|
||||||
|
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class SessionExpirationAdapter(
|
internal class SessionExpirationAdapter @Inject constructor(
|
||||||
private val sessionExpirationListener: SessionExpirationListener
|
private val sessionExpirationListener: SessionExpirationListener
|
||||||
) :
|
) : NetworkSessionExpirationListener {
|
||||||
NetworkSessionExpirationListener {
|
|
||||||
|
|
||||||
override fun onSessionExpired() = sessionExpirationListener.onSessionExpired()
|
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.model.session.Session
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class NetworkSessionLocalStorageAdapter(
|
internal class NetworkSessionLocalStorageAdapter @Inject constructor(
|
||||||
private val userDataLocalStorage: UserDataLocalStorage
|
private val userDataLocalStorage: UserDataLocalStorage
|
||||||
) : NetworkSessionLocalStorage {
|
) : 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 kotlinx.coroutines.test.runBlockingTest
|
||||||
import org.fnives.test.showcase.core.content.ContentRepository
|
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.core.storage.UserDataLocalStorage
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
import org.fnives.test.showcase.model.network.BaseUrl
|
||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
|
@ -27,7 +28,7 @@ internal class LogoutUseCaseTest : KoinTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockUserDataLocalStorage = mock()
|
mockUserDataLocalStorage = mock()
|
||||||
sut = LogoutUseCase(mockUserDataLocalStorage)
|
sut = LogoutUseCase(mockUserDataLocalStorage, null)
|
||||||
startKoin {
|
startKoin {
|
||||||
modules(
|
modules(
|
||||||
createCoreModule(
|
createCoreModule(
|
||||||
|
|
@ -6,6 +6,7 @@ project.ext {
|
||||||
androidx_livedata_version = "2.3.1"
|
androidx_livedata_version = "2.3.1"
|
||||||
androidx_swiperefreshlayout_version = "1.1.0"
|
androidx_swiperefreshlayout_version = "1.1.0"
|
||||||
androidx_room_version = "2.3.0"
|
androidx_room_version = "2.3.0"
|
||||||
|
activity_ktx_version = "1.3.1"
|
||||||
|
|
||||||
coroutines_version = "1.4.3"
|
coroutines_version = "1.4.3"
|
||||||
koin_version = "3.1.2"
|
koin_version = "3.1.2"
|
||||||
|
|
@ -13,6 +14,7 @@ project.ext {
|
||||||
retrofit_version = "2.9.0"
|
retrofit_version = "2.9.0"
|
||||||
okhttp_version = "4.9.1"
|
okhttp_version = "4.9.1"
|
||||||
moshi_version = "1.12.0"
|
moshi_version = "1.12.0"
|
||||||
|
reloadable_module_version = "0.1.0"
|
||||||
|
|
||||||
testing_androidx_code_version = "1.4.0"
|
testing_androidx_code_version = "1.4.0"
|
||||||
testing_androidx_junit_version = "1.1.3"
|
testing_androidx_junit_version = "1.1.3"
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,8 @@ plugins {
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = 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"
|
implementation "com.squareup.moshi:moshi:$moshi_version"
|
||||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||||
|
|
||||||
|
// koin
|
||||||
api "io.insert-koin:koin-core:$koin_version"
|
api "io.insert-koin:koin-core:$koin_version"
|
||||||
|
|
||||||
|
// hilt
|
||||||
|
implementation "com.google.dagger:hilt-core:$hilt_version"
|
||||||
|
|
||||||
api project(":model")
|
api project(":model")
|
||||||
|
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
|
||||||
|
|
@ -28,4 +33,6 @@ dependencies {
|
||||||
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
|
testImplementation "io.insert-koin:koin-test-junit5:$koin_version"
|
||||||
testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version"
|
testImplementation "org.skyscreamer:jsonassert:$testing_json_assert_version"
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$testing_junit5_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 org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class LoginErrorConverter {
|
internal class LoginErrorConverter @Inject constructor() {
|
||||||
|
|
||||||
@Throws(ParsingException::class)
|
@Throws(ParsingException::class)
|
||||||
suspend fun invoke(request: suspend () -> Response<LoginResponse>): LoginStatusResponses =
|
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.ExceptionWrapper
|
||||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
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 loginService: LoginService,
|
||||||
private val loginErrorConverter: LoginErrorConverter
|
private val loginErrorConverter: LoginErrorConverter
|
||||||
) : LoginRemoteSource {
|
) : 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.ContentId
|
||||||
import org.fnives.test.showcase.model.content.ImageUrl
|
import org.fnives.test.showcase.model.content.ImageUrl
|
||||||
import org.fnives.test.showcase.network.shared.ExceptionWrapper
|
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> =
|
override suspend fun get(): List<Content> =
|
||||||
ExceptionWrapper.wrap {
|
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 okhttp3.OkHttpClient
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
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.ContentRemoteSource
|
||||||
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
|
import org.fnives.test.showcase.network.content.ContentRemoteSourceImpl
|
||||||
import org.fnives.test.showcase.network.content.ContentService
|
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.AuthenticationHeaderInterceptor
|
||||||
import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
|
import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
package org.fnives.test.showcase.network.session
|
package org.fnives.test.showcase.network.session
|
||||||
|
|
||||||
import okhttp3.Request
|
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 =
|
fun hasToken(okhttpRequest: Request): Boolean =
|
||||||
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken
|
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken
|
||||||
|
|
||||||
fun attachToken(okhttpRequest: Request): Request =
|
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 {
|
companion object {
|
||||||
private const val KEY = "Authorization"
|
private const val KEY = "Authorization"
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.Route
|
import okhttp3.Route
|
||||||
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
|
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 networkSessionLocalStorage: NetworkSessionLocalStorage,
|
||||||
private val loginRemoteSource: LoginRemoteSourceImpl,
|
private val loginRemoteSource: LoginRemoteSourceImpl,
|
||||||
private val authenticationHeaderUtils: AuthenticationHeaderUtils,
|
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 kotlinx.coroutines.runBlocking
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
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.ContentData
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
|
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
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 kotlinx.coroutines.runBlocking
|
||||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
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.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
|
||||||
import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson
|
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.mockserver.scenario.auth.AuthScenario
|
||||||
|
|
@ -3,7 +3,7 @@ package org.fnives.test.showcase.network.content
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.mockwebserver.MockWebServer
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
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.NetworkSessionExpirationListener
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
||||||
import org.junit.jupiter.api.AfterEach
|
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 kotlinx.coroutines.runBlocking
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
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.ContentData
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
||||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
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 kotlinx.coroutines.runBlocking
|
||||||
import org.fnives.test.showcase.model.network.BaseUrl
|
import org.fnives.test.showcase.model.network.BaseUrl
|
||||||
import org.fnives.test.showcase.model.session.Session
|
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.ContentData
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
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.mockserver.scenario.refresh.RefreshTokenScenario
|
||||||
Loading…
Add table
Add a link
Reference in a new issue