Issue#49 Add core-network integration tests

This commit is contained in:
Gergely Hegedus 2022-01-27 20:48:03 +02:00
parent a69fdce26c
commit 555ad6d05f
4 changed files with 286 additions and 29 deletions

View file

@ -27,5 +27,7 @@ dependencies {
testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
testImplementation "app.cash.turbine:turbine:$turbine_version"
testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version"
testImplementation project(':mockserver')
}

View file

@ -0,0 +1,167 @@
package org.fnives.test.showcase.core.integration
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.core.di.koin.createCoreModule
import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.core.login.LogoutUseCase
import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
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.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.test.KoinTest
import org.koin.test.inject
import org.mockito.kotlin.mock
import org.mockito.kotlin.verifyZeroInteractions
import java.util.stream.Stream
@OptIn(ExperimentalCoroutinesApi::class)
class AuthIntegrationTest : KoinTest {
private lateinit var mockServerScenarioSetup: MockServerScenarioSetup
private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage
private lateinit var mockSessionExpirationListener: SessionExpirationListener
private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage
private val isUserLoggedInUseCase by inject<IsUserLoggedInUseCase>()
private val loginUseCase by inject<LoginUseCase>()
private val logoutUseCase by inject<LogoutUseCase>()
@BeforeEach
fun setup() {
mockSessionExpirationListener = mock()
mockServerScenarioSetup = MockServerScenarioSetup()
val url = mockServerScenarioSetup.start(false)
fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage()
fakeUserDataLocalStorage = FakeUserDataLocalStorage(null)
startKoin {
modules(
createCoreModule(
baseUrl = BaseUrl(url),
enableNetworkLogging = true,
favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage },
sessionExpirationListenerProvider = { mockSessionExpirationListener },
userDataLocalStorageProvider = { fakeUserDataLocalStorage }
).toList()
)
}
}
@AfterEach
fun tearDown() {
mockServerScenarioSetup.stop()
stopKoin()
}
@DisplayName("GIVEN no session saved WHEN checking if user is logged in THEN they are not")
@Test
fun withoutSessionTheUserIsNotLoggedIn() = runTest {
fakeUserDataLocalStorage.session = null
val actual = isUserLoggedInUseCase.invoke()
Assertions.assertFalse(actual, "User is expected to be not logged in")
verifyZeroInteractions(mockSessionExpirationListener)
}
@DisplayName("GIVEN no session WHEN user is logging in THEN they get session")
@Test
fun login() = runTest {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true)
val expectedSession = ContentData.loginSuccessResponse
val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc"))
val actual = isUserLoggedInUseCase.invoke()
Assertions.assertEquals(Answer.Success(LoginStatus.SUCCESS), answer)
Assertions.assertTrue(actual, "User is expected to be logged in")
Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session)
verifyZeroInteractions(mockSessionExpirationListener)
}
@MethodSource("localInputErrorArguments")
@ParameterizedTest(name = "GIVEN {0} credentials WHEN login called THEN error {1} is shown")
fun localInputError(credentials: LoginCredentials, loginError: LoginStatus) = runTest {
val answer = loginUseCase.invoke(credentials)
val actual = isUserLoggedInUseCase.invoke()
Assertions.assertEquals(Answer.Success(loginError), answer)
Assertions.assertFalse(actual, "User is expected to be not logged in")
Assertions.assertEquals(null, fakeUserDataLocalStorage.session)
verifyZeroInteractions(mockSessionExpirationListener)
}
@DisplayName("GIVEN no session WHEN user is logging in THEN they get session")
@Test
fun loginInvalidCredentials() = runTest {
mockServerScenarioSetup.setScenario(AuthScenario.InvalidCredentials(username = "usr", password = "sEc"), validateArguments = true)
val answer = loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc"))
val actual = isUserLoggedInUseCase.invoke()
Assertions.assertEquals(Answer.Success(LoginStatus.INVALID_CREDENTIALS), answer)
Assertions.assertFalse(actual, "User is expected to be not logged in")
Assertions.assertEquals(null, fakeUserDataLocalStorage.session)
verifyZeroInteractions(mockSessionExpirationListener)
}
@MethodSource("networkErrorArguments")
@ParameterizedTest(name = "GIVEN {0} network response WHEN login called THEN error is shown")
fun networkInputError(authScenario: AuthScenario) = runTest {
mockServerScenarioSetup.setScenario(authScenario, validateArguments = true)
val credentials = LoginCredentials(username = authScenario.username, password = authScenario.password)
val answer = loginUseCase.invoke(credentials)
val actual = isUserLoggedInUseCase.invoke()
Assertions.assertTrue(answer is Answer.Error, "Answer is expected to be an Error")
Assertions.assertFalse(actual, "User is expected to be not logged in")
Assertions.assertEquals(null, fakeUserDataLocalStorage.session)
verifyZeroInteractions(mockSessionExpirationListener)
}
@DisplayName("GIVEN logged in user WHEN user is login out THEN they no longer have a session and content is cleared")
@Test
fun logout() = runTest {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "usr", password = "sEc"), validateArguments = true)
loginUseCase.invoke(LoginCredentials(username = "usr", password = "sEc"))
logoutUseCase.invoke()
val actual = isUserLoggedInUseCase.invoke()
Assertions.assertEquals(false, actual, "User is expected to be logged out")
verifyZeroInteractions(mockSessionExpirationListener)
}
companion object {
@JvmStatic
fun localInputErrorArguments() = Stream.of(
Arguments.of(LoginCredentials("", "password"), LoginStatus.INVALID_USERNAME),
Arguments.of(LoginCredentials("username", ""), LoginStatus.INVALID_PASSWORD)
)
@JvmStatic
fun networkErrorArguments() = Stream.of(
Arguments.of(AuthScenario.GenericError(username = "a", password = "b")),
Arguments.of(AuthScenario.UnexpectedJsonAsSuccessResponse(username = "a", password = "b")),
Arguments.of(AuthScenario.MalformedJsonAsSuccessResponse(username = "a", password = "b")),
Arguments.of(AuthScenario.MissingFieldJson(username = "a", password = "b"))
)
}
}

View file

@ -37,9 +37,6 @@ import org.koin.core.context.stopKoin
import org.koin.test.KoinTest
import org.koin.test.inject
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
@OptIn(ExperimentalCoroutinesApi::class)
@ -335,30 +332,4 @@ class ContentIntegrationTest : KoinTest {
val expectedSession = Session(accessToken = "refreshed-access", refreshToken = "refreshed-refresh")
Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session)
}
@DisplayName("GIVEN session expiration and failing token-refresh response WHEN observing THEN session expiration is attached")
@Test
fun sessionExpiration() = runTest {
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
.setScenario(
ContentScenario.Unauthorized(usingRefreshedToken = false)
.then(ContentScenario.Success(usingRefreshedToken = true))
)
val actual = async {
getAllContentUseCase.get()
.take(2)
.toList()
}
val actualValues = actual.await()
Assertions.assertEquals(Resource.Loading<List<FavouriteContent>>(), actualValues[0])
Assertions.assertTrue(actualValues[1] is Resource.Error, "Resource is Error")
Assertions.assertTrue((actualValues[1] as Resource.Error).error is NetworkException, "Resource is Network Error")
verify(mockSessionExpirationListener, times(1)).onSessionExpired()
verifyNoMoreInteractions(mockSessionExpirationListener)
Assertions.assertEquals(null, fakeUserDataLocalStorage.session)
}
}

View file

@ -0,0 +1,117 @@
package org.fnives.test.showcase.core.integration
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.fnives.test.showcase.core.content.FetchContentUseCase
import org.fnives.test.showcase.core.content.GetAllContentUseCase
import org.fnives.test.showcase.core.di.koin.createCoreModule
import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.core.login.LoginUseCase
import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
import org.fnives.test.showcase.network.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.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.verifyZeroInteractions
@OptIn(ExperimentalCoroutinesApi::class)
class SessionExpirationIntegrationTest : KoinTest {
private lateinit var mockServerScenarioSetup: MockServerScenarioSetup
private lateinit var fakeFavouriteContentLocalStorage: FakeFavouriteContentLocalStorage
private lateinit var mockSessionExpirationListener: SessionExpirationListener
private lateinit var fakeUserDataLocalStorage: FakeUserDataLocalStorage
private val isUserLoggedInUseCase by inject<IsUserLoggedInUseCase>()
private val getAllContentUseCase by inject<GetAllContentUseCase>()
private val loginUseCase by inject<LoginUseCase>()
private val fetchContentUseCase by inject<FetchContentUseCase>()
@BeforeEach
fun setup() {
mockSessionExpirationListener = mock()
mockServerScenarioSetup = MockServerScenarioSetup()
val url = mockServerScenarioSetup.start(false)
fakeFavouriteContentLocalStorage = FakeFavouriteContentLocalStorage()
fakeUserDataLocalStorage = FakeUserDataLocalStorage(null)
startKoin {
modules(
createCoreModule(
baseUrl = BaseUrl(url),
enableNetworkLogging = true,
favouriteContentLocalStorageProvider = { fakeFavouriteContentLocalStorage },
sessionExpirationListenerProvider = { mockSessionExpirationListener },
userDataLocalStorageProvider = { fakeUserDataLocalStorage }
).toList()
)
}
}
@AfterEach
fun tearDown() {
mockServerScenarioSetup.stop()
stopKoin()
}
@DisplayName("GIVEN logged in user WHEN fetching but expired THEN user is logged out")
@Test
fun sessionResultsInErrorAndClearsContent() = runTest {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"), validateArguments = true)
loginUseCase.invoke(LoginCredentials(username = "a", password = "b"))
Assertions.assertTrue(isUserLoggedInUseCase.invoke())
verifyZeroInteractions(mockSessionExpirationListener)
mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(usingRefreshedToken = false))
.setScenario(RefreshTokenScenario.Error)
getAllContentUseCase.get().take(2).toList() // getting session expiration
verify(mockSessionExpirationListener, times(1)).onSessionExpired()
verifyNoMoreInteractions(mockSessionExpirationListener)
Assertions.assertFalse(isUserLoggedInUseCase.invoke(), "User is expected to be logged out")
}
@DisplayName("GIVEN session expiration and failing token-refresh response WHEN requiring data THEN error is returned and data is cleared")
@Test
fun sessionExpirationResultsInLogout() = runTest {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "", password = ""), validateArguments = true)
loginUseCase.invoke(LoginCredentials(username = "", password = ""))
mockServerScenarioSetup.setScenario(RefreshTokenScenario.Error)
.setScenario(
ContentScenario.Success(usingRefreshedToken = true)
.then(ContentScenario.Unauthorized(usingRefreshedToken = false))
.then(ContentScenario.Success(usingRefreshedToken = true))
)
getAllContentUseCase.get().take(2).toList() // cachedData
fetchContentUseCase.invoke()
val unauthorizedData = getAllContentUseCase.get().take(2).last()
Assertions.assertTrue(unauthorizedData is Resource.Error, "Resource is Error")
Assertions.assertTrue((unauthorizedData as Resource.Error).error is NetworkException, "Resource is Network Error")
}
}