Issue#41 Copy full example into separate module with Hilt Integration
This commit is contained in:
parent
69e76dc0da
commit
52a99a82fc
229 changed files with 8416 additions and 11 deletions
1
hilt/hilt-network/.gitignore
vendored
Normal file
1
hilt/hilt-network/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
36
hilt/hilt-network/build.gradle
Normal file
36
hilt/hilt-network/build.gradle
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
plugins {
|
||||
id 'java-library'
|
||||
id 'kotlin'
|
||||
id 'kotlin-kapt'
|
||||
id 'java-test-fixtures'
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
|
||||
implementation "com.squareup.moshi:moshi:$moshi_version"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||
|
||||
api project(":model")
|
||||
|
||||
applyNetworkTestDependenciesTo(this)
|
||||
|
||||
// hilt
|
||||
implementation "com.google.dagger:hilt-core:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
|
||||
kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
|
||||
|
||||
testFixturesApi project(':mockserver')
|
||||
testFixturesApi "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
testFixturesImplementation "com.google.dagger:hilt-core:$hilt_version"
|
||||
testFixturesApi "org.junit.jupiter:junit-jupiter-engine:$junit5_version"
|
||||
}
|
||||
0
hilt/hilt-network/consumer-rules.pro
Normal file
0
hilt/hilt-network/consumer-rules.pro
Normal file
21
hilt/hilt-network/proguard-rules.pro
vendored
Normal file
21
hilt/hilt-network/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginResponse
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.hilt.network.shared.ExceptionWrapper
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class LoginErrorConverter @Inject internal constructor() {
|
||||
|
||||
@Throws(ParsingException::class)
|
||||
suspend fun invoke(request: suspend () -> Response<LoginResponse>): LoginStatusResponses =
|
||||
ExceptionWrapper.wrap {
|
||||
val response = request()
|
||||
if (response.code() == 400) {
|
||||
return@wrap LoginStatusResponses.InvalidCredentials
|
||||
} else if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
val parsedResponse = try {
|
||||
response.body()!!
|
||||
} catch (nullPointerException: NullPointerException) {
|
||||
throw ParsingException(nullPointerException)
|
||||
}
|
||||
|
||||
val session = Session(
|
||||
accessToken = parsedResponse.accessToken,
|
||||
refreshToken = parsedResponse.refreshToken
|
||||
)
|
||||
LoginStatusResponses.Success(session)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||
|
||||
interface LoginRemoteSource {
|
||||
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
suspend fun login(credentials: LoginCredentials): LoginStatusResponses
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.CredentialsRequest
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.hilt.network.shared.ExceptionWrapper
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class LoginRemoteSourceImpl @Inject internal constructor(
|
||||
private val loginService: LoginService,
|
||||
private val loginErrorConverter: LoginErrorConverter
|
||||
) : LoginRemoteSource {
|
||||
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
override suspend fun login(credentials: LoginCredentials): LoginStatusResponses =
|
||||
loginErrorConverter.invoke {
|
||||
loginService.login(CredentialsRequest(user = credentials.username, password = credentials.password))
|
||||
}
|
||||
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
internal suspend fun refresh(refreshToken: String): Session = ExceptionWrapper.wrap {
|
||||
val response = loginService.refreshToken(refreshToken)
|
||||
Session(accessToken = response.accessToken, refreshToken = response.refreshToken)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.CredentialsRequest
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
internal interface LoginService {
|
||||
|
||||
@POST("login")
|
||||
suspend fun login(@Body credentials: CredentialsRequest): Response<LoginResponse>
|
||||
|
||||
@PUT("login/{token}")
|
||||
suspend fun refreshToken(@Path("token") token: String): LoginResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal class CredentialsRequest(
|
||||
@Json(name = "username")
|
||||
val user: String,
|
||||
@Json(name = "password")
|
||||
val password: String
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class LoginResponse(
|
||||
@Json(name = "accessToken")
|
||||
val accessToken: String,
|
||||
@Json(name = "refreshToken")
|
||||
val refreshToken: String
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth.model
|
||||
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
|
||||
sealed class LoginStatusResponses {
|
||||
data class Success(val session: Session) : LoginStatusResponses()
|
||||
object InvalidCredentials : LoginStatusResponses()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.fnives.test.showcase.hilt.network.content
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.model.content.Content
|
||||
|
||||
interface ContentRemoteSource {
|
||||
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
suspend fun get(): List<Content>
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.fnives.test.showcase.hilt.network.content
|
||||
|
||||
import org.fnives.test.showcase.hilt.network.shared.ExceptionWrapper
|
||||
import org.fnives.test.showcase.model.content.Content
|
||||
import org.fnives.test.showcase.model.content.ContentId
|
||||
import org.fnives.test.showcase.model.content.ImageUrl
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ContentRemoteSourceImpl @Inject internal constructor(
|
||||
private val contentService: ContentService
|
||||
) : ContentRemoteSource {
|
||||
|
||||
override suspend fun get(): List<Content> =
|
||||
ExceptionWrapper.wrap {
|
||||
contentService.getContent().mapNotNull(::mapResponse)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun mapResponse(response: ContentResponse): Content? {
|
||||
return Content(
|
||||
id = response.id?.let(::ContentId) ?: return null,
|
||||
title = response.title ?: return null,
|
||||
description = response.description ?: return null,
|
||||
imageUrl = ImageUrl(response.imageUrl ?: return null)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.fnives.test.showcase.hilt.network.content
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class ContentResponse internal constructor(
|
||||
@Json(name = "id")
|
||||
val id: String?,
|
||||
@Json(name = "title")
|
||||
val title: String?,
|
||||
@Json(name = "image")
|
||||
val imageUrl: String?,
|
||||
@Json(name = "says")
|
||||
val description: String?
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.fnives.test.showcase.hilt.network.content
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface ContentService {
|
||||
|
||||
@GET("content")
|
||||
suspend fun getContent(): List<ContentResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.fnives.test.showcase.hilt.network.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
abstract class BindsBaseOkHttpClient {
|
||||
|
||||
@Binds
|
||||
@SessionLessQualifier
|
||||
abstract fun bindsSessionLess(okHttpClient: OkHttpClient): OkHttpClient
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package org.fnives.test.showcase.hilt.network.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSource
|
||||
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSourceImpl
|
||||
import org.fnives.test.showcase.hilt.network.auth.LoginService
|
||||
import org.fnives.test.showcase.hilt.network.content.ContentRemoteSource
|
||||
import org.fnives.test.showcase.hilt.network.content.ContentRemoteSourceImpl
|
||||
import org.fnives.test.showcase.hilt.network.content.ContentService
|
||||
import org.fnives.test.showcase.hilt.network.session.AuthenticationHeaderInterceptor
|
||||
import org.fnives.test.showcase.hilt.network.session.SessionAuthenticator
|
||||
import org.fnives.test.showcase.hilt.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, platformInterceptor: PlatformInterceptor) =
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(platformInterceptor)
|
||||
.setupLogging(enableLogging)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@SessionLessQualifier
|
||||
fun provideSessionLessRetrofit(
|
||||
baseUrl: String,
|
||||
converterFactory: Converter.Factory,
|
||||
@SessionLessQualifier okHttpClient: OkHttpClient,
|
||||
): Retrofit =
|
||||
Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(converterFactory)
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@SessionQualifier
|
||||
internal fun provideSessionOkHttpClient(
|
||||
@SessionLessQualifier okHttpClient: OkHttpClient,
|
||||
sessionAuthenticator: SessionAuthenticator,
|
||||
authenticationHeaderInterceptor: AuthenticationHeaderInterceptor,
|
||||
) =
|
||||
okHttpClient
|
||||
.newBuilder()
|
||||
.authenticator(sessionAuthenticator)
|
||||
.addInterceptor(authenticationHeaderInterceptor)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@SessionQualifier
|
||||
fun provideSessionRetrofit(@SessionLessQualifier retrofit: Retrofit, @SessionQualifier okHttpClient: OkHttpClient): Retrofit =
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.hilt.network.di
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
internal fun OkHttpClient.Builder.setupLogging(enable: Boolean) = run {
|
||||
if (enable) {
|
||||
addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY })
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.fnives.test.showcase.hilt.network.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class SessionLessQualifier
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.fnives.test.showcase.hilt.network.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class SessionQualifier
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.fnives.test.showcase.hilt.network.session
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AuthenticationHeaderInterceptor @Inject internal constructor(
|
||||
private val authenticationHeaderUtils: AuthenticationHeaderUtils
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response =
|
||||
chain.proceed(authenticationHeaderUtils.attachToken(chain.request()))
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package org.fnives.test.showcase.hilt.network.session
|
||||
|
||||
import okhttp3.Request
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AuthenticationHeaderUtils @Inject internal constructor(
|
||||
private val networkSessionLocalStorage: NetworkSessionLocalStorage
|
||||
) {
|
||||
|
||||
fun hasToken(okhttpRequest: Request): Boolean =
|
||||
okhttpRequest.header(KEY) == networkSessionLocalStorage.session?.accessToken
|
||||
|
||||
fun attachToken(okhttpRequest: Request): Request =
|
||||
okhttpRequest.newBuilder()
|
||||
.header(KEY, networkSessionLocalStorage.session?.accessToken.orEmpty()).build()
|
||||
|
||||
companion object {
|
||||
private const val KEY = "Authorization"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.fnives.test.showcase.hilt.network.session
|
||||
|
||||
interface NetworkSessionExpirationListener {
|
||||
|
||||
fun onSessionExpired()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.fnives.test.showcase.hilt.network.session
|
||||
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
|
||||
interface NetworkSessionLocalStorage {
|
||||
|
||||
var session: Session?
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package org.fnives.test.showcase.hilt.network.session
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSourceImpl
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SessionAuthenticator @Inject internal constructor(
|
||||
private val networkSessionLocalStorage: NetworkSessionLocalStorage,
|
||||
private val loginRemoteSource: LoginRemoteSourceImpl,
|
||||
private val authenticationHeaderUtils: AuthenticationHeaderUtils,
|
||||
private val networkSessionExpirationListener: NetworkSessionExpirationListener
|
||||
) : Authenticator {
|
||||
|
||||
@Suppress("SwallowedException")
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (authenticationHeaderUtils.hasToken(response.request)) {
|
||||
return runBlocking {
|
||||
try {
|
||||
val refreshToken = networkSessionLocalStorage.session
|
||||
?.refreshToken
|
||||
.orEmpty()
|
||||
val newSession = loginRemoteSource.refresh(refreshToken)
|
||||
networkSessionLocalStorage.session = newSession
|
||||
return@runBlocking authenticationHeaderUtils.attachToken(response.request)
|
||||
} catch (throwable: Throwable) {
|
||||
networkSessionLocalStorage.session = null
|
||||
networkSessionExpirationListener.onSessionExpired()
|
||||
return@runBlocking null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return authenticationHeaderUtils.attachToken(response.request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package org.fnives.test.showcase.hilt.network.shared
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonEncodingException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import java.io.EOFException
|
||||
|
||||
internal object ExceptionWrapper {
|
||||
|
||||
@Suppress("RethrowCaughtException")
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
suspend fun <T> wrap(request: suspend () -> T) = try {
|
||||
request()
|
||||
} catch (jsonDataException: JsonDataException) {
|
||||
throw ParsingException(jsonDataException)
|
||||
} catch (jsonEncodingException: JsonEncodingException) {
|
||||
throw ParsingException(jsonEncodingException)
|
||||
} catch (eofException: EOFException) {
|
||||
throw ParsingException(eofException)
|
||||
} catch (parsingException: ParsingException) {
|
||||
throw parsingException
|
||||
} catch (networkException: NetworkException) {
|
||||
throw networkException
|
||||
} catch (throwable: Throwable) {
|
||||
throw NetworkException(throwable)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package org.fnives.test.showcase.hilt.network.shared
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class PlatformInterceptor @Inject internal constructor() : Interceptor {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response =
|
||||
chain.proceed(chain.request().newBuilder().header("Platform", "Android").build())
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.fnives.test.showcase.hilt.network.shared.exceptions
|
||||
|
||||
class NetworkException(cause: Throwable) : RuntimeException(cause.message, cause)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.fnives.test.showcase.hilt.network.shared.exceptions
|
||||
|
||||
class ParsingException(cause: Throwable) : RuntimeException(cause.message, cause)
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.internal.http.RealResponseBody
|
||||
import okio.Buffer
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginResponse
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
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 retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class LoginErrorConverterTest {
|
||||
|
||||
private lateinit var sut: LoginErrorConverter
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
sut = LoginErrorConverter()
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN throwing lambda WHEN parsing login error THEN network exception is thrown")
|
||||
@Test
|
||||
fun generallyThrowingLambdaResultsInNetworkException() {
|
||||
Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking {
|
||||
sut.invoke { throw IOException() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN jsonException throwing lambda WHEN parsing login error THEN network exception is thrown")
|
||||
@Test
|
||||
fun jsonDataThrowingLambdaResultsInParsingException() {
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking {
|
||||
sut.invoke { throw JsonDataException("") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN 400 error response WHEN parsing login error THEN invalid credentials is returned")
|
||||
@Test
|
||||
fun code400ResponseResultsInInvalidCredentials() = runTest {
|
||||
val expected = LoginStatusResponses.InvalidCredentials
|
||||
|
||||
val actual = sut.invoke {
|
||||
val responseBody = RealResponseBody(null, 0, Buffer())
|
||||
Response.error(400, responseBody)
|
||||
}
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN successful response WHEN parsing login error THEN successful response is returned")
|
||||
@Test
|
||||
fun successResponseResultsInSessionResponse() = runTest {
|
||||
val loginResponse = LoginResponse("a", "r")
|
||||
val expectedSession = Session(accessToken = loginResponse.accessToken, refreshToken = loginResponse.refreshToken)
|
||||
val expected = LoginStatusResponses.Success(expectedSession)
|
||||
|
||||
val actual = sut.invoke {
|
||||
Response.success(200, loginResponse)
|
||||
}
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.hilt.network.testutil.DaggerTestNetworkComponent
|
||||
import org.fnives.test.showcase.hilt.network.testutil.MockServerScenarioSetupExtensions
|
||||
import org.fnives.test.showcase.network.mockserver.ContentData
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
|
||||
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.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, validateArguments = false)
|
||||
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, validateArguments = 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, validateArguments = false)
|
||||
|
||||
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, validateArguments = false)
|
||||
|
||||
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, validateArguments = false)
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package org.fnives.test.showcase.hilt.network.auth
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.EOFException
|
||||
import org.fnives.test.showcase.hilt.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.hilt.network.testutil.DaggerTestNetworkComponent
|
||||
import org.fnives.test.showcase.hilt.network.testutil.MockServerScenarioSetupExtensions
|
||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||
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.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 retrofit2.HttpException
|
||||
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(username = "a", password = "b"), validateArguments = false)
|
||||
val expected = LoginStatusResponses.Success(ContentData.loginSuccessResponse)
|
||||
|
||||
val actual = sut.login(LoginCredentials(username = "a", password = "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(username = "a", password = "b"), validateArguments = false)
|
||||
|
||||
sut.login(LoginCredentials(username = "a", password = "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(username = "a", password = "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(username = "a", password = "b"), validateArguments = false)
|
||||
val expected = LoginStatusResponses.InvalidCredentials
|
||||
|
||||
val actual = sut.login(LoginCredentials(username = "a", password = "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(username = "a", password = "b"), validateArguments = false)
|
||||
|
||||
val actual = Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials(username = "a", password = "b")) }
|
||||
}
|
||||
|
||||
Assertions.assertEquals("HTTP 500 Server Error", actual.message)
|
||||
Assertions.assertTrue(actual.cause is HttpException)
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN invalid json response WHEN request is fired THEN network exception is thrown")
|
||||
@Test
|
||||
fun invalidJsonMeansParsingException() {
|
||||
val response = AuthScenario.UnexpectedJsonAsSuccessResponse(username = "a", password = "b")
|
||||
mockServerScenarioSetup.setScenario(response, validateArguments = false)
|
||||
|
||||
val actual = Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials(username = "a", password = "b")) }
|
||||
}
|
||||
|
||||
Assertions.assertEquals("Expected BEGIN_OBJECT but was BEGIN_ARRAY at path \$", actual.message)
|
||||
Assertions.assertTrue(actual.cause is JsonDataException)
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN json response with missing field WHEN request is fired THEN network exception is thrown")
|
||||
@Test
|
||||
fun missingFieldJsonMeansParsingException() {
|
||||
val response = AuthScenario.MissingFieldJson(username = "a", password = "b")
|
||||
mockServerScenarioSetup.setScenario(response, validateArguments = false)
|
||||
|
||||
val actual = Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials(username = "a", password = "b")) }
|
||||
}
|
||||
|
||||
Assertions.assertEquals("Required value 'accessToken' missing at \$", actual.message)
|
||||
Assertions.assertTrue(actual.cause is JsonDataException)
|
||||
}
|
||||
|
||||
@DisplayName("GIVEN malformed json response WHEN request is fired THEN network exception is thrown")
|
||||
@Test
|
||||
fun malformedJsonMeansParsingException() {
|
||||
val response = AuthScenario.MalformedJsonAsSuccessResponse(username = "a", "b")
|
||||
mockServerScenarioSetup.setScenario(response, validateArguments = false)
|
||||
|
||||
val actual = Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials(username = "a", "b")) }
|
||||
}
|
||||
|
||||
Assertions.assertEquals("End of input", actual.message)
|
||||
Assertions.assertTrue(actual.cause is EOFException)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package org.fnives.test.showcase.hilt.network.content
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.ParsingException
|
||||
import org.fnives.test.showcase.hilt.network.testutil.DaggerTestNetworkComponent
|
||||
import org.fnives.test.showcase.hilt.network.testutil.MockServerScenarioSetupExtensions
|
||||
import org.fnives.test.showcase.network.mockserver.ContentData
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
|
||||
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.KoinTest
|
||||
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 : KoinTest {
|
||||
|
||||
@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(usingRefreshedToken = false), validateArguments = 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(usingRefreshedToken = false), validateArguments = 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)
|
||||
val response = ContentScenario.SuccessWithMissingFields(usingRefreshedToken = false)
|
||||
mockServerScenarioSetup.setScenario(response, validateArguments = 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(usingRefreshedToken = false), validateArguments = 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)
|
||||
val response = ContentScenario.UnexpectedJsonAsSuccessResponse(usingRefreshedToken = false)
|
||||
mockServerScenarioSetup.setScenario(response, validateArguments = 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)
|
||||
val response = ContentScenario.MalformedJsonAsSuccessResponse(usingRefreshedToken = false)
|
||||
mockServerScenarioSetup.setScenario(response, validateArguments = false)
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package org.fnives.test.showcase.hilt.network.content
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionExpirationListener
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
|
||||
import org.fnives.test.showcase.hilt.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.hilt.network.testutil.DaggerTestNetworkComponent
|
||||
import org.fnives.test.showcase.hilt.network.testutil.MockServerScenarioSetupExtensions
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
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.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.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.verifyNoInteractions
|
||||
import org.mockito.kotlin.verifyNoMoreInteractions
|
||||
import org.mockito.kotlin.whenever
|
||||
import retrofit2.HttpException
|
||||
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(usingRefreshedToken = false)
|
||||
.then(ContentScenario.Success(usingRefreshedToken = true)),
|
||||
validateArguments = false
|
||||
)
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, validateArguments = 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")
|
||||
)
|
||||
verifyNoInteractions(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(usingRefreshedToken = false), validateArguments = false)
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error, validateArguments = false)
|
||||
|
||||
val actual = Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking { sut.get() }
|
||||
}
|
||||
|
||||
Assertions.assertEquals("HTTP 401 Client Error", actual.message)
|
||||
Assertions.assertTrue(actual.cause is HttpException)
|
||||
verify(mockNetworkSessionLocalStorage, times(3)).session
|
||||
verify(mockNetworkSessionLocalStorage, times(1)).session = null
|
||||
verifyNoMoreInteractions(mockNetworkSessionLocalStorage)
|
||||
verify(mockNetworkSessionExpirationListener, times(1)).onSessionExpired()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package org.fnives.test.showcase.hilt.network.testutil
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSourceRefreshActionImplTest
|
||||
import org.fnives.test.showcase.hilt.network.auth.LoginRemoteSourceTest
|
||||
import org.fnives.test.showcase.hilt.network.content.ContentRemoteSourceImplTest
|
||||
import org.fnives.test.showcase.hilt.network.content.SessionExpirationTest
|
||||
import org.fnives.test.showcase.hilt.network.di.BindsBaseOkHttpClient
|
||||
import org.fnives.test.showcase.hilt.network.di.HiltNetworkModule
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionExpirationListener
|
||||
import org.fnives.test.showcase.hilt.network.session.NetworkSessionLocalStorage
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [HiltNetworkModule::class, BindsBaseOkHttpClient::class])
|
||||
interface TestNetworkComponent {
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
fun setBaseUrl(baseUrl: String): Builder
|
||||
|
||||
@BindsInstance
|
||||
fun setEnableLogging(enableLogging: Boolean): Builder
|
||||
|
||||
@BindsInstance
|
||||
fun setNetworkSessionLocalStorage(storage: NetworkSessionLocalStorage): Builder
|
||||
|
||||
@BindsInstance
|
||||
fun setNetworkSessionExpirationListener(listener: NetworkSessionExpirationListener): Builder
|
||||
|
||||
fun build(): TestNetworkComponent
|
||||
}
|
||||
|
||||
fun inject(contentRemoteSourceImplTest: ContentRemoteSourceImplTest)
|
||||
|
||||
fun inject(sessionExpirationTest: SessionExpirationTest)
|
||||
|
||||
fun inject(loginRemoteSourceRefreshActionImplTest: LoginRemoteSourceRefreshActionImplTest)
|
||||
|
||||
fun inject(loginRemoteSourceTest: LoginRemoteSourceTest)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"accessToken": "login-access",
|
||||
"refreshToken": "login-refresh"
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package org.fnives.test.showcase.hilt.network.testutil
|
||||
|
||||
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
class MockServerScenarioSetupExtensions : BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
lateinit var url: String
|
||||
lateinit var mockServerScenarioSetup: MockServerScenarioSetup
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
mockServerScenarioSetup = MockServerScenarioSetup()
|
||||
url = mockServerScenarioSetup.start(false)
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
mockServerScenarioSetup.stop()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue