Merge pull request #42 from fknives/issue#31-move-hilt-to-separate-branch

Issue#31 move hilt to separate branch
This commit is contained in:
Gergely Hegedis 2022-01-24 18:04:35 +02:00 committed by GitHub
commit 4b985bec25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 87 additions and 1884 deletions

View file

@ -2,8 +2,6 @@ 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 {
@ -33,19 +31,6 @@ android {
} }
} }
flavorDimensions 'di' flavorDimensions 'di'
productFlavors {
hilt {
dimension 'di'
resValue "string", "app_name", "Hilt Test-ShowCase"
applicationId "org.fnives.test.showcase.hilt"
testInstrumentationRunner "org.fnives.test.showcase.testutils.configuration.HiltTestRunner"
}
koin {
dimension 'di'
resValue "string", "app_name", "Koin Test-ShowCase"
applicationId "org.fnives.test.showcase.koin"
}
}
buildFeatures { buildFeatures {
viewBinding true viewBinding true
@ -56,29 +41,11 @@ android {
java.srcDirs += "src/sharedTest/java" java.srcDirs += "src/sharedTest/java"
assets.srcDirs += files("$projectDir/schemas".toString()) assets.srcDirs += files("$projectDir/schemas".toString())
} }
androidTestHilt {
java.srcDirs += "src/sharedTestHilt/java"
assets.srcDirs += files("$projectDir/schemas".toString())
}
androidTestKoin {
java.srcDirs += "src/sharedTestKoin/java"
assets.srcDirs += files("$projectDir/schemas".toString())
}
test { test {
java.srcDirs += "src/sharedTest/java" java.srcDirs += "src/sharedTest/java"
java.srcDirs += "src/robolectricTest/java" java.srcDirs += "src/robolectricTest/java"
resources.srcDirs += files("$projectDir/schemas".toString()) resources.srcDirs += files("$projectDir/schemas".toString())
} }
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
@ -90,17 +57,10 @@ 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
testKoinDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble') testDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testKoinReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble') testReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testHiltDebugUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
testHiltReleaseUnitTest.dependsOn tasks.getByPath(':mockserver:assemble')
} }
dependencies { dependencies {
@ -113,13 +73,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version" implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version"
// Koin implementation "io.insert-koin:koin-android:$koin_version"
koinImplementation "io.insert-koin:koin-android:$koin_version"
// Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kaptHilt "com.google.dagger:hilt-compiler:$hilt_version"
hiltImplementation "androidx.activity:activity-ktx:$activity_ktx_version"
implementation "androidx.room:room-runtime:$androidx_room_version" implementation "androidx.room:room-runtime:$androidx_room_version"
kapt "androidx.room:room-compiler:$androidx_room_version" kapt "androidx.room:room-compiler:$androidx_room_version"
@ -151,8 +105,6 @@ dependencies {
testImplementation project(':mockserver') testImplementation project(':mockserver')
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 "androidx.room:room-testing:$androidx_room_version" androidTestImplementation "androidx.room:room-testing:$androidx_room_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
@ -167,9 +119,6 @@ dependencies {
androidTestImplementation project(':mockserver') androidTestImplementation project(':mockserver')
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
implementation "io.reactivex.rxjava3:rxjava:3.1.3" implementation "io.reactivex.rxjava3:rxjava:3.1.3"
} }

View file

@ -1,14 +0,0 @@
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
}
}

View file

@ -1,12 +0,0 @@
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)
}

View file

@ -1,33 +0,0 @@
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()
}

View file

@ -1,22 +0,0 @@
<?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>

View file

@ -1,7 +0,0 @@
package org.fnives.test.showcase
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TestShowcaseApplication : Application()

View file

@ -1,52 +0,0 @@
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
}

View file

@ -1,14 +0,0 @@
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)
}

View file

@ -1,12 +0,0 @@
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")
}

View file

@ -1,12 +0,0 @@
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)
}
}

View file

@ -1,12 +0,0 @@
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)
}
}

View file

@ -1,8 +0,0 @@
package org.fnives.test.showcase.ui.splash
import android.annotation.SuppressLint
import dagger.hilt.android.AndroidEntryPoint
@SuppressLint("CustomSplashScreen")
@AndroidEntryPoint
class HiltSplashActivity : SplashActivity()

View file

@ -1,21 +0,0 @@
<?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>

View file

@ -13,6 +13,18 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
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>

View file

@ -4,15 +4,10 @@ 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.IntentCoordinator import org.fnives.test.showcase.ui.IntentCoordinator
import javax.inject.Inject
class SessionExpirationListenerImpl @Inject constructor( class SessionExpirationListenerImpl(private val context: Context) : SessionExpirationListener {
@ApplicationContext
private val context: Context
) : SessionExpirationListener {
override fun onSessionExpired() { override fun onSessionExpired() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {

View file

@ -4,9 +4,8 @@ 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( class FavouriteContentLocalStorageImpl(
private val favouriteDao: FavouriteDao private val favouriteDao: FavouriteDao
) : FavouriteContentLocalStorage { ) : FavouriteContentLocalStorage {

View file

@ -4,17 +4,14 @@ 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
@HiltViewModel class AuthViewModel(private val loginUseCase: LoginUseCase) : ViewModel() {
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

View file

@ -6,8 +6,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
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.launch import kotlinx.coroutines.launch
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.core.content.FetchContentUseCase import org.fnives.test.showcase.core.content.FetchContentUseCase
@ -18,10 +16,8 @@ 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
@HiltViewModel class MainViewModel(
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,

View file

@ -4,15 +4,12 @@ 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
@HiltViewModel class SplashViewModel(isUserLoggedInUseCase: IsUserLoggedInUseCase) : ViewModel() {
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

View file

@ -1,4 +1,5 @@
<resources> <resources>
<string name="app_name">Test ShowCase</string>
<string name="login">Login</string> <string name="login">Login</string>
<string name="username">Username</string> <string name="username">Username</string>
<string name="password">Password</string> <string name="password">Password</string>

View file

@ -1,101 +0,0 @@
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.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
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: TestDispatcher
@Before
fun setUp() {
testDispatcher = StandardTestDispatcher(TestCoroutineScheduler())
DatabaseInitialization.dispatcher = testDispatcher
hiltRule.inject()
}
/** GIVEN content_id WHEN added to Favourite THEN it can be read out */
@Test
fun addingContentIdToFavouriteCanBeLaterReadOut() = runTest(testDispatcher) {
val expected = listOf(ContentId("a"))
sut.markAsFavourite(ContentId("a"))
val actual = sut.observeFavourites().first()
Assert.assertEquals(expected, actual)
}
/** GIVEN content_id added WHEN removed to Favourite THEN it no longer can be read out */
@Test
fun contentIdAddedThenRemovedCanNoLongerBeReadOut() = runTest(testDispatcher) {
val expected = listOf<ContentId>()
sut.markAsFavourite(ContentId("b"))
sut.deleteAsFavourite(ContentId("b"))
val actual = sut.observeFavourites().first()
Assert.assertEquals(expected, actual)
}
/** GIVEN empty database WHILE observing content WHEN favourite added THEN change is emitted */
@Test
fun addingFavouriteUpdatesExistingObservers() = runTest(testDispatcher) {
val expected = listOf(listOf(), listOf(ContentId("a")))
val actual = async(coroutineContext) {
sut.observeFavourites().take(2).toList()
}
advanceUntilIdle()
sut.markAsFavourite(ContentId("a"))
advanceUntilIdle()
Assert.assertEquals(expected, actual.getCompleted())
}
/** GIVEN non empty database WHILE observing content WHEN favourite removed THEN change is emitted */
@Test
fun removingFavouriteUpdatesExistingObservers() = runTest(testDispatcher) {
val expected = listOf(listOf(ContentId("a")), listOf())
sut.markAsFavourite(ContentId("a"))
val actual = async(coroutineContext) {
sut.observeFavourites().take(2).toList()
}
advanceUntilIdle()
sut.deleteAsFavourite(ContentId("a"))
advanceUntilIdle()
Assert.assertEquals(expected, actual.getCompleted())
}
}

View file

@ -1,4 +0,0 @@
sdk=28
shadows=org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
instrumentedPackages=androidx.loader.content
application=dagger.hilt.android.testing.HiltTestApplication

View file

@ -5,8 +5,9 @@ import androidx.test.core.app.ActivityScenario
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.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule
import org.fnives.test.showcase.ui.ActivityClassHolder import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.HomeRobot import org.fnives.test.showcase.ui.home.HomeRobot
import org.fnives.test.showcase.ui.home.MainActivity
import org.fnives.test.showcase.ui.login.LoginRobot import org.fnives.test.showcase.ui.login.LoginRobot
import org.koin.test.KoinTest import org.koin.test.KoinTest
@ -22,7 +23,7 @@ object SetupAuthenticationState : KoinTest {
password = "b" password = "b"
) )
) )
val activityScenario = ActivityScenario.launch(ActivityClassHolder.authActivity().java) val activityScenario = ActivityScenario.launch(AuthActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED) activityScenario.moveToState(Lifecycle.State.RESUMED)
val loginRobot = LoginRobot() val loginRobot = LoginRobot()
loginRobot.setupIntentResults() loginRobot.setupIntentResults()
@ -39,7 +40,7 @@ object SetupAuthenticationState : KoinTest {
fun setupLogout( fun setupLogout(
mainDispatcherTestRule: MainDispatcherTestRule mainDispatcherTestRule: MainDispatcherTestRule
) { ) {
val activityScenario = ActivityScenario.launch(ActivityClassHolder.mainActivity().java) val activityScenario = ActivityScenario.launch(MainActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED) activityScenario.moveToState(Lifecycle.State.RESUMED)
val homeRobot = HomeRobot() val homeRobot = HomeRobot()
homeRobot homeRobot

View file

@ -26,14 +26,14 @@ import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState
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.ActivityClassHolder import org.fnives.test.showcase.ui.auth.AuthActivity
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(ActivityClassHolder.authActivity().java.canonicalName)) Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null)) .respondWith(Instrumentation.ActivityResult(0, null))
} }
@ -42,11 +42,11 @@ class HomeRobot : Robot {
} }
fun assertNavigatedToAuth() = apply { fun assertNavigatedToAuth() = apply {
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName)) Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
} }
fun assertDidNotNavigateToAuth() = apply { fun assertDidNotNavigateToAuth() = apply {
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName)) notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
} }
fun clickSignOut() = apply { fun clickSignOut() = apply {

View file

@ -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.ActivityClassHolder import org.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNot.not
class LoginRobot( class LoginRobot(
@ -41,7 +41,7 @@ class LoginRobot(
} }
fun setupIntentResults() { fun setupIntentResults() {
intending(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName)) intending(hasComponent(MainActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
} }
@ -95,10 +95,10 @@ class LoginRobot(
} }
fun assertNavigatedToHome() = apply { fun assertNavigatedToHome() = apply {
intended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName)) intended(hasComponent(MainActivity::class.java.canonicalName))
} }
fun assertNotNavigatedToHome() = apply { fun assertNotNavigatedToHome() = apply {
notIntended(hasComponent(ActivityClassHolder.mainActivity().java.canonicalName)) notIntended(hasComponent(MainActivity::class.java.canonicalName))
} }
} }

View file

@ -8,15 +8,16 @@ import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.robot.Robot import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState
import org.fnives.test.showcase.testutils.viewactions.notIntended import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.ActivityClassHolder import org.fnives.test.showcase.ui.auth.AuthActivity
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(ActivityClassHolder.mainActivity().java.canonicalName)) Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null)) .respondWith(Instrumentation.ActivityResult(0, null))
Intents.intending(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName)) Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(0, null)) .respondWith(Instrumentation.ActivityResult(0, null))
} }
@ -42,18 +43,18 @@ class SplashRobot : Robot {
} }
fun assertHomeIsStarted() = apply { fun assertHomeIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName)) Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
} }
fun assertHomeIsNotStarted() = apply { fun assertHomeIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.mainActivity().java.canonicalName)) notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
} }
fun assertAuthIsStarted() = apply { fun assertAuthIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName)) Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
} }
fun assertAuthIsNotStarted() = apply { fun assertAuthIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(ActivityClassHolder.authActivity().java.canonicalName)) notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
} }
} }

View file

@ -1,41 +0,0 @@
package org.fnives.test.showcase.testutils.idling
import androidx.annotation.CheckResult
import androidx.test.espresso.IdlingResource
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")
}
}

View file

@ -1,14 +0,0 @@
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
}

View file

@ -1,251 +0,0 @@
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.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 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 networkSynchronization: NetworkSynchronization
private lateinit var disposable: Disposable
@Before
fun setUp() {
SpecificTestConfigurationsFactory.createServerTypeConfiguration()
.invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
hiltRule.inject()
disposable = networkSynchronization.registerNetworkingSynchronization()
homeRobot.setupLogin(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup)
}
@After
fun tearDown() {
activityScenario.moveToState(Lifecycle.State.DESTROYED)
disposable.dispose()
}
/** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */
@Test
fun signOutClickedResultsInNavigation() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Error(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickSignOut()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
homeRobot.assertNavigatedToAuth()
}
/** GIVEN success response WHEN data is returned THEN it is shown on the ui */
@Test
fun successfulDataLoadingShowsTheElementsOnTheUI() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
ContentData.contentSuccess.forEachIndexed { index, content ->
homeRobot.assertContainsItem(index, FavouriteContent(content, false))
}
homeRobot.assertDidNotNavigateToAuth()
}
/** GIVEN success response WHEN item is clicked THEN ui is updated */
@Test
fun clickingOnListElementUpdatesTheElementsFavouriteState() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
homeRobot.assertContainsItem(0, expectedItem)
.assertDidNotNavigateToAuth()
}
/** GIVEN success response WHEN item is clicked THEN ui is updated even if activity is recreated */
@Test
fun elementFavouritedIsKeptEvenIfActivityIsRecreated() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(0, 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(0, expectedItem)
.assertDidNotNavigateToAuth()
}
/** GIVEN success response WHEN item is clicked then clicked again THEN ui is updated */
@Test
fun clickingAnElementMultipleTimesProperlyUpdatesIt() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), false)
homeRobot.assertContainsItem(0, expectedItem)
.assertDidNotNavigateToAuth()
}
/** GIVEN error response WHEN loaded THEN error is Shown */
@Test
fun networkErrorResultsInUIErrorStateShown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Error(false))
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.assertContainsNoItems()
.assertContainsError()
.assertDidNotNavigateToAuth()
}
/** GIVEN error response then success WHEN retried THEN success is shown */
@Test
fun retryingFromErrorStateAndSucceedingShowsTheData() {
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.forEachIndexed { index, content ->
homeRobot.assertContainsItem(index, FavouriteContent(content, false))
}
homeRobot.assertDidNotNavigateToAuth()
}
/** GIVEN success then error WHEN retried THEN error is shown */
@Test
fun errorIsShownIfTheDataIsFetchedAndErrorIsReceived() {
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()
}
/** GIVEN unauthenticated then success WHEN loaded THEN success is shown */
@Test
fun authenticationIsHandledWithASingleLoading() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Unauthorized(false)
.then(ContentScenario.Success(true))
)
.setScenario(RefreshTokenScenario.Success)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
ContentData.contentSuccess.forEachIndexed { index, content ->
homeRobot.assertContainsItem(index, FavouriteContent(content, false))
}
homeRobot.assertDidNotNavigateToAuth()
}
/** GIVEN unauthenticated then error WHEN loaded THEN navigated to auth */
@Test
fun sessionExpirationResultsInNavigation() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Unauthorized(false))
.setScenario(RefreshTokenScenario.Error)
activityScenario = ActivityScenario.launch(HiltMainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
homeRobot.assertNavigatedToAuth()
}
}

View file

@ -1,170 +0,0 @@
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()
}
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
@Test
fun properLoginResultsInNavigationToHome() {
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()
}
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
@Test
fun emptyPasswordShowsProperErrorMessage() {
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setUsername("banan")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loginRobot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
/** GIVEN password and empty username WHEN signIn THEN error username is shown */
@Test
fun emptyUserNameShowsProperErrorMessage() {
activityScenario = ActivityScenario.launch(HiltAuthActivity::class.java)
loginRobot
.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loginRobot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
/** GIVEN password and username and invalid credentials response WHEN signIn THEN error invalid credentials is shown */
@Test
fun invalidCredentialsGivenShowsProperErrorMessage() {
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()
}
/** GIVEN password and username and error response WHEN signIn THEN error invalid credentials is shown */
@Test
fun networkErrorShowsProperErrorMessage() {
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()
}
}

View file

@ -1,120 +0,0 @@
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.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 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()
}
/** GIVEN loggedInState WHEN opened THEN MainActivity is started */
@Test
fun loggedInStateNavigatesToHome() {
splashRobot.setupLoggedInState(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup)
activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
mainDispatcherTestRule.advanceTimeBy(501)
splashRobot.assertHomeIsStarted()
.assertAuthIsNotStarted()
}
/** GIVEN loggedOffState WHEN opened THEN AuthActivity is started */
@Test
fun loggedOutStatesNavigatesToAuthentication() {
splashRobot.setupLoggedOutState(mainDispatcherTestRule)
activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
mainDispatcherTestRule.advanceTimeBy(501)
splashRobot.assertAuthIsStarted()
.assertHomeIsNotStarted()
}
@Test
fun loggedOutStatesNotEnoughTime() {
splashRobot.setupLoggedOutState(mainDispatcherTestRule)
activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
mainDispatcherTestRule.advanceTimeBy(10)
splashRobot.assertAuthIsNotStarted()
.assertHomeIsNotStarted()
}
/** GIVEN loggedInState and not enough time WHEN opened THEN no activity is started */
@Test
fun loggedInStatesNotEnoughTime() {
splashRobot.setupLoggedInState(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup)
activityScenario = ActivityScenario.launch(HiltSplashActivity::class.java)
mainDispatcherTestRule.advanceTimeBy(10)
splashRobot.assertHomeIsNotStarted()
.assertAuthIsNotStarted()
}
}

View file

@ -1,14 +0,0 @@
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
}

View file

@ -2,14 +2,12 @@
buildscript { buildscript {
ext.kotlin_version = "1.6.10" ext.kotlin_version = "1.6.10"
ext.detekt_version = "1.19.0" ext.detekt_version = "1.19.0"
ext.hilt_version = "2.40.5"
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.4' classpath 'com.android.tools.build:gradle:7.0.4'
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.1" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1"
@ -24,15 +22,6 @@ 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
}
} }
} }
@ -40,17 +29,17 @@ task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
task unitTests(dependsOn: ["app:testKoinDebugUnitTest", "app:testHiltDebugUnitTest", "core:test", "network:test"]){ task unitTests(dependsOn: ["app:testDebugUnitTest", "core:test", "network:test"]){
group = 'Tests' group = 'Tests'
description = 'Run all unit tests' description = 'Run all unit tests'
} }
task robolectricTests(dependsOn: ["app:testKoinDebugUnitTest", "app:testHiltDebugUnitTest"]){ task robolectricTests(dependsOn: ["app:testDebugUnitTest"]){
group = 'Tests' group = 'Tests'
description = 'Run all robolectric tests' description = 'Run all robolectric tests'
} }
task androidTests(dependsOn: ["app:connectedKoinDebugAndroidTest", "app:connectedHiltDebugAndroidTest"]){ task androidTests(dependsOn: ["app:connectedDebugAndroidTest"]){
group = 'Tests' group = 'Tests'
description = 'Run all Android tests' description = 'Run all Android tests'
} }

View file

@ -9,12 +9,6 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
compileKotlin {
kotlinOptions {
freeCompilerArgs += ['-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi']
}
}
kapt { kapt {
correctErrorTypes = true correctErrorTypes = true
} }
@ -25,18 +19,11 @@ dependencies {
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" testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
testImplementation "app.cash.turbine:turbine:$turbine_version" testImplementation "app.cash.turbine:turbine:$turbine_version"
} }

View file

@ -2,9 +2,8 @@ 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 @Inject internal constructor( class AddContentToFavouriteUseCase internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) { ) {

View file

@ -7,7 +7,6 @@ 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
@ -15,7 +14,7 @@ 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 @LoggedInModuleInject constructor( internal class ContentRepository(
private val contentRemoteSource: ContentRemoteSource private val contentRemoteSource: ContentRemoteSource
) { ) {

View file

@ -1,8 +1,6 @@
package org.fnives.test.showcase.core.content package org.fnives.test.showcase.core.content
import javax.inject.Inject class FetchContentUseCase internal constructor(private val contentRepository: ContentRepository) {
class FetchContentUseCase @Inject internal constructor(private val contentRepository: ContentRepository) {
fun invoke() = contentRepository.fetch() fun invoke() = contentRepository.fetch()
} }

View file

@ -7,9 +7,8 @@ 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 @Inject internal constructor( class GetAllContentUseCase internal constructor(
private val contentRepository: ContentRepository, private val contentRepository: ContentRepository,
private val favouriteContentLocalStorage: FavouriteContentLocalStorage private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) { ) {

View file

@ -2,9 +2,8 @@ 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 @Inject internal constructor( class RemoveContentFromFavouritesUseCase internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) { ) {

View file

@ -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.koin.createNetworkModules import org.fnives.test.showcase.network.di.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(), null) } factory { LogoutUseCase(get()) }
factory { GetAllContentUseCase(get(), get()) } factory { GetAllContentUseCase(get(), get()) }
factory { AddContentToFavouriteUseCase(get()) } factory { AddContentToFavouriteUseCase(get()) }
factory { RemoveContentFromFavouritesUseCase(get()) } factory { RemoveContentFromFavouritesUseCase(get()) }

View file

@ -1,33 +0,0 @@
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)
}

View file

@ -1,8 +0,0 @@
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

View file

@ -1,9 +1,8 @@
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 @Inject constructor( class IsUserLoggedInUseCase(
private val userDataLocalStorage: UserDataLocalStorage private val userDataLocalStorage: UserDataLocalStorage
) { ) {

View file

@ -7,9 +7,8 @@ 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 @Inject internal constructor( class LoginUseCase internal constructor(
private val loginRemoteSource: LoginRemoteSource, private val loginRemoteSource: LoginRemoteSource,
private val userDataLocalStorage: UserDataLocalStorage private val userDataLocalStorage: UserDataLocalStorage
) { ) {

View file

@ -1,22 +1,15 @@
package org.fnives.test.showcase.core.login package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.di.hilt.ReloadLoggedInModuleInjectModule
import org.fnives.test.showcase.core.di.koin.repositoryModule 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( class LogoutUseCase(
private val storage: UserDataLocalStorage, private val storage: UserDataLocalStorage
private val reloadLoggedInModuleInjectModule: ReloadLoggedInModuleInjectModule?
) { ) {
suspend fun invoke() { suspend fun invoke() {
if (KoinPlatformTools.defaultContext().getOrNull() == null) {
reloadLoggedInModuleInjectModule?.reload()
} else {
loadKoinModules(repositoryModule()) loadKoinModules(repositoryModule())
}
storage.session = null storage.session = null
} }
} }

View file

@ -1,9 +1,8 @@
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 @Inject constructor( internal class SessionExpirationAdapter(
private val sessionExpirationListener: SessionExpirationListener private val sessionExpirationListener: SessionExpirationListener
) : NetworkSessionExpirationListener { ) : NetworkSessionExpirationListener {

View file

@ -2,9 +2,8 @@ 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 @Inject constructor( internal class NetworkSessionLocalStorageAdapter(
private val userDataLocalStorage: UserDataLocalStorage private val userDataLocalStorage: UserDataLocalStorage
) : NetworkSessionLocalStorage { ) : NetworkSessionLocalStorage {

View file

@ -1,10 +1,9 @@
package org.fnives.test.showcase.core.login.koin package org.fnives.test.showcase.core.login
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.core.content.ContentRepository import org.fnives.test.showcase.core.content.ContentRepository
import org.fnives.test.showcase.core.di.koin.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
@ -31,7 +30,7 @@ internal class LogoutUseCaseTest : KoinTest {
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
mockUserDataLocalStorage = mock() mockUserDataLocalStorage = mock()
sut = LogoutUseCase(mockUserDataLocalStorage, null) sut = LogoutUseCase(mockUserDataLocalStorage)
startKoin { startKoin {
modules( modules(
createCoreModule( createCoreModule(

View file

@ -1,62 +0,0 @@
package org.fnives.test.showcase.core.login.hilt
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
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.DisplayName
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")
@OptIn(ExperimentalCoroutinesApi::class)
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)
}
@DisplayName("WHEN no call THEN storage is not interacted")
@Test
fun initializedDoesntAffectStorage() {
verifyZeroInteractions(mockUserDataLocalStorage)
}
@DisplayName("WHEN logout invoked THEN storage is cleared")
@Test
fun logoutResultsInStorageCleaning() = runTest {
val repositoryBefore = contentRepository
sut.invoke()
testCoreComponent.inject(this@LogoutUseCaseTest)
val repositoryAfter = contentRepository
verify(mockUserDataLocalStorage, times(1)).session = null
verifyNoMoreInteractions(mockUserDataLocalStorage)
Assertions.assertNotSame(repositoryBefore, repositoryAfter)
}
}

View file

@ -1,36 +0,0 @@
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)
}

View file

@ -8,6 +8,13 @@ subprojects { module ->
showStandardStreams true showStandardStreams true
} }
} }
module.tasks.configureEach { task ->
if (task.taskIdentity.type.toString() == "class org.jetbrains.kotlin.gradle.tasks.KotlinCompile") {
task.kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
}
} }
plugins.withId("com.android.application") { plugins.withId("com.android.application") {

View file

@ -15,7 +15,6 @@ 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.13.0" moshi_version = "1.13.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"

View file

@ -1,6 +0,0 @@
package org.fnives.test.showcase.hilt
import javax.inject.Qualifier
@Qualifier
annotation class SessionLessQualifier

View file

@ -1,6 +0,0 @@
package org.fnives.test.showcase.hilt
import javax.inject.Qualifier
@Qualifier
annotation class SessionQualifier

View file

@ -21,9 +21,6 @@ dependencies {
// koin // 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"
@ -33,6 +30,4 @@ 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"
} }

View file

@ -7,9 +7,8 @@ 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 @Inject constructor() { internal class LoginErrorConverter {
@Throws(ParsingException::class) @Throws(ParsingException::class)
suspend fun invoke(request: suspend () -> Response<LoginResponse>): LoginStatusResponses = suspend fun invoke(request: suspend () -> Response<LoginResponse>): LoginStatusResponses =

View file

@ -7,9 +7,8 @@ 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 @Inject constructor( internal class LoginRemoteSourceImpl(
private val loginService: LoginService, private val loginService: LoginService,
private val loginErrorConverter: LoginErrorConverter private val loginErrorConverter: LoginErrorConverter
) : LoginRemoteSource { ) : LoginRemoteSource {

View file

@ -4,9 +4,8 @@ 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 @Inject constructor( internal class ContentRemoteSourceImpl(
private val contentService: ContentService private val contentService: ContentService
) : ContentRemoteSource { ) : ContentRemoteSource {

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.network.di.koin package org.fnives.test.showcase.network.di
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.model.network.BaseUrl
@ -9,7 +9,6 @@ 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

View file

@ -1,17 +0,0 @@
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
}

View file

@ -1,96 +0,0 @@
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)
}

View file

@ -1,9 +1,8 @@
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 @Inject constructor( internal class AuthenticationHeaderUtils(
private val networkSessionLocalStorage: NetworkSessionLocalStorage private val networkSessionLocalStorage: NetworkSessionLocalStorage
) { ) {

View file

@ -6,9 +6,8 @@ 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 @Inject constructor( internal class SessionAuthenticator(
private val networkSessionLocalStorage: NetworkSessionLocalStorage, private val networkSessionLocalStorage: NetworkSessionLocalStorage,
private val loginRemoteSource: LoginRemoteSourceImpl, private val loginRemoteSource: LoginRemoteSourceImpl,
private val authenticationHeaderUtils: AuthenticationHeaderUtils, private val authenticationHeaderUtils: AuthenticationHeaderUtils,

View file

@ -1,44 +0,0 @@
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)
}

View file

@ -1,8 +1,9 @@
package org.fnives.test.showcase.network.auth package org.fnives.test.showcase.network.auth
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest
import okhttp3.internal.http.RealResponseBody import okhttp3.internal.http.RealResponseBody
import okio.Buffer import okio.Buffer
import org.fnives.test.showcase.model.session.Session import org.fnives.test.showcase.model.session.Session
@ -18,6 +19,7 @@ import retrofit2.Response
import java.io.IOException import java.io.IOException
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
class LoginErrorConverterTest { class LoginErrorConverterTest {
private lateinit var sut: LoginErrorConverter private lateinit var sut: LoginErrorConverter
@ -49,7 +51,7 @@ class LoginErrorConverterTest {
@DisplayName("GIVEN 400 error response WHEN parsing login error THEN invalid credentials is returned") @DisplayName("GIVEN 400 error response WHEN parsing login error THEN invalid credentials is returned")
@Test @Test
fun code400ResponseResultsInInvalidCredentials() = runBlockingTest { fun code400ResponseResultsInInvalidCredentials() = runTest {
val expected = LoginStatusResponses.InvalidCredentials val expected = LoginStatusResponses.InvalidCredentials
val actual = sut.invoke { val actual = sut.invoke {
@ -62,7 +64,7 @@ class LoginErrorConverterTest {
@DisplayName("GIVEN successful response WHEN parsing login error THEN successful response is returned") @DisplayName("GIVEN successful response WHEN parsing login error THEN successful response is returned")
@Test @Test
fun successResponseResultsInSessionResponse() = runBlockingTest { fun successResponseResultsInSessionResponse() = runTest {
val loginResponse = LoginResponse("a", "r") val loginResponse = LoginResponse("a", "r")
val expectedSession = Session(accessToken = loginResponse.accessToken, refreshToken = loginResponse.refreshToken) val expectedSession = Session(accessToken = loginResponse.accessToken, refreshToken = loginResponse.refreshToken)
val expected = LoginStatusResponses.Success(expectedSession) val expected = LoginStatusResponses.Success(expectedSession)

View file

@ -1,9 +1,8 @@
package org.fnives.test.showcase.network.auth.koin package org.fnives.test.showcase.network.auth
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.auth.LoginRemoteSourceImpl 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.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

View file

@ -1,11 +1,10 @@
package org.fnives.test.showcase.network.auth.koin package org.fnives.test.showcase.network.auth
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.koin.createNetworkModules import org.fnives.test.showcase.network.di.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

View file

@ -1,103 +0,0 @@
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.DisplayName
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)
}
@DisplayName("GIVEN successful response WHEN refresh request is fired THEN session is returned")
@Test
fun successResponseResultsInSession() = runBlocking {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success)
val expected = ContentData.refreshSuccessResponse
val actual = sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN successful response WHEN refresh request is fired THEN the request is setup properly")
@Test
fun refreshRequestIsSetupProperly() = 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())
}
@DisplayName("GIVEN internal error response WHEN refresh request is fired THEN network exception is thrown")
@Test
fun generalErrorResponseResultsInNetworkException() {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
Assertions.assertThrows(NetworkException::class.java) {
runBlocking { sut.refresh(ContentData.refreshSuccessResponse.refreshToken) }
}
}
@DisplayName("GIVEN invalid json response WHEN refresh request is fired THEN network exception is thrown")
@Test
fun jsonErrorResponseResultsInParsingException() {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.UnexpectedJsonAsSuccessResponse)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
}
}
@DisplayName("GIVEN malformed json response WHEN refresh request is fired THEN parsing exception is thrown")
@Test
fun malformedJsonErrorResponseResultsInParsingException() {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.MalformedJson)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
}
}
}

View file

@ -1,120 +0,0 @@
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")) }
}
}
}

View file

@ -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.koin.createNetworkModules import org.fnives.test.showcase.network.di.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

View file

@ -1,9 +1,8 @@
package org.fnives.test.showcase.network.content.koin package org.fnives.test.showcase.network.content
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.content.ContentRemoteSourceImpl 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.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

View file

@ -1,10 +1,9 @@
package org.fnives.test.showcase.network.content.koin package org.fnives.test.showcase.network.content
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.content.ContentRemoteSourceImpl 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.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

View file

@ -1,121 +0,0 @@
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.DisplayName
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)
}
@DisplayName("GIVEN successful response WHEN getting content THEN its parsed and returned correctly")
@Test
fun successResponseParsing() = runBlocking {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.Success(false))
val expected = ContentData.contentSuccess
val actual = sut.get()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN successful response WHEN getting content THEN the request is setup properly")
@Test
fun successResponseRequestIsCorrect() = 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())
}
@DisplayName("GIVEN response with missing Field WHEN getting content THEN invalid is ignored others are returned")
@Test
fun dataMissingFieldIsIgnored() = runBlocking {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.SuccessWithMissingFields(false))
val expected = ContentData.contentSuccessWithMissingFields
val actual = sut.get()
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN error response WHEN getting content THEN network request is thrown")
@Test
fun errorResponseResultsInNetworkException() {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.Error(false))
Assertions.assertThrows(NetworkException::class.java) {
runBlocking { sut.get() }
}
}
@DisplayName("GIVEN unexpected json response WHEN getting content THEN parsing request is thrown")
@Test
fun unexpectedJSONResultsInParsingException() {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.UnexpectedJsonAsSuccessResponse(false))
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.get() }
}
}
@DisplayName("GIVEN malformed json response WHEN getting content THEN parsing request is thrown")
@Test
fun malformedJSONResultsInParsingException() {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.MalformedJsonAsSuccessResponse(false))
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.get() }
}
}
}

View file

@ -1,110 +0,0 @@
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()
}
}