initial commit
This commit is contained in:
parent
85ef73b2ba
commit
90a9426b7d
221 changed files with 7611 additions and 0 deletions
|
|
@ -0,0 +1,35 @@
|
|||
package org.fnives.test.showcase.network.auth
|
||||
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import org.fnives.test.showcase.network.auth.model.LoginResponse
|
||||
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.network.shared.ExceptionWrapper
|
||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
internal class LoginErrorConverter {
|
||||
|
||||
@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.network.auth
|
||||
|
||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||
|
||||
interface LoginRemoteSource {
|
||||
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
suspend fun login(credentials: LoginCredentials): LoginStatusResponses
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package org.fnives.test.showcase.network.auth
|
||||
|
||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import org.fnives.test.showcase.network.auth.model.CredentialsRequest
|
||||
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.network.shared.ExceptionWrapper
|
||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||
|
||||
internal class LoginRemoteSourceImpl 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.network.auth
|
||||
|
||||
import org.fnives.test.showcase.network.auth.model.CredentialsRequest
|
||||
import org.fnives.test.showcase.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.network.auth.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class CredentialsRequest(
|
||||
@Json(name = "username")
|
||||
val user: String,
|
||||
@Json(name = "password")
|
||||
val password: String
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.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.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,12 @@
|
|||
package org.fnives.test.showcase.network.content
|
||||
|
||||
import org.fnives.test.showcase.model.content.Content
|
||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
interface ContentRemoteSource {
|
||||
|
||||
@Throws(NetworkException::class, ParsingException::class)
|
||||
suspend fun get(): List<Content>
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package org.fnives.test.showcase.network.content
|
||||
|
||||
import org.fnives.test.showcase.model.content.Content
|
||||
import org.fnives.test.showcase.model.content.ContentId
|
||||
import org.fnives.test.showcase.model.content.ImageUrl
|
||||
import org.fnives.test.showcase.network.shared.ExceptionWrapper
|
||||
|
||||
internal class ContentRemoteSourceImpl(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.network.content
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class ContentResponse(
|
||||
@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.network.content
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface ContentService {
|
||||
|
||||
@GET("content")
|
||||
suspend fun getContent(): List<ContentResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.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,87 @@
|
|||
package org.fnives.test.showcase.network.di
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import org.fnives.test.showcase.model.network.BaseUrl
|
||||
import org.fnives.test.showcase.network.auth.LoginErrorConverter
|
||||
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.session.AuthenticationHeaderInterceptor
|
||||
import org.fnives.test.showcase.network.session.AuthenticationHeaderUtils
|
||||
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
|
||||
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
|
||||
import org.fnives.test.showcase.network.session.SessionAuthenticator
|
||||
import org.fnives.test.showcase.network.shared.PlatformInterceptor
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.StringQualifier
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
fun createNetworkModules(
|
||||
baseUrl: BaseUrl,
|
||||
enableLogging: Boolean,
|
||||
networkSessionLocalStorageProvider: Scope.() -> NetworkSessionLocalStorage,
|
||||
networkSessionExpirationListenerProvider: Scope.() -> NetworkSessionExpirationListener
|
||||
): Sequence<Module> =
|
||||
sequenceOf(
|
||||
loginModule(),
|
||||
contentModule(),
|
||||
sessionlessNetworkingModule(baseUrl, enableLogging),
|
||||
sessionNetworkingModule(networkSessionLocalStorageProvider, networkSessionExpirationListenerProvider)
|
||||
)
|
||||
|
||||
private fun loginModule() = module {
|
||||
factory { LoginRemoteSourceImpl(get(), get()) }
|
||||
factory<LoginRemoteSource> { get<LoginRemoteSourceImpl>() }
|
||||
factory { LoginErrorConverter() }
|
||||
factory { get<Retrofit>(sessionless).create(LoginService::class.java) }
|
||||
}
|
||||
|
||||
private fun contentModule() = module {
|
||||
factory { get<Retrofit>(session).create(ContentService::class.java) }
|
||||
factory { ContentRemoteSourceImpl(get()) }
|
||||
factory<ContentRemoteSource> { get<ContentRemoteSourceImpl>() }
|
||||
}
|
||||
|
||||
private fun sessionlessNetworkingModule(baseUrl: BaseUrl, enableLogging: Boolean) = module {
|
||||
factory { MoshiConverterFactory.create() }
|
||||
single(qualifier = sessionless, override = true) {
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(PlatformInterceptor())
|
||||
.setupLogging(enableLogging)
|
||||
.build()
|
||||
}
|
||||
single(qualifier = sessionless) {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(baseUrl.baseUrl)
|
||||
.addConverterFactory(get<MoshiConverterFactory>())
|
||||
.client(get(sessionless))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sessionNetworkingModule(
|
||||
networkSessionLocalStorageProvider: Scope.() -> NetworkSessionLocalStorage,
|
||||
networkSessionExpirationListenerProvider: Scope.() -> NetworkSessionExpirationListener
|
||||
) = module {
|
||||
single { AuthenticationHeaderUtils(get()) }
|
||||
single { networkSessionExpirationListenerProvider() }
|
||||
single { networkSessionLocalStorageProvider() }
|
||||
factory { SessionAuthenticator(get(), get(), get(), get()) }
|
||||
single(qualifier = session) {
|
||||
get<OkHttpClient>(sessionless)
|
||||
.newBuilder()
|
||||
.authenticator(get<SessionAuthenticator>())
|
||||
.addInterceptor(AuthenticationHeaderInterceptor(get()))
|
||||
.build()
|
||||
}
|
||||
single(qualifier = session) { get<Retrofit>(sessionless).newBuilder().client(get(session)).build() }
|
||||
}
|
||||
|
||||
private val session = StringQualifier("SESSION-NETWORKING")
|
||||
private val sessionless = StringQualifier("SESSIONLESS-NETWORKING")
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.fnives.test.showcase.network.session
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
internal class AuthenticationHeaderInterceptor(
|
||||
private val authenticationHeaderUtils: AuthenticationHeaderUtils
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response =
|
||||
chain.proceed(authenticationHeaderUtils.attachToken(chain.request()))
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package org.fnives.test.showcase.network.session
|
||||
|
||||
import okhttp3.Request
|
||||
|
||||
internal class AuthenticationHeaderUtils(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.network.session
|
||||
|
||||
interface NetworkSessionExpirationListener {
|
||||
|
||||
fun onSessionExpired()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package org.fnives.test.showcase.network.session
|
||||
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
|
||||
interface NetworkSessionLocalStorage {
|
||||
|
||||
var session: Session?
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package org.fnives.test.showcase.network.session
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.fnives.test.showcase.network.auth.LoginRemoteSourceImpl
|
||||
|
||||
internal class SessionAuthenticator(
|
||||
private val networkSessionLocalStorage: NetworkSessionLocalStorage,
|
||||
private val loginRemoteSource: LoginRemoteSourceImpl,
|
||||
private val authenticationHeaderUtils: AuthenticationHeaderUtils,
|
||||
private val networkSessionExpirationListener: NetworkSessionExpirationListener
|
||||
) : Authenticator {
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (authenticationHeaderUtils.hasToken(response.request)) {
|
||||
return runBlocking {
|
||||
try {
|
||||
val newSession = loginRemoteSource.refresh(networkSessionLocalStorage.session?.refreshToken.orEmpty())
|
||||
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,27 @@
|
|||
package org.fnives.test.showcase.network.shared
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonEncodingException
|
||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||
import java.io.EOFException
|
||||
|
||||
internal object ExceptionWrapper {
|
||||
|
||||
@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,12 @@
|
|||
package org.fnives.test.showcase.network.shared
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
class PlatformInterceptor : 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.network.shared.exceptions
|
||||
|
||||
class NetworkException(cause: Throwable) : RuntimeException(cause.message, cause)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.fnives.test.showcase.network.shared.exceptions
|
||||
|
||||
class ParsingException(cause: Throwable) : RuntimeException(cause.message, cause)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package org.fnives.test.showcase.network.auth
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import okhttp3.internal.http.RealResponseBody
|
||||
import okio.Buffer
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import org.fnives.test.showcase.network.auth.model.LoginResponse
|
||||
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
|
||||
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
class LoginErrorConverterTest {
|
||||
|
||||
private lateinit var sut: LoginErrorConverter
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
sut = LoginErrorConverter()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_throwing_lambda_WHEN_parsing_login_error_THEN_network_exception_is_thrown() {
|
||||
Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking {
|
||||
sut.invoke { throw IOException() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_jsonException_throwing_lambda_WHEN_parsing_login_error_THEN_network_exception_is_thrown() {
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking {
|
||||
sut.invoke { throw JsonDataException("") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_400_error_response_WHEN_parsing_login_error_THEN_invalid_credentials_is_returned() = runBlockingTest {
|
||||
val expected = LoginStatusResponses.InvalidCredentials
|
||||
|
||||
val actual = sut.invoke {
|
||||
val responseBody = RealResponseBody(null, 0, Buffer())
|
||||
Response.error(400, responseBody)
|
||||
}
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_parsing_login_error_THEN_successful_response_is_returned() = runBlockingTest {
|
||||
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,105 @@
|
|||
package org.fnives.test.showcase.network.auth
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.model.network.BaseUrl
|
||||
import org.fnives.test.showcase.network.di.createNetworkModules
|
||||
import org.fnives.test.showcase.network.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.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.inject
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
class LoginRemoteSourceRefreshActionImplTest : KoinTest {
|
||||
|
||||
private val sut by inject<LoginRemoteSourceImpl>()
|
||||
private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
|
||||
|
||||
@RegisterExtension
|
||||
@JvmField
|
||||
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
|
||||
private val mockServerScenarioSetup
|
||||
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockNetworkSessionLocalStorage = mock()
|
||||
startKoin {
|
||||
modules(
|
||||
createNetworkModules(
|
||||
baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url),
|
||||
enableLogging = true,
|
||||
networkSessionExpirationListenerProvider = { mock() },
|
||||
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
|
||||
).toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_refresh_request_is_fired_THEN_session() = runBlocking {
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success)
|
||||
val expected = ContentData.refreshSuccessResponse
|
||||
|
||||
val actual = sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_refresh_request_is_fired_THEN_the_request_is_setup_properly() = runBlocking {
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
|
||||
|
||||
sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
|
||||
val request = mockServerScenarioSetup.takeRequest()
|
||||
|
||||
Assertions.assertEquals("PUT", request.method)
|
||||
Assertions.assertEquals("Android", request.getHeader("Platform"))
|
||||
Assertions.assertEquals(null, request.getHeader("Authorization"))
|
||||
Assertions.assertEquals("/login/${ContentData.refreshSuccessResponse.refreshToken}", request.path)
|
||||
Assertions.assertEquals("", request.body.readUtf8())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_internal_error_response_WHEN_refresh_request_is_fired_THEN_network_exception_is_thrown() {
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
|
||||
|
||||
Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking { sut.refresh(ContentData.refreshSuccessResponse.refreshToken) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_invalid_json_response_WHEN_refresh_request_is_fired_THEN_network_exception_is_thrown() {
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.UnexpectedJsonAsSuccessResponse)
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_malformed_json_response_WHEN_refresh_request_is_fired_THEN_network_exception_is_thrown() {
|
||||
mockServerScenarioSetup.setScenario(RefreshTokenScenario.MalformedJson)
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package org.fnives.test.showcase.network.auth
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.model.auth.LoginCredentials
|
||||
import org.fnives.test.showcase.model.network.BaseUrl
|
||||
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
|
||||
import org.fnives.test.showcase.network.di.createNetworkModules
|
||||
import org.fnives.test.showcase.network.mockserver.ContentData
|
||||
import org.fnives.test.showcase.network.mockserver.ContentData.createExpectedLoginRequestJson
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||
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.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.inject
|
||||
import org.mockito.kotlin.mock
|
||||
import org.skyscreamer.jsonassert.JSONAssert
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
class LoginRemoteSourceTest : KoinTest {
|
||||
|
||||
private val sut by inject<LoginRemoteSource>()
|
||||
@RegisterExtension
|
||||
@JvmField
|
||||
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
|
||||
private val mockServerScenarioSetup
|
||||
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
|
||||
private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockNetworkSessionLocalStorage = mock()
|
||||
startKoin {
|
||||
modules(
|
||||
createNetworkModules(
|
||||
baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url),
|
||||
enableLogging = true,
|
||||
networkSessionExpirationListenerProvider = mock(),
|
||||
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
|
||||
).toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_request_is_fired_THEN_login_status_success_is_returned() = runBlocking {
|
||||
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
|
||||
val expected = LoginStatusResponses.Success(ContentData.loginSuccessResponse)
|
||||
|
||||
val actual = sut.login(LoginCredentials("a", "b"))
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_request_is_fired_THEN_the_request_is_setup_properly() = 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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_bad_request_response_WHEN_request_is_fired_THEN_login_status_invalid_credentials_is_returned() = runBlocking {
|
||||
mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials("a", "b"))
|
||||
val expected = LoginStatusResponses.InvalidCredentials
|
||||
|
||||
val actual = sut.login(LoginCredentials("a", "b"))
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_internal_error_response_WHEN_request_is_fired_THEN_network_exception_is_thrown() {
|
||||
mockServerScenarioSetup.setScenario(AuthScenario.GenericError("a", "b"))
|
||||
|
||||
Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials("a", "b")) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_invalid_json_response_WHEN_request_is_fired_THEN_network_exception_is_thrown() {
|
||||
mockServerScenarioSetup.setScenario(AuthScenario.UnexpectedJsonAsSuccessResponse("a", "b"))
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials("a", "b")) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_malformed_json_response_WHEN_request_is_fired_THEN_network_exception_is_thrown() {
|
||||
mockServerScenarioSetup.setScenario(AuthScenario.MalformedJsonAsSuccessResponse("a", "b"))
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.login(LoginCredentials("a", "b")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package org.fnives.test.showcase.network.content
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.model.network.BaseUrl
|
||||
import org.fnives.test.showcase.network.di.createNetworkModules
|
||||
import org.fnives.test.showcase.network.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.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
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
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
class ContentRemoteSourceImplTest : KoinTest {
|
||||
|
||||
private val sut: ContentRemoteSourceImpl by inject()
|
||||
|
||||
@RegisterExtension
|
||||
@JvmField
|
||||
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
|
||||
private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
|
||||
private val mockServerScenarioSetup
|
||||
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockNetworkSessionLocalStorage = mock()
|
||||
startKoin {
|
||||
modules(
|
||||
createNetworkModules(
|
||||
baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url),
|
||||
enableLogging = true,
|
||||
networkSessionExpirationListenerProvider = { mock() },
|
||||
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
|
||||
).toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_getting_content_THEN_its_parsed_and_returned_correctly() = runBlocking {
|
||||
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Success(false))
|
||||
val expected = ContentData.contentSuccess
|
||||
|
||||
val actual = sut.get()
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_successful_response_WHEN_getting_content_THEN_the_request_is_setup_properly() = runBlocking {
|
||||
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Success(false), false)
|
||||
|
||||
sut.get()
|
||||
val request = mockServerScenarioSetup.takeRequest()
|
||||
|
||||
Assertions.assertEquals("GET", request.method)
|
||||
Assertions.assertEquals("Android", request.getHeader("Platform"))
|
||||
Assertions.assertEquals(ContentData.loginSuccessResponse.accessToken, request.getHeader("Authorization"))
|
||||
Assertions.assertEquals("/content", request.path)
|
||||
Assertions.assertEquals("", request.body.readUtf8())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_response_with_missing_Field_WHEN_getting_content_THEN_invalid_is_ignored_others_are_returned() = runBlocking {
|
||||
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.SuccessWithMissingFields(false))
|
||||
|
||||
val expected = ContentData.contentSuccessWithMissingFields
|
||||
|
||||
val actual = sut.get()
|
||||
|
||||
Assertions.assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_error_response_WHEN_getting_content_THEN_network_request_is_thrown() {
|
||||
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.Error(false))
|
||||
|
||||
Assertions.assertThrows(NetworkException::class.java) {
|
||||
runBlocking { sut.get() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_unexpected_json_response_WHEN_getting_content_THEN_parsing_request_is_thrown() {
|
||||
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.UnexpectedJsonAsSuccessResponse(false))
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.get() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_malformed_json_response_WHEN_getting_content_THEN_parsing_request_is_thrown() {
|
||||
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
|
||||
mockServerScenarioSetup.setScenario(ContentScenario.MalformedJsonAsSuccessResponse(false))
|
||||
|
||||
Assertions.assertThrows(ParsingException::class.java) {
|
||||
runBlocking { sut.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package org.fnives.test.showcase.network.content
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.fnives.test.showcase.model.network.BaseUrl
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import org.fnives.test.showcase.network.di.createNetworkModules
|
||||
import org.fnives.test.showcase.network.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.AfterEach
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.koin.test.KoinTest
|
||||
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
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
class SessionExpirationTest : KoinTest {
|
||||
|
||||
private val sut: ContentRemoteSourceImpl by inject()
|
||||
|
||||
@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()
|
||||
startKoin {
|
||||
modules(
|
||||
createNetworkModules(
|
||||
baseUrl = BaseUrl(mockServerScenarioSetupExtensions.url),
|
||||
enableLogging = true,
|
||||
networkSessionExpirationListenerProvider = { mockNetworkSessionExpirationListener },
|
||||
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
|
||||
).toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_401_THEN_refresh_token_ok_response_WHEN_content_requested_THE_tokens_are_refreshed_and_request_retried_with_new_tokens() =
|
||||
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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_401_THEN_failing_refresh_WHEN_content_requested_THE_error_is_returned_and_callback_is_Called() = 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package org.fnives.test.showcase.network.shared
|
||||
|
||||
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 {
|
||||
|
||||
val url: String = "${MockServerScenarioSetup.HTTP_BASE_URL}:${MockServerScenarioSetup.PORT}/"
|
||||
lateinit var mockServerScenarioSetup: MockServerScenarioSetup
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
mockServerScenarioSetup = MockServerScenarioSetup()
|
||||
mockServerScenarioSetup.start(false)
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
mockServerScenarioSetup.stop()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue