diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt index 643be01..74e0d91 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/login/AuthActivityTest.kt @@ -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) diff --git a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/ScenarioToRequestScenario.kt b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/ScenarioToRequestScenario.kt index f699cda..cc13620 100644 --- a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/ScenarioToRequestScenario.kt +++ b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/ScenarioToRequestScenario.kt @@ -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) diff --git a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/auth/AuthScenario.kt b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/auth/AuthScenario.kt index c2418db..ee35194 100644 --- a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/auth/AuthScenario.kt +++ b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/auth/AuthScenario.kt @@ -12,4 +12,5 @@ sealed class AuthScenario : GenericScenario() { 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() } diff --git a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/createresponse/CreateMalformedJsonSuccessResponse.kt b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/createresponse/CreateMalformedJsonSuccessResponse.kt index ec8ea9d..d178922 100644 --- a/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/createresponse/CreateMalformedJsonSuccessResponse.kt +++ b/mockserver/src/main/java/org/fnives/test/showcase/network/mockserver/scenario/createresponse/CreateMalformedJsonSuccessResponse.kt @@ -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("{") } diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/CodeKataLoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/CodeKataLoginRemoteSourceTest.kt index d4ae897..34ae47e 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/CodeKataLoginRemoteSourceTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/CodeKataLoginRemoteSourceTest.kt @@ -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() } } diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt index 90dbcbc..0075cf9 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceTest.kt @@ -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 { @@ -103,9 +106,12 @@ class LoginRemoteSourceTest : KoinTest { fun genericErrorMeansNetworkError() { mockServerScenarioSetup.setScenario(AuthScenario.GenericError(username = "a", password = "b"), validateArguments = false) - Assertions.assertThrows(NetworkException::class.java) { + 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") @@ -114,9 +120,26 @@ class LoginRemoteSourceTest : KoinTest { val response = AuthScenario.UnexpectedJsonAsSuccessResponse(username = "a", password = "b") mockServerScenarioSetup.setScenario(response, validateArguments = false) - Assertions.assertThrows(ParsingException::class.java) { + 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") @@ -125,8 +148,11 @@ class LoginRemoteSourceTest : KoinTest { val response = AuthScenario.MalformedJsonAsSuccessResponse(username = "a", "b") mockServerScenarioSetup.setScenario(response, validateArguments = false) - Assertions.assertThrows(ParsingException::class.java) { + 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) } } diff --git a/network/src/test/java/org/fnives/test/showcase/network/auth/PlainLoginRemoteSourceTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/PlainLoginRemoteSourceTest.kt new file mode 100644 index 0000000..099cf67 --- /dev/null +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/PlainLoginRemoteSourceTest.kt @@ -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() + 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) + } +}