Skip to content

Commit

Permalink
[#18] Add SurveyRepository
Browse files Browse the repository at this point in the history
  • Loading branch information
blyscuit committed Nov 18, 2022
1 parent 0d25c52 commit daf1064
Show file tree
Hide file tree
Showing 16 changed files with 562 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package co.nimblehq.blisskmmic.data.model

import co.nimblehq.blisskmmic.domain.model.PaginationMeta
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
class PaginationMetaApiModel(
val page: Int,
val pages: Int,
@SerialName("page_size")
val pageSize: Int,
val records: Int
)

fun PaginationMetaApiModel.toPaginationMeta(): PaginationMeta {
return PaginationMeta(page, pages, pageSize, records)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package co.nimblehq.blisskmmic.data.model

import co.nimblehq.blisskmmic.domain.model.Survey
import co.nimblehq.jsonapi.model.ApiJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class SurveyApiModel(
val title: String,
val description: String,
@SerialName("is_active")
val isActive: Boolean,
@SerialName("cover_image_url")
val coverImageUrl: String,
@SerialName("survey_type")
val surveyType: String
)

fun SurveyApiModel.toSurvey(): Survey {
return Survey(
title,
description,
isActive,
coverImageUrl,
surveyType
)
}

Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
package co.nimblehq.blisskmmic.data.network.datasource

import co.nimblehq.blisskmmic.data.model.PaginationMetaApiModel
import co.nimblehq.blisskmmic.data.model.SurveyApiModel
import co.nimblehq.blisskmmic.data.network.core.NetworkClient
import co.nimblehq.blisskmmic.data.network.target.LoginTargetType
import co.nimblehq.blisskmmic.data.network.target.SurveySelectionTargetType
import co.nimblehq.blisskmmic.data.network.target.SurveyTargetType
import co.nimblehq.blisskmmic.domain.model.PaginationMeta
import co.nimblehq.blisskmmic.domain.model.Survey
import co.nimblehq.blisskmmic.domain.model.TokenApiModel
import kotlinx.coroutines.flow.Flow

interface NetworkDataSource {

fun logIn(target: LoginTargetType): Flow<TokenApiModel>
fun survey(target: SurveyTargetType): Flow<Pair<List<SurveyApiModel>, PaginationMetaApiModel>>
}

class NetworkDataSourceImpl(private val networkClient: NetworkClient): NetworkDataSource {

override fun logIn(target: LoginTargetType): Flow<TokenApiModel> {
return networkClient.fetch(target.requestBuilder)
}

override fun survey(target: SurveyTargetType): Flow<Pair<List<SurveyApiModel>, PaginationMetaApiModel>> {
return networkClient.fetchWithMeta<List<SurveyApiModel>, PaginationMetaApiModel>(
target.requestBuilder
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package co.nimblehq.blisskmmic.data.network.target

import co.nimblehq.blisskmmic.data.network.helpers.TargetType
import io.ktor.http.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

sealed class SurveyTargetType: TargetType

class SurveySelectionTargetType(page: Int = 1, size: Int = 3): SurveyTargetType() {

@Serializable
data class SurveySelectionInput(
@SerialName("page[number]")
val pageNumber: Int,
@SerialName("page[size]")
val pageSize: Int
)

override var path = "surveys"
override var method = HttpMethod.Post
override var body = SurveySelectionInput(page, size)
override var headers: Map<String, String>? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package co.nimblehq.blisskmmic.data.repository

import co.nimblehq.blisskmmic.data.model.*
import co.nimblehq.blisskmmic.data.network.core.NetworkClient
import co.nimblehq.blisskmmic.data.network.datasource.NetworkDataSource
import co.nimblehq.blisskmmic.data.network.target.SurveySelectionTargetType
import co.nimblehq.blisskmmic.domain.model.PaginationMeta
import co.nimblehq.blisskmmic.domain.model.Survey
import co.nimblehq.blisskmmic.domain.repository.SurveyRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class SurveyRepositoryImpl(private val networkDataSource: NetworkDataSource): SurveyRepository {

override fun survey(page: Int): Flow<Pair<List<Survey>, PaginationMeta>> {
return networkDataSource.survey(SurveySelectionTargetType(page))
.map {
Pair(
it.first.map { item -> item.toSurvey() },
it.second.toPaginationMeta()
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package co.nimblehq.blisskmmic.di.koin.modules

import co.nimblehq.blisskmmic.data.repository.SurveyRepositoryImpl
import co.nimblehq.blisskmmic.data.repository.TokenRepositoryImpl
import co.nimblehq.blisskmmic.di.koin.constants.TOKENIZED_NETWORK_CLIENT_KOIN
import co.nimblehq.blisskmmic.domain.repository.SurveyRepository
import co.nimblehq.blisskmmic.domain.repository.TokenRepository
import org.koin.core.qualifier.named
import org.koin.dsl.module

val repositoryModule = module {
single<TokenRepository> { TokenRepositoryImpl(get(), get()) }
factory<SurveyRepository> { SurveyRepositoryImpl(get(named(TOKENIZED_NETWORK_CLIENT_KOIN))) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.nimblehq.blisskmmic.domain.model

data class PaginationMeta(
val page: Int,
val pages: Int,
val pageSize: Int,
val records: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package co.nimblehq.blisskmmic.domain.model

import kotlinx.serialization.SerialName

data class Survey(
val title: String,
val description: String,
val isActive: Boolean,
val coverImageUrl: String,
val surveyType: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package co.nimblehq.blisskmmic.domain.repository

import co.nimblehq.blisskmmic.domain.model.PaginationMeta
import co.nimblehq.blisskmmic.domain.model.Survey
import co.nimblehq.blisskmmic.domain.model.Token
import kotlinx.coroutines.flow.Flow

interface SurveyRepository {

fun survey(page: Int): Flow<Pair<List<Survey>, PaginationMeta>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package co.nimblehq.blisskmmic.domain.usecase

import co.nimblehq.blisskmmic.domain.model.Survey
import co.nimblehq.blisskmmic.domain.model.Token
import co.nimblehq.blisskmmic.domain.repository.SurveyRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

interface SurveyListUseCase {

operator fun invoke(page: Int): Flow<List<Survey>>
}

class SurveyListUseCaseImpl(
private val repository: SurveyRepository
) : SurveyListUseCase {

override operator fun invoke(page: Int): Flow<List<Survey>> {
return repository
.survey(page)
.map { it.first }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package co.nimblehq.blisskmmic.data.datasource
import co.nimblehq.blisskmmic.data.network.core.NetworkClient
import co.nimblehq.blisskmmic.data.network.datasource.NetworkDataSourceImpl
import co.nimblehq.blisskmmic.data.network.target.LoginTargetType
import co.nimblehq.blisskmmic.data.network.target.SurveySelectionTargetType
import co.nimblehq.blisskmmic.helpers.json.ERROR_JSON_RESULT
import co.nimblehq.blisskmmic.helpers.json.LOG_IN_JSON_RESULT
import co.nimblehq.blisskmmic.helpers.json.SURVEY_LIST_JSON_RESULT
import co.nimblehq.blisskmmic.helpers.mock.ktor.jsonMockEngine
import co.nimblehq.jsonapi.model.JsonApiException
import io.kotest.matchers.collections.shouldContain
Expand Down Expand Up @@ -48,4 +50,37 @@ class NetworkDataSourceTest {
fail("Should not return object")
}
}

@Test
fun `When calling survey with success response, it returns correct object`() = runTest {
val engine = jsonMockEngine(SURVEY_LIST_JSON_RESULT, "surveys")
val networkClient = NetworkClient(engine = engine)
val dataSource = NetworkDataSourceImpl(networkClient)
dataSource
.survey(SurveySelectionTargetType())
.collect {
it.first.size shouldBe 2
it.first.first().title shouldBe "Scarlett Bangkok"
it.second.page shouldBe 1
}
}

@Suppress("MaxLineLength")
@Test
fun `When calling survey with failure response, it returns correct error`() = runTest {
val engine = jsonMockEngine(ERROR_JSON_RESULT, "surveys")
val networkClient = NetworkClient(engine = engine)
val dataSource = NetworkDataSourceImpl(networkClient)
dataSource
.survey(SurveySelectionTargetType())
.catch { error ->
when(error) {
is JsonApiException -> error.errors.map { it.code } shouldContain "invalid_token"
else -> fail("Should not return incorrect error type")
}
}
.collect {
fail("Should not return object")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package co.nimblehq.blisskmmic.data.network.core

import co.nimblehq.blisskmmic.helpers.mock.NETWORK_MOCK_MODEL_RESULT
import co.nimblehq.blisskmmic.helpers.mock.NETWORK_META_MOCK_MODEL_RESULT
import co.nimblehq.blisskmmic.helpers.mock.NetworkMockModel
import co.nimblehq.blisskmmic.helpers.mock.NETWORK_MOCK_MODEL_RESULT
import co.nimblehq.blisskmmic.helpers.mock.NetworkMetaMockModel
import co.nimblehq.blisskmmic.helpers.mock.ktor.jsonMockEngine
import co.nimblehq.jsonapi.model.JsonApiException
import io.kotest.matchers.collections.shouldContain
Expand Down Expand Up @@ -30,6 +32,18 @@ class NetworkClientTest {
}
}

@Test
fun `when calling fetchWithMeta, it returns correct object`() = runTest {
val engine = jsonMockEngine(NETWORK_META_MOCK_MODEL_RESULT, path)
val networkClient = NetworkClient(engine = engine)
networkClient
.fetchWithMeta<NetworkMockModel, NetworkMetaMockModel>(request)
.collect {
it.first.title shouldBe "Hello"
it.second.page shouldBe 1
}
}

@Test
fun `when calling fetch with invalid path, it returns correct object`() = runTest {
val engine = jsonMockEngine(NETWORK_MOCK_MODEL_RESULT, "")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package co.nimblehq.blisskmmic.data.network.core

import co.nimblehq.blisskmmic.data.database.datasource.LocalDataSource
import co.nimblehq.blisskmmic.data.database.datasource.MockLocalDataSource
import co.nimblehq.blisskmmic.data.database.model.TokenDatabaseModel
import co.nimblehq.blisskmmic.helpers.mock.NETWORK_META_MOCK_MODEL_RESULT
import co.nimblehq.blisskmmic.helpers.mock.NETWORK_MOCK_MODEL_RESULT
import co.nimblehq.blisskmmic.helpers.mock.NetworkMetaMockModel
import co.nimblehq.blisskmmic.helpers.mock.NetworkMockModel
import co.nimblehq.blisskmmic.helpers.mock.ktor.jsonTokenizedMockEngine
import co.nimblehq.jsonapi.model.JsonApiException
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.shouldBe
import io.ktor.client.request.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.kodein.mock.Mocker
import org.kodein.mock.UsesMocks
import kotlin.test.Test
import kotlin.test.assertTrue
import kotlin.test.fail

@ExperimentalCoroutinesApi
@UsesMocks(LocalDataSource::class)
class TokenizedNetworkClientTest {

val token = TokenDatabaseModel(
"Access",
"",
1,
"",
1
)
val path = "/user"
val request = HttpRequestBuilder(path = path)

@Test
fun `when calling fetch, it returns correct object`() = runTest {
val mocker = Mocker()
val localDataSource = MockLocalDataSource(mocker)
mocker.every {
localDataSource.getToken()
} returns flow { emit(token) }
val engine = jsonTokenizedMockEngine(
NETWORK_MOCK_MODEL_RESULT,
token.accessToken,
path
)
val networkClient = TokenizedNetworkClient(engine = engine, localDataSource)
networkClient
.fetch<NetworkMockModel>(request)
.collect {
it.title shouldBe "Hello"
}
}

@Test
fun `when calling fetchWithMeta, it returns correct object`() = runTest {
val mocker = Mocker()
val localDataSource = MockLocalDataSource(mocker)
mocker.every {
localDataSource.getToken()
} returns flow { emit(token) }
val engine = jsonTokenizedMockEngine(
NETWORK_META_MOCK_MODEL_RESULT,
token.accessToken,
path
)
val networkClient = TokenizedNetworkClient(engine = engine, localDataSource)
networkClient
.fetchWithMeta<NetworkMockModel, NetworkMetaMockModel>(request)
.collect {
it.first.title shouldBe "Hello"
it.second.page shouldBe 1
}
}

@Test
fun `when calling fetch with incorrect token, it returns correct error`() = runTest {
val mocker = Mocker()
val localDataSource = MockLocalDataSource(mocker)
mocker.every {
localDataSource.getToken()
} returns flow { emit(token) }
val engine = jsonTokenizedMockEngine(
NETWORK_MOCK_MODEL_RESULT,
"no access",
path
)
val networkClient = TokenizedNetworkClient(engine = engine, localDataSource)
networkClient
.fetch<NetworkMockModel>(request)
.catch { error ->
when(error) {
is JsonApiException -> error.errors.map { it.code } shouldContain "unauthorized"
else -> fail("Should not return incorrect error type")
}
}
.collect {
fail("Should not return object")
}
}
}
Loading

0 comments on commit daf1064

Please sign in to comment.