initial commit

This commit is contained in:
Gergely Hegedus 2021-04-07 21:12:10 +03:00
parent 85ef73b2ba
commit 90a9426b7d
221 changed files with 7611 additions and 0 deletions

View file

@ -0,0 +1,12 @@
package org.fnives.test.showcase.core.content
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
class AddContentToFavouriteUseCase internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {
suspend fun invoke(contentId: ContentId) =
favouriteContentLocalStorage.markAsFavourite(contentId)
}

View file

@ -0,0 +1,35 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.fnives.test.showcase.core.shared.Optional
import org.fnives.test.showcase.core.shared.mapIntoResource
import org.fnives.test.showcase.core.shared.wrapIntoAnswer
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.content.ContentRemoteSource
internal class ContentRepository(private val contentRemoteSource: ContentRemoteSource) {
private val mutableContentFlow = MutableStateFlow(Optional<List<Content>>(null))
private val requestFlow: Flow<Resource<List<Content>>> = flow {
emit(Resource.Loading())
val response = wrapIntoAnswer { contentRemoteSource.get() }.mapIntoResource()
if (response is Resource.Success) {
mutableContentFlow.value = Optional(response.data)
}
emit(response)
}
val contents: Flow<Resource<List<Content>>> = mutableContentFlow.flatMapLatest {
if (it.item != null) flowOf(Resource.Success(it.item)) else requestFlow
}
.distinctUntilChanged()
fun fetch() {
mutableContentFlow.value = Optional(null)
}
}

View file

@ -0,0 +1,6 @@
package org.fnives.test.showcase.core.content
class FetchContentUseCase internal constructor(private val contentRepository: ContentRepository) {
fun invoke() = contentRepository.fetch()
}

View file

@ -0,0 +1,33 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.shared.Resource
class GetAllContentUseCase internal constructor(
private val contentRepository: ContentRepository,
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {
fun get(): Flow<Resource<List<FavouriteContent>>> =
contentRepository.contents.combine(favouriteContentLocalStorage.observeFavourites(), ::combineContentWithFavourites)
companion object {
private fun combineContentWithFavourites(
contentResource: Resource<List<Content>>,
favouriteContents: List<ContentId>
): Resource<List<FavouriteContent>> =
when (contentResource) {
is Resource.Error -> Resource.Error(contentResource.error)
is Resource.Loading -> Resource.Loading()
is Resource.Success -> Resource.Success(combineContentWithFavourites(contentResource.data, favouriteContents))
}
private fun combineContentWithFavourites(content: List<Content>, favourite: List<ContentId>): List<FavouriteContent> =
content.map { FavouriteContent(content = it, isFavourite = favourite.contains(it.id)) }
}
}

View file

@ -0,0 +1,13 @@
package org.fnives.test.showcase.core.content
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
class RemoveContentFromFavouritesUseCase internal constructor(
private val favouriteContentLocalStorage: FavouriteContentLocalStorage
) {
suspend fun invoke(contentId: ContentId) {
favouriteContentLocalStorage.deleteAsFavourite(contentId)
}
}

View file

@ -0,0 +1,60 @@
package org.fnives.test.showcase.core.di
import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase
import org.fnives.test.showcase.core.content.ContentRepository
import org.fnives.test.showcase.core.content.FetchContentUseCase
import org.fnives.test.showcase.core.content.GetAllContentUseCase
import org.fnives.test.showcase.core.content.RemoveContentFromFavouritesUseCase
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.SessionExpirationAdapter
import org.fnives.test.showcase.core.session.SessionExpirationListener
import org.fnives.test.showcase.core.storage.NetworkSessionLocalStorageAdapter
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.network.di.createNetworkModules
import org.koin.core.module.Module
import org.koin.core.scope.Scope
import org.koin.dsl.module
fun createCoreModule(
baseUrl: BaseUrl,
enableNetworkLogging: Boolean,
userDataLocalStorageProvider: Scope.() -> UserDataLocalStorage,
sessionExpirationListenerProvider: Scope.() -> SessionExpirationListener,
favouriteContentLocalStorageProvider: Scope.() -> FavouriteContentLocalStorage
): Sequence<Module> =
createNetworkModules(
baseUrl = baseUrl,
enableLogging = enableNetworkLogging,
networkSessionLocalStorageProvider = { get<NetworkSessionLocalStorageAdapter>() },
networkSessionExpirationListenerProvider = { SessionExpirationAdapter(sessionExpirationListenerProvider()) }
)
.plus(useCaseModule())
.plus(storageModule(userDataLocalStorageProvider, favouriteContentLocalStorageProvider))
.plus(repositoryModule())
fun repositoryModule() = module {
single(override = true) { ContentRepository(get()) }
}
fun useCaseModule() = module {
factory { LoginUseCase(get(), get()) }
factory { LogoutUseCase(get()) }
factory { GetAllContentUseCase(get(), get()) }
factory { AddContentToFavouriteUseCase(get()) }
factory { RemoveContentFromFavouritesUseCase(get()) }
factory { IsUserLoggedInUseCase(get()) }
factory { FetchContentUseCase(get()) }
}
fun storageModule(
userDataLocalStorageProvider: Scope.() -> UserDataLocalStorage,
favouriteContentLocalStorageProvider: Scope.() -> FavouriteContentLocalStorage
) = module {
single { userDataLocalStorageProvider() }
single { favouriteContentLocalStorageProvider() }
factory { NetworkSessionLocalStorageAdapter(get()) }
}

View file

@ -0,0 +1,8 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
class IsUserLoggedInUseCase(private val userDataLocalStorage: UserDataLocalStorage) {
fun invoke(): Boolean = userDataLocalStorage.session != null
}

View file

@ -0,0 +1,30 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.shared.wrapIntoAnswer
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
class LoginUseCase internal constructor(
private val loginRemoteSource: LoginRemoteSource,
private val userDataLocalStorage: UserDataLocalStorage
) {
suspend fun invoke(credentials: LoginCredentials): Answer<LoginStatus> {
if (credentials.username.isBlank()) return Answer.Success(LoginStatus.INVALID_USERNAME)
if (credentials.password.isBlank()) return Answer.Success(LoginStatus.INVALID_PASSWORD)
return wrapIntoAnswer {
when (val response = loginRemoteSource.login(credentials)) {
LoginStatusResponses.InvalidCredentials -> LoginStatus.INVALID_CREDENTIALS
is LoginStatusResponses.Success -> {
userDataLocalStorage.session = response.session
LoginStatus.SUCCESS
}
}
}
}
}

View file

@ -0,0 +1,13 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.di.repositoryModule
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.koin.core.context.loadKoinModules
class LogoutUseCase(private val storage: UserDataLocalStorage) {
suspend fun invoke() {
loadKoinModules(repositoryModule())
storage.session = null
}
}

View file

@ -0,0 +1,11 @@
package org.fnives.test.showcase.core.session
import org.fnives.test.showcase.network.session.NetworkSessionExpirationListener
internal class SessionExpirationAdapter(
private val sessionExpirationListener: SessionExpirationListener
) :
NetworkSessionExpirationListener {
override fun onSessionExpired() = sessionExpirationListener.onSessionExpired()
}

View file

@ -0,0 +1,5 @@
package org.fnives.test.showcase.core.session
interface SessionExpirationListener {
fun onSessionExpired()
}

View file

@ -0,0 +1,25 @@
package org.fnives.test.showcase.core.shared
import kotlinx.coroutines.CancellationException
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
internal suspend fun <T> wrapIntoAnswer(callback: suspend () -> T): Answer<T> =
try {
Answer.Success(callback())
} catch (networkException: NetworkException) {
Answer.Error(networkException)
} catch (parsingException: ParsingException) {
Answer.Error(parsingException)
} catch (cancellationException: CancellationException) {
throw cancellationException
} catch (throwable: Throwable) {
Answer.Error(UnexpectedException(throwable))
}
internal fun <T> Answer<T>.mapIntoResource() = when (this) {
is Answer.Error -> Resource.Error(error)
is Answer.Success -> Resource.Success(data)
}

View file

@ -0,0 +1,3 @@
package org.fnives.test.showcase.core.shared
internal class Optional<T : Any>(val item: T?)

View file

@ -0,0 +1,11 @@
package org.fnives.test.showcase.core.shared
class UnexpectedException(cause: Throwable) : RuntimeException(cause.message, cause) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return this.cause == (other as UnexpectedException).cause
}
override fun hashCode(): Int = super.hashCode() + cause.hashCode()
}

View file

@ -0,0 +1,15 @@
package org.fnives.test.showcase.core.storage
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.network.session.NetworkSessionLocalStorage
internal class NetworkSessionLocalStorageAdapter(
private val userDataLocalStorage: UserDataLocalStorage
) : NetworkSessionLocalStorage {
override var session: Session?
get() = userDataLocalStorage.session
set(value) {
userDataLocalStorage.session = value
}
}

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.core.storage
import org.fnives.test.showcase.model.session.Session
interface UserDataLocalStorage {
var session: Session?
}

View file

@ -0,0 +1,13 @@
package org.fnives.test.showcase.core.storage.content
import kotlinx.coroutines.flow.Flow
import org.fnives.test.showcase.model.content.ContentId
interface FavouriteContentLocalStorage {
fun observeFavourites(): Flow<List<ContentId>>
suspend fun markAsFavourite(contentId: ContentId)
suspend fun deleteAsFavourite(contentId: ContentId)
}

View file

@ -0,0 +1,51 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doThrow
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
@Suppress("TestFunctionName")
internal class AddContentToFavouriteUseCaseTest {
private lateinit var sut: AddContentToFavouriteUseCase
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
@BeforeEach
fun setUp() {
mockFavouriteContentLocalStorage = mock()
sut = AddContentToFavouriteUseCase(mockFavouriteContentLocalStorage)
}
@Test
fun WHEN_nothing_happens_THEN_the_storage_is_not_touched() {
verifyZeroInteractions(mockFavouriteContentLocalStorage)
}
@Test
fun GIVEN_contentId_WHEN_called_THEN_storage_is_called() = runBlockingTest {
sut.invoke(ContentId("a"))
verify(mockFavouriteContentLocalStorage, times(1)).markAsFavourite(ContentId("a"))
verifyNoMoreInteractions(mockFavouriteContentLocalStorage)
}
@Test
fun GIVEN_throwing_local_storage_WHEN_thrown_THEN_its_thrown() = runBlockingTest {
whenever(mockFavouriteContentLocalStorage.markAsFavourite(ContentId("a"))).doThrow(RuntimeException())
assertThrows(RuntimeException::class.java) {
runBlocking { sut.invoke(ContentId("a")) }
}
}
}

View file

@ -0,0 +1,153 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.shared.UnexpectedException
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.content.ContentRemoteSource
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doSuspendableAnswer
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class ContentRepositoryTest {
private lateinit var sut: ContentRepository
private lateinit var mockContentRemoteSource: ContentRemoteSource
private lateinit var testDispatcher: TestCoroutineDispatcher
@BeforeEach
fun setUp() {
testDispatcher = TestCoroutineDispatcher()
mockContentRemoteSource = mock()
sut = ContentRepository(mockContentRemoteSource)
}
@Test
fun GIVEN_no_interaction_THEN_remote_source_is_not_called() {
verifyNoMoreInteractions(mockContentRemoteSource)
}
@Test
fun GIVEN_no_response_from_remote_source_WHEN_content_observed_THEN_loading_is_returned() =
runBlockingTest(testDispatcher) {
val expected = Resource.Loading<List<Content>>()
val suspendedRequest = CompletableDeferred<Unit>()
whenever(mockContentRemoteSource.get()).doSuspendableAnswer {
suspendedRequest.await()
emptyList()
}
val actual = sut.contents.take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
suspendedRequest.complete(Unit)
}
@Test
fun GIVEN_content_response_WHEN_content_observed_THEN_loading_AND_data_is_returned() =
runBlockingTest(testDispatcher) {
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
)
whenever(mockContentRemoteSource.get())
.doReturn(listOf(Content(ContentId("a"), "", "", ImageUrl(""))))
val actual = sut.contents.take(2).toList()
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_content_error_WHEN_content_observed_THEN_loading_AND_data_is_returned() =
runBlockingTest(testDispatcher) {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
whenever(mockContentRemoteSource.get()).doThrow(exception)
val actual = sut.contents.take(2).toList()
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_content_response_THEN_error_WHEN_fetched_THEN_returned_states_are_loading_data_loading_error() =
runBlockingTest(testDispatcher) {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Success(emptyList()),
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
var first = true
whenever(mockContentRemoteSource.get()).doAnswer {
if (first) emptyList<Content>().also { first = false } else throw exception
}
val actual = async(testDispatcher) { sut.contents.take(4).toList() }
testDispatcher.advanceUntilIdle()
sut.fetch()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
}
@Test
fun GIVEN_content_response_THEN_error_WHEN_fetched_THEN_only_4_items_are_emitted() {
Assertions.assertThrows(IllegalStateException::class.java) {
runBlockingTest(testDispatcher) {
val exception = RuntimeException()
val expected = listOf(
Resource.Loading(),
Resource.Success(emptyList()),
Resource.Loading(),
Resource.Error<List<Content>>(UnexpectedException(exception))
)
var first = true
whenever(mockContentRemoteSource.get()).doAnswer {
if (first) emptyList<Content>().also { first = false } else throw exception
}
val actual = async(testDispatcher) { sut.contents.take(5).toList() }
testDispatcher.advanceUntilIdle()
sut.fetch()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
}
}
}
@Test
fun GIVEN_saved_cache_WHEN_collected_THEN_cache_is_returned() = runBlockingTest {
val content = Content(ContentId("1"), "", "", ImageUrl(""))
val expected = listOf(Resource.Success(listOf(content)))
whenever(mockContentRemoteSource.get()).doReturn(listOf(content))
sut.contents.take(2).toList()
val actual = sut.contents.take(1).toList()
verify(mockContentRemoteSource, times(1)).get()
Assertions.assertEquals(expected, actual)
}
}

View file

@ -0,0 +1,49 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doThrow
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
@Suppress("TestFunctionName")
internal class FetchContentUseCaseTest {
private lateinit var sut: FetchContentUseCase
private lateinit var mockContentRepository: ContentRepository
@BeforeEach
fun setUp() {
mockContentRepository = mock()
sut = FetchContentUseCase(mockContentRepository)
}
@Test
fun WHEN_nothing_happens_THEN_the_storage_is_not_touched() {
verifyZeroInteractions(mockContentRepository)
}
@Test
fun WHEN_called_THEN_repository_is_called() = runBlockingTest {
sut.invoke()
verify(mockContentRepository, times(1)).fetch()
verifyNoMoreInteractions(mockContentRepository)
}
@Test
fun GIVEN_throwing_local_storage_WHEN_thrown_THEN_its_thrown() = runBlockingTest {
whenever(mockContentRepository.fetch()).doThrow(RuntimeException())
assertThrows(RuntimeException::class.java) {
runBlocking { sut.invoke() }
}
}
}

View file

@ -0,0 +1,214 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.ContentId
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.model.content.ImageUrl
import org.fnives.test.showcase.model.shared.Resource
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class GetAllContentUseCaseTest {
private lateinit var sut: GetAllContentUseCase
private lateinit var mockContentRepository: ContentRepository
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
private lateinit var contentFlow: MutableStateFlow<Resource<List<Content>>>
private lateinit var favouriteContentIdFlow: MutableStateFlow<List<ContentId>>
private lateinit var testDispatcher: TestCoroutineDispatcher
@BeforeEach
fun setUp() {
testDispatcher = TestCoroutineDispatcher()
mockFavouriteContentLocalStorage = mock()
mockContentRepository = mock()
favouriteContentIdFlow = MutableStateFlow(emptyList())
contentFlow = MutableStateFlow(Resource.Loading())
whenever(mockFavouriteContentLocalStorage.observeFavourites()).doReturn(favouriteContentIdFlow)
whenever(mockContentRepository.contents).doReturn(contentFlow)
sut = GetAllContentUseCase(mockContentRepository, mockFavouriteContentLocalStorage)
}
@Test
fun GIVEN_loading_AND_empty_favourite_WHEN_observed_THEN_loading_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
contentFlow.value = Resource.Loading()
val expected = Resource.Loading<List<FavouriteContent>>()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_loading_AND_listOfFavourite_WHEN_observed_THEN_loading_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("a"))
contentFlow.value = Resource.Loading()
val expected = Resource.Loading<List<FavouriteContent>>()
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_error_AND_empty_favourite_WHEN_observed_THEN_error_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = Resource.Error<List<FavouriteContent>>(exception)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_error_AND_listOfFavourite_WHEN_observed_THEN_error_is_shown() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("b"))
val exception = Throwable()
contentFlow.value = Resource.Error(exception)
val expected = Resource.Error<List<FavouriteContent>>(exception)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_listOfContent_AND_empty_favourite_WHEN_observed_THEN_favourites_are_returned() = runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, false)
)
val expected = Resource.Success(items)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_listOfContent_AND_other_favourite_id_WHEN_observed_THEN_favourites_are_returned() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("x"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, false)
)
val expected = Resource.Success(items)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_listOfContent_AND_same_favourite_id_WHEN_observed_THEN_favourites_are_returned() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("a"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Success(listOf(content))
val items = listOf(
FavouriteContent(content, true)
)
val expected = Resource.Success(items)
val actual = sut.get().take(1).toList()
Assertions.assertEquals(listOf(expected), actual)
}
@Test
fun GIVEN_loading_then_data_then_added_favourite_WHEN_observed_THEN_loading_then_correct_favourites_are_returned() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = emptyList()
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Loading()
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(FavouriteContent(content, false))),
Resource.Success(listOf(FavouriteContent(content, true)))
)
val actual = async(testDispatcher) {
sut.get().take(3).toList()
}
testDispatcher.advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
testDispatcher.advanceUntilIdle()
favouriteContentIdFlow.value = listOf(ContentId("a"))
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
}
@Test
fun GIVEN_loading_then_data_then_removed_favourite_WHEN_observed_THEN_loading_then_correct_favourites_are_returned() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("a"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Loading()
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(FavouriteContent(content, true))),
Resource.Success(listOf(FavouriteContent(content, false)))
)
val actual = async(testDispatcher) {
sut.get().take(3).toList()
}
testDispatcher.advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
testDispatcher.advanceUntilIdle()
favouriteContentIdFlow.value = emptyList()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
}
@Test
fun GIVEN_loading_then_data_then_loading_WHEN_observed_THEN_loading_then_correct_favourites_then_loadingare_returned() =
runBlockingTest(testDispatcher) {
favouriteContentIdFlow.value = listOf(ContentId("a"))
val content = Content(ContentId("a"), "b", "c", ImageUrl("d"))
contentFlow.value = Resource.Loading()
val expected = listOf(
Resource.Loading(),
Resource.Success(listOf(FavouriteContent(content, true))),
Resource.Loading()
)
val actual = async(testDispatcher) {
sut.get().take(3).toList()
}
testDispatcher.advanceUntilIdle()
contentFlow.value = Resource.Success(listOf(content))
testDispatcher.advanceUntilIdle()
contentFlow.value = Resource.Loading()
testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expected, actual.await())
}
}

View file

@ -0,0 +1,51 @@
package org.fnives.test.showcase.core.content
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
import org.fnives.test.showcase.model.content.ContentId
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doThrow
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
@Suppress("TestFunctionName")
internal class RemoveContentFromFavouritesUseCaseTest {
private lateinit var sut: RemoveContentFromFavouritesUseCase
private lateinit var mockFavouriteContentLocalStorage: FavouriteContentLocalStorage
@BeforeEach
fun setUp() {
mockFavouriteContentLocalStorage = mock()
sut = RemoveContentFromFavouritesUseCase(mockFavouriteContentLocalStorage)
}
@Test
fun WHEN_nothing_happens_THEN_the_storage_is_not_touched() {
verifyZeroInteractions(mockFavouriteContentLocalStorage)
}
@Test
fun GIVEN_contentId_WHEN_called_THEN_storage_is_called() = runBlockingTest {
sut.invoke(ContentId("a"))
verify(mockFavouriteContentLocalStorage, times(1)).deleteAsFavourite(ContentId("a"))
verifyNoMoreInteractions(mockFavouriteContentLocalStorage)
}
@Test
fun GIVEN_throwing_local_storage_WHEN_thrown_THEN_its_thrown() = runBlockingTest {
whenever(mockFavouriteContentLocalStorage.deleteAsFavourite(ContentId("a"))).doThrow(RuntimeException())
Assertions.assertThrows(RuntimeException::class.java) {
runBlocking { sut.invoke(ContentId("a")) }
}
}
}

View file

@ -0,0 +1,61 @@
package org.fnives.test.showcase.core.login
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.session.Session
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class IsUserLoggedInUseCaseTest {
private lateinit var sut: IsUserLoggedInUseCase
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
sut = IsUserLoggedInUseCase(mockUserDataLocalStorage)
}
@Test
fun WHEN_nothing_is_called_THEN_storage_is_not_called() {
verifyZeroInteractions(mockUserDataLocalStorage)
}
@Test
fun GIVEN_session_data_saved_WHEN_is_user_logged_in_checked_THEN_true_is_returned() {
whenever(mockUserDataLocalStorage.session).doReturn(Session("a", "b"))
val actual = sut.invoke()
Assertions.assertEquals(true, actual)
}
@Test
fun GIVEN_no_session_data_saved_WHEN_is_user_logged_in_checked_THEN_false_is_returned() {
whenever(mockUserDataLocalStorage.session).doReturn(null)
val actual = sut.invoke()
Assertions.assertEquals(false, actual)
}
@Test
fun GIVEN_no_session_THEN_session_THEN_no_session_WHEN_is_user_logged_in_checked_over_again_THEN_every_return_is_correct() {
whenever(mockUserDataLocalStorage.session).doReturn(null)
val actual1 = sut.invoke()
whenever(mockUserDataLocalStorage.session).doReturn(Session("", ""))
val actual2 = sut.invoke()
whenever(mockUserDataLocalStorage.session).doReturn(null)
val actual3 = sut.invoke()
Assertions.assertEquals(false, actual1)
Assertions.assertEquals(true, actual2)
Assertions.assertEquals(false, actual3)
}
}

View file

@ -0,0 +1,95 @@
package org.fnives.test.showcase.core.login
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.shared.UnexpectedException
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.auth.LoginCredentials
import org.fnives.test.showcase.model.auth.LoginStatus
import org.fnives.test.showcase.model.session.Session
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.network.auth.LoginRemoteSource
import org.fnives.test.showcase.network.auth.model.LoginStatusResponses
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyZeroInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName")
internal class LoginUseCaseTest {
private lateinit var sut: LoginUseCase
private lateinit var mockLoginRemoteSource: LoginRemoteSource
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockLoginRemoteSource = mock()
mockUserDataLocalStorage = mock()
sut = LoginUseCase(mockLoginRemoteSource, mockUserDataLocalStorage)
}
@Test
fun GIVEN_empty_username_WHEN_trying_to_login_THEN_invalid_username_is_returned() = runBlockingTest {
val expected = Answer.Success(LoginStatus.INVALID_USERNAME)
val actual = sut.invoke(LoginCredentials("", "a"))
Assertions.assertEquals(expected, actual)
verifyZeroInteractions(mockLoginRemoteSource)
verifyZeroInteractions(mockUserDataLocalStorage)
}
@Test
fun GIVEN_empty_password_WHEN_trying_to_login_THEN_invalid_password_is_returned() = runBlockingTest {
val expected = Answer.Success(LoginStatus.INVALID_PASSWORD)
val actual = sut.invoke(LoginCredentials("a", ""))
Assertions.assertEquals(expected, actual)
verifyZeroInteractions(mockLoginRemoteSource)
verifyZeroInteractions(mockUserDataLocalStorage)
}
@Test
fun GIVEN_login_invalid_credentials_response_WHEN_trying_to_login_THEN_invalid_credentials_is_returned() = runBlockingTest {
val expected = Answer.Success(LoginStatus.INVALID_CREDENTIALS)
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
.doReturn(LoginStatusResponses.InvalidCredentials)
val actual = sut.invoke(LoginCredentials("a", "b"))
Assertions.assertEquals(expected, actual)
verifyZeroInteractions(mockUserDataLocalStorage)
}
@Test
fun GIVEN_valid_login_response_WHEN_trying_to_login_THEN_Success_is_returned() = runBlockingTest {
val expected = Answer.Success(LoginStatus.SUCCESS)
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
.doReturn(LoginStatusResponses.Success(Session("c", "d")))
val actual = sut.invoke(LoginCredentials("a", "b"))
Assertions.assertEquals(expected, actual)
verify(mockUserDataLocalStorage, times(1)).session = Session("c", "d")
}
@Test
fun GIVEN_throwing_remote_source_WHEN_trying_to_login_THEN_error_is_returned() = runBlockingTest {
val exception = RuntimeException()
val expected = Answer.Error<LoginStatus>(UnexpectedException(exception))
whenever(mockLoginRemoteSource.login(LoginCredentials("a", "b")))
.doThrow(exception)
val actual = sut.invoke(LoginCredentials("a", "b"))
Assertions.assertEquals(expected, actual)
verifyZeroInteractions(mockUserDataLocalStorage)
}
}

View file

@ -0,0 +1,65 @@
package org.fnives.test.showcase.core.login
import kotlinx.coroutines.test.runBlockingTest
import org.fnives.test.showcase.core.content.ContentRepository
import org.fnives.test.showcase.core.di.createCoreModule
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
import org.fnives.test.showcase.model.network.BaseUrl
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
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.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.verifyZeroInteractions
@Suppress("TestFunctionName")
internal class LogoutUseCaseTest : KoinTest {
private lateinit var sut: LogoutUseCase
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
sut = LogoutUseCase(mockUserDataLocalStorage)
startKoin {
modules(
createCoreModule(
baseUrl = BaseUrl("https://a.b.com"),
enableNetworkLogging = true,
favouriteContentLocalStorageProvider = { mock() },
sessionExpirationListenerProvider = { mock() },
userDataLocalStorageProvider = { mock() }
).toList()
)
}
}
@AfterEach
fun tearDown() {
stopKoin()
}
@Test
fun WHEN_no_call_THEN_storage_is_not_interacted() {
verifyZeroInteractions(mockUserDataLocalStorage)
}
@Test
fun WHEN_logout_invoked_THEN_storage_is_cleared() = runBlockingTest {
val repositoryBefore = getKoin().get<ContentRepository>()
sut.invoke()
val repositoryAfter = getKoin().get<ContentRepository>()
verify(mockUserDataLocalStorage, times(1)).session = null
verifyNoMoreInteractions(mockUserDataLocalStorage)
Assertions.assertNotSame(repositoryBefore, repositoryAfter)
}
}

View file

@ -0,0 +1,35 @@
package org.fnives.test.showcase.core.session
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
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
@Suppress("TestFunctionName")
internal class SessionExpirationAdapterTest {
private lateinit var sut: SessionExpirationAdapter
private lateinit var mockSessionExpirationListener: SessionExpirationListener
@BeforeEach
fun setUp() {
mockSessionExpirationListener = mock()
sut = SessionExpirationAdapter(mockSessionExpirationListener)
}
@Test
fun WHEN_onSessionExpired_is_called_THEN_its_delegated() {
sut.onSessionExpired()
verify(mockSessionExpirationListener, times(1)).onSessionExpired()
verifyNoMoreInteractions(mockSessionExpirationListener)
}
@Test
fun WHEN_nothing_is_changed_THEN_delegate_is_not_touched() {
verifyZeroInteractions(mockSessionExpirationListener)
}
}

View file

@ -0,0 +1,79 @@
package org.fnives.test.showcase.core.shared
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.model.shared.Answer
import org.fnives.test.showcase.model.shared.Resource
import org.fnives.test.showcase.network.shared.exceptions.NetworkException
import org.fnives.test.showcase.network.shared.exceptions.ParsingException
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@Suppress("TestFunctionName")
internal class AnswerUtilsKtTest {
@Test
fun GIVEN_network_exception_thrown_WHEN_wrapped_into_answer_THEN_answer_error_is_returned() = runBlocking {
val exception = NetworkException(Throwable())
val expected = Answer.Error<Unit>(exception)
val actual = wrapIntoAnswer<Unit> { throw exception }
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_parsing_exception_thrown_WHEN_wrapped_into_answer_THEN_answer_error_is_returned() = runBlocking {
val exception = ParsingException(Throwable())
val expected = Answer.Error<Unit>(exception)
val actual = wrapIntoAnswer<Unit> { throw exception }
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_parsing_throwable_thrown_WHEN_wrapped_into_answer_THEN_answer_error_is_returned() = runBlocking {
val exception = Throwable()
val expected = Answer.Error<Unit>(UnexpectedException(exception))
val actual = wrapIntoAnswer<Unit> { throw exception }
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_string_WHEN_wrapped_into_answer_THEN_string_answer_is_returned() = runBlocking {
val expected = Answer.Success("banan")
val actual = wrapIntoAnswer { "banan" }
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_cancellation_exception_WHEN_wrapped_into_answer_THEN_cancellation_exception_is_thrown() {
Assertions.assertThrows(CancellationException::class.java) {
runBlocking { wrapIntoAnswer { throw CancellationException() } }
}
}
@Test
fun GIVEN_success_answer_WHEN_converted_into_resource_THEN_Resource_success_is_returned() {
val expected = Resource.Success("alma")
val actual = Answer.Success("alma").mapIntoResource()
Assertions.assertEquals(expected, actual)
}
@Test
fun GIVEN_error_answer_WHEN_converted_into_resource_THEN_Resource_error_is_returned() {
val exception = Throwable()
val expected = Resource.Error<Unit>(exception)
val actual = Answer.Error<Unit>(exception).mapIntoResource()
Assertions.assertEquals(expected, actual)
}
}

View file

@ -0,0 +1,55 @@
package org.fnives.test.showcase.core.storage
import org.fnives.test.showcase.model.session.Session
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
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.whenever
@Suppress("TestFunctionName")
internal class NetworkSessionLocalStorageAdapterTest {
private lateinit var sut: NetworkSessionLocalStorageAdapter
private lateinit var mockUserDataLocalStorage: UserDataLocalStorage
@BeforeEach
fun setUp() {
mockUserDataLocalStorage = mock()
sut = NetworkSessionLocalStorageAdapter(mockUserDataLocalStorage)
}
@Test
fun GIVEN_null_as_session_WHEN_saved_THEN_its_delegated() {
sut.session = null
verify(mockUserDataLocalStorage, times(1)).session = null
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
@Test
fun GIVEN_session_WHEN_saved_THEN_its_delegated() {
val expected = Session("a", "b")
sut.session = Session("a", "b")
verify(mockUserDataLocalStorage, times(1)).session = expected
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
@Test
fun WHEN_session_requested_THEN_its_returned_from_delegated() {
val expected = Session("a", "b")
whenever(mockUserDataLocalStorage.session).doReturn(expected)
val actual = sut.session
Assertions.assertSame(expected, actual)
verify(mockUserDataLocalStorage, times(1)).session
verifyNoMoreInteractions(mockUserDataLocalStorage)
}
}

View file

@ -0,0 +1 @@
mock-maker-inline