diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt index a2b2a8b..b6a6dd3 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/testutils/statesetup/SetupAuthenticationState.kt @@ -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() diff --git a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt index aa2d19a..8534fb1 100644 --- a/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt +++ b/app/src/sharedTest/java/org/fnives/test/showcase/ui/home/MainActivityTest.kt @@ -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,11 +170,10 @@ 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,11 +190,10 @@ 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,11 +212,10 @@ 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) activityScenario = ActivityScenario.launch(MainActivity::class.java) @@ -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) 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/gradlescripts/testoptions.gradle b/gradlescripts/testoptions.gradle index 42b9166..7193f48 100644 --- a/gradlescripts/testoptions.gradle +++ b/gradlescripts/testoptions.gradle @@ -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" + } + } + } } } \ No newline at end of file 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/LoginRemoteSourceRefreshActionImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt index 90f7cf5..dc982fe 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/auth/LoginRemoteSourceRefreshActionImplTest.kt @@ -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) } 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 6e08ee1..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 { @@ -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) } } 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) + } +} diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt index 352488f..9fe16ff 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/content/ContentRemoteSourceImplTest.kt @@ -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() } diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/PlainSessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/PlainSessionExpirationTest.kt new file mode 100644 index 0000000..3536478 --- /dev/null +++ b/network/src/test/java/org/fnives/test/showcase/network/content/PlainSessionExpirationTest.kt @@ -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() + 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() + } +} diff --git a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt b/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt index f521175..c4ee725 100644 --- a/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt +++ b/network/src/test/java/org/fnives/test/showcase/network/content/SessionExpirationTest.kt @@ -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)