Merge pull request #43 from fknives/issue#30-adjust-mockwebserver-readability

Issue#30 adjust mockwebserver readability
This commit is contained in:
Gergely Hegedis 2022-01-24 20:59:52 +02:00 committed by GitHub
commit 2f26467558
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 399 additions and 90 deletions

View file

@ -17,12 +17,7 @@ object SetupAuthenticationState : KoinTest {
mainDispatcherTestRule: MainDispatcherTestRule,
mockServerScenarioSetup: MockServerScenarioSetup
) {
mockServerScenarioSetup.setScenario(
AuthScenario.Success(
username = "a",
password = "b"
)
)
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"))
val activityScenario = ActivityScenario.launch(AuthActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
val loginRobot = LoginRobot()

View file

@ -47,6 +47,8 @@ class MainActivityTest : KoinTest {
@JvmField
val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
@Rule
@JvmField
val mainDispatcherTestRule = SpecificTestConfigurationsFactory.createMainDispatcherTestRule()
@ -60,12 +62,12 @@ class MainActivityTest : KoinTest {
@Before
fun setUp() {
SpecificTestConfigurationsFactory.createServerTypeConfiguration()
.invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
.invoke(mockServerScenarioSetup)
disposable = NetworkSynchronization.registerNetworkingSynchronization()
homeRobot.setupLogin(
mainDispatcherTestRule,
mockServerScenarioSetupTestRule.mockServerScenarioSetup
mockServerScenarioSetup
)
}
@ -78,8 +80,7 @@ class MainActivityTest : KoinTest {
/** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */
@Test
fun signOutClickedResultsInNavigation() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Error(false))
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -92,8 +93,7 @@ class MainActivityTest : KoinTest {
/** GIVEN success response WHEN data is returned THEN it is shown on the ui */
@Test
fun successfulDataLoadingShowsTheElementsOnTheUI() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -106,8 +106,7 @@ class MainActivityTest : KoinTest {
/** GIVEN success response WHEN item is clicked THEN ui is updated */
@Test
fun clickingOnListElementUpdatesTheElementsFavouriteState() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -122,8 +121,7 @@ class MainActivityTest : KoinTest {
/** 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))
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -143,8 +141,7 @@ class MainActivityTest : KoinTest {
/** GIVEN success response WHEN item is clicked then clicked again THEN ui is updated */
@Test
fun clickingAnElementMultipleTimesProperlyUpdatesIt() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Success(false))
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -161,8 +158,7 @@ class MainActivityTest : KoinTest {
/** GIVEN error response WHEN loaded THEN error is Shown */
@Test
fun networkErrorResultsInUIErrorStateShown() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Error(false))
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -174,10 +170,9 @@ class MainActivityTest : KoinTest {
/** GIVEN error response then success WHEN retried THEN success is shown */
@Test
fun retryingFromErrorStateAndSucceedingShowsTheData() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Error(false)
.then(ContentScenario.Success(false))
mockServerScenarioSetup.setScenario(
ContentScenario.Error(usingRefreshedToken = false)
.then(ContentScenario.Success(usingRefreshedToken = false))
)
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -195,10 +190,9 @@ class MainActivityTest : KoinTest {
/** GIVEN success then error WHEN retried THEN error is shown */
@Test
fun errorIsShownIfTheDataIsFetchedAndErrorIsReceived() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Success(false)
.then(ContentScenario.Error(false))
mockServerScenarioSetup.setScenario(
ContentScenario.Success(usingRefreshedToken = false)
.then(ContentScenario.Error(usingRefreshedToken = false))
)
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
@ -218,10 +212,9 @@ class MainActivityTest : KoinTest {
/** GIVEN unauthenticated then success WHEN loaded THEN success is shown */
@Test
fun authenticationIsHandledWithASingleLoading() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(
ContentScenario.Unauthorized(false)
.then(ContentScenario.Success(true))
mockServerScenarioSetup.setScenario(
ContentScenario.Unauthorized(usingRefreshedToken = false)
.then(ContentScenario.Success(usingRefreshedToken = true))
)
.setScenario(RefreshTokenScenario.Success)
@ -237,8 +230,7 @@ class MainActivityTest : KoinTest {
/** GIVEN unauthenticated then error WHEN loaded THEN navigated to auth */
@Test
fun sessionExpirationResultsInNavigation() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup
.setScenario(ContentScenario.Unauthorized(false))
mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(usingRefreshedToken = false))
.setScenario(RefreshTokenScenario.Error)
activityScenario = ActivityScenario.launch(MainActivity::class.java)

View file

@ -42,6 +42,7 @@ class AuthActivityTest : KoinTest {
@Rule
@JvmField
val mockServerScenarioSetupTestRule = MockServerScenarioSetupTestRule()
val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
@Rule
@JvmField
@ -56,7 +57,7 @@ class AuthActivityTest : KoinTest {
@Before
fun setUp() {
SpecificTestConfigurationsFactory.createServerTypeConfiguration()
.invoke(mockServerScenarioSetupTestRule.mockServerScenarioSetup)
.invoke(mockServerScenarioSetup)
disposable = NetworkSynchronization.registerNetworkingSynchronization()
}
@ -69,11 +70,8 @@ class AuthActivityTest : KoinTest {
/** 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"
)
mockServerScenarioSetup.setScenario(
AuthScenario.Success(password = "alma", username = "banan")
)
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
loginRobot
@ -123,7 +121,7 @@ class AuthActivityTest : KoinTest {
/** GIVEN password and username and invalid credentials response WHEN signIn THEN error invalid credentials is shown */
@Test
fun invalidCredentialsGivenShowsProperErrorMessage() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
mockServerScenarioSetup.setScenario(
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
)
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
@ -144,7 +142,7 @@ class AuthActivityTest : KoinTest {
/** GIVEN password and username and error response WHEN signIn THEN error invalid credentials is shown */
@Test
fun networkErrorShowsProperErrorMessage() {
mockServerScenarioSetupTestRule.mockServerScenarioSetup.setScenario(
mockServerScenarioSetup.setScenario(
AuthScenario.GenericError(username = "alma", password = "banan")
)
activityScenario = ActivityScenario.launch(AuthActivity::class.java)

View file

@ -31,6 +31,13 @@ subprojects { module ->
includeAndroidResources = 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.library") {
@ -47,5 +54,12 @@ subprojects { module ->
includeAndroidResources = true
}
}
module.tasks.configureEach { task ->
if (task.taskIdentity.type.toString() == "class org.jetbrains.kotlin.gradle.tasks.KotlinCompile") {
task.kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
}
}
}

View file

@ -38,6 +38,7 @@ internal class ScenarioToRequestScenario {
is AuthScenario.Success -> CreateAuthSuccessResponse()
is AuthScenario.MalformedJsonAsSuccessResponse -> CreateMalformedJsonSuccessResponse()
is AuthScenario.UnexpectedJsonAsSuccessResponse -> CreateGenericSuccessResponseByJson("[]")
is AuthScenario.MissingFieldJson -> CreateGenericSuccessResponseByJson("{}")
}
val requestMatchingChecker = AuthRequestMatchingChecker(authScenario, validateArguments)
return SpecificRequestScenario(requestMatchingChecker, createResponse)

View file

@ -12,4 +12,5 @@ sealed class AuthScenario : GenericScenario<AuthScenario>() {
class GenericError(override val username: String, override val password: String) : AuthScenario()
class UnexpectedJsonAsSuccessResponse(override val username: String, override val password: String) : AuthScenario()
class MalformedJsonAsSuccessResponse(override val username: String, override val password: String) : AuthScenario()
class MissingFieldJson(override val username: String, override val password: String) : AuthScenario()
}

View file

@ -3,5 +3,5 @@ package org.fnives.test.showcase.network.mockserver.scenario.createresponse
import okhttp3.mockwebserver.MockResponse
internal class CreateMalformedJsonSuccessResponse : CreateResponse {
override fun getResponse(): MockResponse = MockResponse().setResponseCode(200).setBody("[")
override fun getResponse(): MockResponse = MockResponse().setResponseCode(200).setBody("{")
}

View file

@ -45,6 +45,11 @@ class CodeKataLoginRemoteSourceTest {
fun invalidJsonMeansParsingException() {
}
@DisplayName("GIVEN json response with missing field WHEN request is fired THEN network exception is thrown")
@Test
fun missingFieldJsonMeansParsingException() {
}
@DisplayName("GIVEN malformed json response WHEN request is fired THEN network exception is thrown")
@Test
fun malformedJsonMeansParsingException() {
@ -66,5 +71,13 @@ class CodeKataLoginRemoteSourceTest {
} while (true)
}
}
internal fun getLoginBodyJson(username: String, password: String): String =
"""
{
"username": "$username",
"password": "$password"
}
""".trimIndent()
}
}

View file

@ -30,8 +30,7 @@ class LoginRemoteSourceRefreshActionImplTest : KoinTest {
@RegisterExtension
@JvmField
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
private val mockServerScenarioSetup
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
@BeforeEach
fun setUp() {
@ -56,7 +55,7 @@ class LoginRemoteSourceRefreshActionImplTest : KoinTest {
@DisplayName("GIVEN successful response WHEN refresh request is fired THEN session is returned")
@Test
fun successResponseResultsInSession() = runBlocking {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, validateArguments = false)
val expected = ContentData.refreshSuccessResponse
val actual = sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
@ -67,7 +66,7 @@ class LoginRemoteSourceRefreshActionImplTest : KoinTest {
@DisplayName("GIVEN successful response WHEN refresh request is fired THEN the request is setup properly")
@Test
fun refreshRequestIsSetupProperly() = runBlocking {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, validateArguments = false)
sut.refresh(ContentData.refreshSuccessResponse.refreshToken)
val request = mockServerScenarioSetup.takeRequest()
@ -82,7 +81,7 @@ class LoginRemoteSourceRefreshActionImplTest : KoinTest {
@DisplayName("GIVEN internal error response WHEN refresh request is fired THEN network exception is thrown")
@Test
fun generalErrorResponseResultsInNetworkException() {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error, validateArguments = false)
Assertions.assertThrows(NetworkException::class.java) {
runBlocking { sut.refresh(ContentData.refreshSuccessResponse.refreshToken) }
@ -92,7 +91,7 @@ class LoginRemoteSourceRefreshActionImplTest : KoinTest {
@DisplayName("GIVEN invalid json response WHEN refresh request is fired THEN network exception is thrown")
@Test
fun jsonErrorResponseResultsInParsingException() {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.UnexpectedJsonAsSuccessResponse)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.UnexpectedJsonAsSuccessResponse, validateArguments = false)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }
@ -102,7 +101,7 @@ class LoginRemoteSourceRefreshActionImplTest : KoinTest {
@DisplayName("GIVEN malformed json response WHEN refresh request is fired THEN parsing exception is thrown")
@Test
fun malformedJsonErrorResponseResultsInParsingException() {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.MalformedJson)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.MalformedJson, validateArguments = false)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.refresh(ContentData.loginSuccessResponse.refreshToken) }

View file

@ -1,6 +1,8 @@
package org.fnives.test.showcase.network.auth
import com.squareup.moshi.JsonDataException
import kotlinx.coroutines.runBlocking
import okio.EOFException
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
@ -25,6 +27,7 @@ import org.koin.test.inject
import org.mockito.kotlin.mock
import org.skyscreamer.jsonassert.JSONAssert
import org.skyscreamer.jsonassert.JSONCompareMode
import retrofit2.HttpException
@Suppress("TestFunctionName")
class LoginRemoteSourceTest : KoinTest {
@ -34,8 +37,7 @@ class LoginRemoteSourceTest : KoinTest {
@RegisterExtension
@JvmField
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
private val mockServerScenarioSetup
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
@BeforeEach
fun setUp() {
@ -60,10 +62,10 @@ class LoginRemoteSourceTest : KoinTest {
@DisplayName("GIVEN successful response WHEN request is fired THEN login status success is returned")
@Test
fun successResponseIsParsedProperly() = runBlocking {
mockServerScenarioSetup.setScenario(AuthScenario.Success("a", "b"))
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"), validateArguments = false)
val expected = LoginStatusResponses.Success(ContentData.loginSuccessResponse)
val actual = sut.login(LoginCredentials("a", "b"))
val actual = sut.login(LoginCredentials(username = "a", password = "b"))
Assertions.assertEquals(expected, actual)
}
@ -71,16 +73,16 @@ class LoginRemoteSourceTest : KoinTest {
@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)
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"), validateArguments = false)
sut.login(LoginCredentials("a", "b"))
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("a", "b")
val loginRequest = createExpectedLoginRequestJson(username = "a", password = "b")
JSONAssert.assertEquals(
loginRequest,
request.body.readUtf8(),
@ -91,10 +93,10 @@ class LoginRemoteSourceTest : KoinTest {
@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"))
mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials(username = "a", password = "b"), validateArguments = false)
val expected = LoginStatusResponses.InvalidCredentials
val actual = sut.login(LoginCredentials("a", "b"))
val actual = sut.login(LoginCredentials(username = "a", password = "b"))
Assertions.assertEquals(expected, actual)
}
@ -102,30 +104,55 @@ class LoginRemoteSourceTest : KoinTest {
@DisplayName("GIVEN internal error response WHEN request is fired THEN network exception is thrown")
@Test
fun genericErrorMeansNetworkError() {
mockServerScenarioSetup.setScenario(AuthScenario.GenericError("a", "b"))
mockServerScenarioSetup.setScenario(AuthScenario.GenericError(username = "a", password = "b"), validateArguments = false)
Assertions.assertThrows(NetworkException::class.java) {
runBlocking { sut.login(LoginCredentials("a", "b")) }
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() {
mockServerScenarioSetup.setScenario(AuthScenario.UnexpectedJsonAsSuccessResponse("a", "b"))
val response = AuthScenario.UnexpectedJsonAsSuccessResponse(username = "a", password = "b")
mockServerScenarioSetup.setScenario(response, validateArguments = false)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.login(LoginCredentials("a", "b")) }
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() {
mockServerScenarioSetup.setScenario(AuthScenario.MalformedJsonAsSuccessResponse("a", "b"))
val response = AuthScenario.MalformedJsonAsSuccessResponse(username = "a", "b")
mockServerScenarioSetup.setScenario(response, validateArguments = false)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.login(LoginCredentials("a", "b")) }
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)
}
}

View file

@ -0,0 +1,155 @@
package org.fnives.test.showcase.network.auth
import com.squareup.moshi.JsonDataException
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.EOFException
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.auth.CodeKataLoginRemoteSourceTest.Companion.getLoginBodyJson
import org.fnives.test.showcase.network.auth.CodeKataLoginRemoteSourceTest.Companion.readResourceFile
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.fnives.test.showcase.network.di.createNetworkModules
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.DisplayName
import org.junit.jupiter.api.Test
import org.koin.core.context.GlobalContext.stopKoin
import org.koin.core.context.startKoin
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
import retrofit2.HttpException
class PlainLoginRemoteSourceTest : KoinTest {
private val sut by inject<LoginRemoteSource>()
private lateinit var mockWebServer: MockWebServer
@BeforeEach
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
startKoin {
modules(
createNetworkModules(
baseUrl = BaseUrl(mockWebServer.url("mockserver/").toString()),
enableLogging = true,
networkSessionExpirationListenerProvider = { mock() },
networkSessionLocalStorageProvider = { mock() }
).toList()
)
}
}
@AfterEach
fun tearDown() {
stopKoin()
mockWebServer.shutdown()
}
@DisplayName("GIVEN successful response WHEN request is fired THEN login status success is returned")
@Test
fun successResponseIsParsedProperly() = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(readResourceFile("success_response_login.json")))
val session = Session(accessToken = "login-access", refreshToken = "login-refresh")
val expected = LoginStatusResponses.Success(session = session)
val actual = sut.login(LoginCredentials(username = "alma", password = "banan"))
Assertions.assertEquals(expected, actual)
}
@DisplayName("GIVEN successful response WHEN request is fired THEN the request is setup properly")
@Test
fun requestProperlySetup() = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(readResourceFile("success_response_login.json")))
sut.login(LoginCredentials(username = "alma", password = "banan"))
val request = mockWebServer.takeRequest()
Assertions.assertEquals("POST", request.method)
Assertions.assertEquals("Android", request.getHeader("Platform"))
Assertions.assertEquals(null, request.getHeader("Authorization"))
Assertions.assertEquals("/mockserver/login", request.path)
val loginRequestBody = getLoginBodyJson(username = "alma", password = "banan")
JSONAssert.assertEquals(
loginRequestBody,
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 {
mockWebServer.enqueue(MockResponse().setResponseCode(400).setBody("{}"))
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() {
mockWebServer.enqueue(MockResponse().setResponseCode(500).setBody("{}"))
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() {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("[]"))
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() {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
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() {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("{"))
val actual = Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.login(LoginCredentials(username = "a", password = "b")) }
}
Assertions.assertEquals("End of input", actual.message)
Assertions.assertTrue(actual.cause is EOFException)
}
}

View file

@ -32,8 +32,7 @@ class ContentRemoteSourceImplTest : KoinTest {
@JvmField
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
private val mockServerScenarioSetup
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
@BeforeEach
fun setUp() {
@ -59,7 +58,7 @@ class ContentRemoteSourceImplTest : KoinTest {
@Test
fun successResponseParsing() = runBlocking {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.Success(false))
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false), validateArguments = false)
val expected = ContentData.contentSuccess
val actual = sut.get()
@ -71,7 +70,7 @@ class ContentRemoteSourceImplTest : KoinTest {
@Test
fun successResponseRequestIsCorrect() = runBlocking {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.Success(false), false)
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false), validateArguments = false)
sut.get()
val request = mockServerScenarioSetup.takeRequest()
@ -87,7 +86,8 @@ class ContentRemoteSourceImplTest : KoinTest {
@Test
fun dataMissingFieldIsIgnored() = runBlocking {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.SuccessWithMissingFields(false))
val response = ContentScenario.SuccessWithMissingFields(usingRefreshedToken = false)
mockServerScenarioSetup.setScenario(response, validateArguments = false)
val expected = ContentData.contentSuccessWithMissingFields
@ -100,7 +100,7 @@ class ContentRemoteSourceImplTest : KoinTest {
@Test
fun errorResponseResultsInNetworkException() {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.Error(false))
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false), validateArguments = false)
Assertions.assertThrows(NetworkException::class.java) {
runBlocking { sut.get() }
@ -111,7 +111,8 @@ class ContentRemoteSourceImplTest : KoinTest {
@Test
fun unexpectedJSONResultsInParsingException() {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.UnexpectedJsonAsSuccessResponse(false))
val response = ContentScenario.UnexpectedJsonAsSuccessResponse(usingRefreshedToken = false)
mockServerScenarioSetup.setScenario(response, validateArguments = false)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.get() }
@ -122,7 +123,8 @@ class ContentRemoteSourceImplTest : KoinTest {
@Test
fun malformedJSONResultsInParsingException() {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.MalformedJsonAsSuccessResponse(false))
val response = ContentScenario.MalformedJsonAsSuccessResponse(usingRefreshedToken = false)
mockServerScenarioSetup.setScenario(response, validateArguments = false)
Assertions.assertThrows(ParsingException::class.java) {
runBlocking { sut.get() }

View file

@ -0,0 +1,111 @@
package org.fnives.test.showcase.network.content
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.auth.CodeKataLoginRemoteSourceTest.Companion.readResourceFile
import org.fnives.test.showcase.network.di.createNetworkModules
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
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.DisplayName
import org.junit.jupiter.api.Test
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
import retrofit2.HttpException
class PlainSessionExpirationTest : KoinTest {
private val sut by inject<ContentRemoteSource>()
private lateinit var mockWebServer: MockWebServer
private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
private lateinit var mockNetworkSessionExpirationListener: NetworkSessionExpirationListener
@BeforeEach
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
mockNetworkSessionLocalStorage = mock()
mockNetworkSessionExpirationListener = mock()
startKoin {
modules(
createNetworkModules(
baseUrl = BaseUrl(mockWebServer.url("mockserver/").toString()),
enableLogging = true,
networkSessionExpirationListenerProvider = { mockNetworkSessionExpirationListener },
networkSessionLocalStorageProvider = { mockNetworkSessionLocalStorage }
).toList()
)
}
}
@AfterEach
fun tearDown() {
stopKoin()
mockWebServer.shutdown()
}
@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 {
mockWebServer.enqueue(MockResponse().setResponseCode(401))
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(readResourceFile("success_response_login.json")))
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("[]"))
var sessionToReturnByMock: Session? = Session(accessToken = "expired-access", refreshToken = "expired-refresh")
whenever(mockNetworkSessionLocalStorage.session).doAnswer { sessionToReturnByMock }
doAnswer { sessionToReturnByMock = it.arguments[0] as Session? }
.whenever(mockNetworkSessionLocalStorage).session = anyOrNull()
sut.get()
mockWebServer.takeRequest()
val refreshRequest = mockWebServer.takeRequest()
val contentRequestAfterRefreshed = mockWebServer.takeRequest()
Assertions.assertEquals("PUT", refreshRequest.method)
Assertions.assertEquals("/mockserver/login/expired-refresh", refreshRequest.path)
Assertions.assertEquals(null, refreshRequest.getHeader("Authorization"))
Assertions.assertEquals("Android", refreshRequest.getHeader("Platform"))
Assertions.assertEquals("", refreshRequest.body.readUtf8())
Assertions.assertEquals("login-access", contentRequestAfterRefreshed.getHeader("Authorization"))
verifyZeroInteractions(mockNetworkSessionExpirationListener)
}
@DisplayName("GIVEN 401 THEN failing refresh WHEN content requested THE error is returned and callback is Called")
@Test
fun failingRefreshResultsInSessionExpiration() = runBlocking {
val currentSession = Session(accessToken = "expired-access", refreshToken = "expired-refresh")
whenever(mockNetworkSessionLocalStorage.session).doReturn(currentSession)
mockWebServer.enqueue(MockResponse().setResponseCode(401))
mockWebServer.enqueue(MockResponse().setResponseCode(400))
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()
}
}

View file

@ -30,6 +30,7 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
import retrofit2.HttpException
@Suppress("TestFunctionName")
class SessionExpirationTest : KoinTest {
@ -39,8 +40,7 @@ class SessionExpirationTest : KoinTest {
@RegisterExtension
@JvmField
val mockServerScenarioSetupExtensions = MockServerScenarioSetupExtensions()
private val mockServerScenarioSetup
get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
private val mockServerScenarioSetup get() = mockServerScenarioSetupExtensions.mockServerScenarioSetup
private lateinit var mockNetworkSessionLocalStorage: NetworkSessionLocalStorage
private lateinit var mockNetworkSessionExpirationListener: NetworkSessionExpirationListener
@ -70,11 +70,11 @@ class SessionExpirationTest : KoinTest {
fun successRefreshResultsInRequestRetry() = runBlocking {
var sessionToReturnByMock: Session? = ContentData.loginSuccessResponse
mockServerScenarioSetup.setScenario(
ContentScenario.Unauthorized(false)
.then(ContentScenario.Success(true)),
false
ContentScenario.Unauthorized(usingRefreshedToken = false)
.then(ContentScenario.Success(usingRefreshedToken = true)),
validateArguments = false
)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, false)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Success, validateArguments = false)
whenever(mockNetworkSessionLocalStorage.session).doAnswer { sessionToReturnByMock }
doAnswer { sessionToReturnByMock = it.arguments[0] as Session? }
.whenever(mockNetworkSessionLocalStorage).session = anyOrNull()
@ -97,8 +97,6 @@ class SessionExpirationTest : KoinTest {
ContentData.refreshSuccessResponse.accessToken,
retryAfterTokenRefreshRequest.getHeader("Authorization")
)
verify(mockNetworkSessionLocalStorage, times(1)).session =
ContentData.refreshSuccessResponse
verifyZeroInteractions(mockNetworkSessionExpirationListener)
}
@ -106,12 +104,15 @@ class SessionExpirationTest : KoinTest {
@Test
fun failingRefreshResultsInSessionExpiration() = runBlocking {
whenever(mockNetworkSessionLocalStorage.session).doReturn(ContentData.loginSuccessResponse)
mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(false))
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(usingRefreshedToken = false), validateArguments = false)
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error, validateArguments = false)
Assertions.assertThrows(NetworkException::class.java) {
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)