Issue#49 Add core-network integration tests
This commit is contained in:
parent
a69fdce26c
commit
555ad6d05f
4 changed files with 286 additions and 29 deletions
|
|
@ -27,5 +27,7 @@ dependencies {
|
||||||
testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
testImplementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
testImplementation "app.cash.turbine:turbine:$turbine_version"
|
testImplementation "app.cash.turbine:turbine:$turbine_version"
|
||||||
|
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-params:$testing_junit5_version"
|
||||||
|
|
||||||
testImplementation project(':mockserver')
|
testImplementation project(':mockserver')
|
||||||
}
|
}
|
||||||
|
|
@ -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"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,9 +37,6 @@ import org.koin.core.context.stopKoin
|
||||||
import org.koin.test.KoinTest
|
import org.koin.test.KoinTest
|
||||||
import org.koin.test.inject
|
import org.koin.test.inject
|
||||||
import org.mockito.kotlin.mock
|
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.verifyZeroInteractions
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -335,30 +332,4 @@ class ContentIntegrationTest : KoinTest {
|
||||||
val expectedSession = Session(accessToken = "refreshed-access", refreshToken = "refreshed-refresh")
|
val expectedSession = Session(accessToken = "refreshed-access", refreshToken = "refreshed-refresh")
|
||||||
Assertions.assertEquals(expectedSession, fakeUserDataLocalStorage.session)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue