diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 017a601a..139217bd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,6 +22,7 @@ plugins {
alias(libs.plugins.kotlin.compose.compiler)
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.kotlin.serialization)
}
val appConfig = AppConfig()
@@ -99,7 +100,6 @@ dependencies {
implementation(libs.ktor.client.content.negotiation)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.serialization.kotlinx.json)
- implementation(libs.converter.gson)
// Compose
// @see: https://developer.android.google.cn/develop/ui/compose/setup?hl=en#kotlin_1
@@ -130,6 +130,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.robolectric)
+ testImplementation(libs.ktor.client.mock)
// UI tests dependencies
androidTestImplementation(composeBom)
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/AllFeatures.kt b/app/src/main/kotlin/com/fernandocejas/sample/AllFeatures.kt
new file mode 100644
index 00000000..c1d1d38c
--- /dev/null
+++ b/app/src/main/kotlin/com/fernandocejas/sample/AllFeatures.kt
@@ -0,0 +1,15 @@
+package com.fernandocejas.sample
+
+import com.fernandocejas.sample.core.navigation.navigationFeature
+import com.fernandocejas.sample.core.network.networkFeature
+import com.fernandocejas.sample.features.auth.authFeature
+import com.fernandocejas.sample.features.login.loginFeature
+import com.fernandocejas.sample.features.movies.di.moviesFeature
+
+fun allFeatures() = listOf(
+ networkFeature(),
+ authFeature(),
+ loginFeature(),
+ moviesFeature(),
+ navigationFeature(),
+)
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/AndroidApplication.kt b/app/src/main/kotlin/com/fernandocejas/sample/AndroidApplication.kt
index 2ca8aa35..7c05915b 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/AndroidApplication.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/AndroidApplication.kt
@@ -16,8 +16,6 @@
package com.fernandocejas.sample
import android.app.Application
-import com.fernandocejas.sample.core.allFeatures
-import com.fernandocejas.sample.core.di.coreModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/di/CoreModule.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/di/CoreModule.kt
deleted file mode 100644
index 3f434298..00000000
--- a/app/src/main/kotlin/com/fernandocejas/sample/core/di/CoreModule.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.fernandocejas.sample.core.di
-
-import com.fernandocejas.sample.core.navigation.Navigator
-import com.fernandocejas.sample.core.network.NetworkHandler
-import okhttp3.OkHttpClient
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
-
-val coreModule = module {
- singleOf(::retrofit)
- singleOf(::NetworkHandler)
- singleOf(::Navigator)
-}
-
-private fun retrofit(): Retrofit {
- return Retrofit.Builder()
- .baseUrl("https://raw.githubusercontent.com/android10/Sample-Data/master/Android-CleanArchitecture-Kotlin/")
- .client(OkHttpClient.Builder().build())
- .addConverterFactory(GsonConverterFactory.create())
- .build()
-}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/Core.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/di/Feature.kt
similarity index 64%
rename from app/src/main/kotlin/com/fernandocejas/sample/core/Core.kt
rename to app/src/main/kotlin/com/fernandocejas/sample/core/di/Feature.kt
index 5b61f028..99f196e6 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/core/Core.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/core/di/Feature.kt
@@ -1,9 +1,5 @@
-package com.fernandocejas.sample.core
+package com.fernandocejas.sample.core.di
-import com.fernandocejas.sample.core.di.coreModule
-import com.fernandocejas.sample.features.auth.authFeature
-import com.fernandocejas.sample.features.login.loginFeature
-import com.fernandocejas.sample.features.movies.moviesFeature
import org.koin.core.module.Module
/**
@@ -45,15 +41,3 @@ interface Feature {
*/
// fun databaseTables(): List
= emptyList()
}
-
-private fun coreFeature() = object : Feature {
- override fun name() = "core"
- override fun diModule() = coreModule
-}
-
-fun allFeatures() = listOf(
- coreFeature(),
- authFeature(),
- loginFeature(),
- moviesFeature(),
-)
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/navigation/Navigator.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/navigation/Navigator.kt
index 8e63a506..b60863da 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/core/navigation/Navigator.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/core/navigation/Navigator.kt
@@ -21,10 +21,14 @@ import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.fragment.app.FragmentActivity
+import com.fernandocejas.sample.core.di.Feature
import com.fernandocejas.sample.core.extension.emptyString
import com.fernandocejas.sample.features.auth.credentials.Authenticator
import com.fernandocejas.sample.features.movies.ui.MovieView
import com.fernandocejas.sample.features.movies.ui.MoviesActivity
+import org.koin.core.module.Module
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
class Navigator(private val authenticator: Authenticator) {
@@ -83,4 +87,10 @@ class Navigator(private val authenticator: Authenticator) {
class Extras(val transitionSharedElement: View)
}
-
+// temporary solution to compile till Navigator is deleted
+fun navigationFeature() = object : Feature {
+ override fun name() = "navigation"
+ override fun diModule() = module {
+ singleOf(::Navigator)
+ }
+}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/network/ApiResponse.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/network/ApiResponse.kt
new file mode 100644
index 00000000..9b7b3b68
--- /dev/null
+++ b/app/src/main/kotlin/com/fernandocejas/sample/core/network/ApiResponse.kt
@@ -0,0 +1,56 @@
+package com.fernandocejas.sample.core.network
+
+import com.fernandocejas.sample.core.functional.Either
+import com.fernandocejas.sample.core.functional.toLeft
+import com.fernandocejas.sample.core.functional.toRight
+
+sealed class ApiResponse {
+ /**
+ * Represents successful network responses (2xx).
+ */
+ data class Success(val body: T) : ApiResponse()
+
+ sealed class Error : ApiResponse() {
+ /**
+ * Represents server (50x) and client (40x) errors.
+ */
+ data class HttpError(val code: Int, val errorBody: E?) : Error()
+
+ /**
+ * Represent IOExceptions and connectivity issues.
+ */
+ data object NetworkError : Error()
+
+ /**
+ * Represent SerializationExceptions.
+ */
+ data object SerializationError : Error()
+ }
+}
+
+// Side Effect helpers
+inline fun ApiResponse.onSuccess(block: (T) -> Unit): ApiResponse {
+ if (this is ApiResponse.Success) {
+ block(body)
+ }
+ return this
+}
+
+fun ApiResponse.toEither(): Either {
+ return when (this) {
+ is ApiResponse.Success -> body.toRight()
+ is ApiResponse.Error.HttpError -> errorBody.toLeft()
+ is ApiResponse.Error.NetworkError -> null.toLeft()
+ is ApiResponse.Error.SerializationError -> null.toLeft()
+ }
+}
+
+fun ApiResponse.toEither(
+ successTransform: (T) -> D,
+ errorTransform: (ApiResponse.Error) -> F,
+): Either {
+ return when (this) {
+ is ApiResponse.Success -> successTransform(body).toRight()
+ is ApiResponse.Error -> errorTransform(this).toLeft()
+ }
+}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/network/HttpClientX.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/network/HttpClientX.kt
new file mode 100644
index 00000000..a603405b
--- /dev/null
+++ b/app/src/main/kotlin/com/fernandocejas/sample/core/network/HttpClientX.kt
@@ -0,0 +1,34 @@
+package com.fernandocejas.sample.core.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.plugins.ClientRequestException
+import io.ktor.client.plugins.ResponseException
+import io.ktor.client.plugins.ServerResponseException
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.client.request.request
+import io.ktor.serialization.JsonConvertException
+import kotlinx.io.IOException
+
+suspend inline fun HttpClient.safeRequest(
+ block: HttpRequestBuilder.() -> Unit,
+): ApiResponse =
+ try {
+ val response = request { block() }
+ ApiResponse.Success(response.body())
+ } catch (e: ClientRequestException) {
+ ApiResponse.Error.HttpError(e.response.status.value, e.errorBody())
+ } catch (e: ServerResponseException) {
+ ApiResponse.Error.HttpError(e.response.status.value, e.errorBody())
+ } catch (e: IOException) {
+ ApiResponse.Error.NetworkError
+ } catch (e: JsonConvertException) {
+ ApiResponse.Error.SerializationError
+ }
+
+suspend inline fun ResponseException.errorBody(): E? =
+ try {
+ response.body()
+ } catch (e: JsonConvertException) {
+ null
+ }
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkHandler.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkHandler.kt
index 1a257f49..929587cd 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkHandler.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkHandler.kt
@@ -17,7 +17,6 @@ package com.fernandocejas.sample.core.network
import android.content.Context
import android.net.NetworkCapabilities
-import android.os.Build
import com.fernandocejas.sample.core.extension.connectivityManager
/**
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkModule.kt b/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkModule.kt
new file mode 100644
index 00000000..1328f361
--- /dev/null
+++ b/app/src/main/kotlin/com/fernandocejas/sample/core/network/NetworkModule.kt
@@ -0,0 +1,54 @@
+package com.fernandocejas.sample.core.network
+
+import co.touchlab.kermit.Logger
+import com.fernandocejas.sample.core.di.Feature
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.cache.HttpCache
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.http.ContentType
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.json.Json
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+import io.ktor.client.plugins.logging.Logger as KtorLogger
+
+fun networkFeature() = object : Feature {
+ override fun name() = "network"
+ override fun diModule() = networkModule
+}
+
+private val networkModule = module {
+ singleOf(::NetworkHandler)
+ single { json }
+ single { client }
+}
+
+private val json = Json {
+ ignoreUnknownKeys = true
+ explicitNulls = false
+}
+
+private val client = HttpClient(OkHttp) {
+ engine {
+ config {
+ followRedirects(true)
+ }
+ }
+ install(HttpCache)
+ install(HttpTimeout)
+ install(ContentNegotiation) {
+ json(json, ContentType.Text.Plain)
+ }
+ install(Logging) {
+ logger = object : KtorLogger {
+ override fun log(message: String) {
+ Logger.withTag("HTTP").d { "\uD83C\uDF10 $message" }
+ }
+ }
+ level = LogLevel.HEADERS
+ }
+}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/auth/Auth.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/auth/Auth.kt
index 0d67816d..4bcd9d04 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/auth/Auth.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/auth/Auth.kt
@@ -1,6 +1,6 @@
package com.fernandocejas.sample.features.auth
-import com.fernandocejas.sample.core.Feature
+import com.fernandocejas.sample.core.di.Feature
import com.fernandocejas.sample.features.auth.credentials.Authenticator
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/auth/di/AuthModule.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/auth/di/AuthModule.kt
index 40b18e11..93895a9d 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/auth/di/AuthModule.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/auth/di/AuthModule.kt
@@ -1,13 +1,8 @@
package com.fernandocejas.sample.features.auth.di
-import com.fernandocejas.sample.core.navigation.Navigator
-import com.fernandocejas.sample.core.network.NetworkHandler
import com.fernandocejas.sample.features.auth.credentials.Authenticator
-import okhttp3.OkHttpClient
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
val authModule = module {
singleOf(::Authenticator)
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/login/Login.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/login/Login.kt
index 4a7e73e6..2acf5b77 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/login/Login.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/login/Login.kt
@@ -1,6 +1,6 @@
package com.fernandocejas.sample.features.login
-import com.fernandocejas.sample.core.Feature
+import com.fernandocejas.sample.core.di.Feature
import org.koin.dsl.module
fun loginFeature() = object : Feature {
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MovieEntity.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MovieEntity.kt
index 2fd1c041..64b5c90a 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MovieEntity.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MovieEntity.kt
@@ -16,7 +16,10 @@
package com.fernandocejas.sample.features.movies.data
import com.fernandocejas.sample.features.movies.interactor.Movie
+import kotlinx.serialization.Serializable
-data class MovieEntity(private val id: Int, private val poster: String) {
- fun toMovie() = Movie(id, poster)
-}
+@Serializable
+data class MovieEntity(val id: Int, val poster: String)
+
+
+fun MovieEntity.toMovie() = Movie(id, poster)
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesApi.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesApi.kt
deleted file mode 100644
index 926851e5..00000000
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesApi.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (C) 2020 Fernando Cejas Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.fernandocejas.sample.features.movies.data
-
-import retrofit2.Call
-import retrofit2.http.GET
-import retrofit2.http.Path
-
-internal interface MoviesApi {
- companion object {
- private const val PARAM_MOVIE_ID = "movieId"
- private const val MOVIES = "movies.json"
- private const val MOVIE_DETAILS = "movie_0{$PARAM_MOVIE_ID}.json"
- }
-
- @GET(MOVIES)
- fun movies(): Call>
-
- @GET(MOVIE_DETAILS)
- fun movieDetails(@Path(PARAM_MOVIE_ID) movieId: Int): Call
-}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepository.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepository.kt
index 6acab472..56775053 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepository.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepository.kt
@@ -19,57 +19,59 @@ import com.fernandocejas.sample.core.failure.Failure
import com.fernandocejas.sample.core.failure.Failure.NetworkConnection
import com.fernandocejas.sample.core.failure.Failure.ServerError
import com.fernandocejas.sample.core.functional.Either
-import com.fernandocejas.sample.core.functional.Either.Left
-import com.fernandocejas.sample.core.functional.Either.Right
+import com.fernandocejas.sample.core.functional.toLeft
+import com.fernandocejas.sample.core.network.ApiResponse
import com.fernandocejas.sample.core.network.NetworkHandler
+import com.fernandocejas.sample.core.network.toEither
import com.fernandocejas.sample.features.movies.interactor.Movie
import com.fernandocejas.sample.features.movies.interactor.MovieDetails
-import retrofit2.Call
interface MoviesRepository {
- fun movies(): Either>
- fun movieDetails(movieId: Int): Either
+ suspend fun movies(): Either>
+ suspend fun movieDetails(movieId: Int): Either
class Network(
private val networkHandler: NetworkHandler,
private val service: MoviesService
) : MoviesRepository {
- override fun movies(): Either> {
+ override suspend fun movies(): Either> {
return when (networkHandler.isNetworkAvailable()) {
- true -> request(
- service.movies(),
- { it.map { movieEntity -> movieEntity.toMovie() } },
- emptyList()
- )
- false -> Left(NetworkConnection)
- }
- }
+ true -> {
+ service.movies()
+ .toEither(
+ successTransform = { it.map { movieEntity -> movieEntity.toMovie() } },
+ errorTransform = {
+ when (it) {
+ is ApiResponse.Error.HttpError<*> -> ServerError
+ is ApiResponse.Error.NetworkError -> NetworkConnection
+ is ApiResponse.Error.SerializationError -> ServerError
+ }
+ },
+ )
+ }
- override fun movieDetails(movieId: Int): Either {
- return when (networkHandler.isNetworkAvailable()) {
- true -> request(
- service.movieDetails(movieId),
- { it.toMovieDetails() },
- MovieDetailsEntity.empty
- )
- false -> Left(NetworkConnection)
+ false -> NetworkConnection.toLeft()
}
}
- private fun request(
- call: Call,
- transform: (T) -> R,
- default: T
- ): Either {
- return try {
- val response = call.execute()
- when (response.isSuccessful) {
- true -> Right(transform((response.body() ?: default)))
- false -> Left(ServerError)
+ override suspend fun movieDetails(movieId: Int): Either {
+ return when (networkHandler.isNetworkAvailable()) {
+ true -> {
+ return service.movieDetails(movieId)
+ .toEither(
+ successTransform = { it.toMovieDetails() },
+ errorTransform = {
+ when (it) {
+ is ApiResponse.Error.HttpError<*> -> ServerError
+ is ApiResponse.Error.NetworkError -> NetworkConnection
+ is ApiResponse.Error.SerializationError -> ServerError
+ }
+ },
+ )
}
- } catch (exception: Throwable) {
- Left(ServerError)
+
+ false -> NetworkConnection.toLeft()
}
}
}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesService.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesService.kt
index 2e9ad3dc..2ec95697 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesService.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/data/MoviesService.kt
@@ -15,11 +15,32 @@
*/
package com.fernandocejas.sample.features.movies.data
-import retrofit2.Retrofit
+import com.fernandocejas.sample.core.network.ApiResponse
+import com.fernandocejas.sample.core.network.safeRequest
+import io.ktor.client.HttpClient
+import io.ktor.client.request.url
+import io.ktor.http.ContentType
+import io.ktor.http.contentType
-class MoviesService(retrofit: Retrofit) : MoviesApi {
- private val moviesApi by lazy { retrofit.create(MoviesApi::class.java) }
+class MoviesService(
+ private val httpClient: HttpClient,
+) {
- override fun movies() = moviesApi.movies()
- override fun movieDetails(movieId: Int) = moviesApi.movieDetails(movieId)
+ suspend fun movies(): ApiResponse, Unit> =
+ httpClient.safeRequest, Unit> {
+ url(BASE_URL + MOVIES)
+ contentType(ContentType.Text.Plain)
+ }
+
+ suspend fun movieDetails(movieId: Int): ApiResponse =
+ httpClient.safeRequest {
+ url(BASE_URL + "movie_0${movieId}.json")
+ contentType(ContentType.Text.Plain)
+ }
+
+ companion object {
+ private const val MOVIES = "movies.json"
+ private const val BASE_URL =
+ "https://raw.githubusercontent.com/android10/Sample-Data/master/Android-CleanArchitecture-Kotlin/"
+ }
}
diff --git a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/Movies.kt b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/di/Movies.kt
similarity index 91%
rename from app/src/main/kotlin/com/fernandocejas/sample/features/movies/Movies.kt
rename to app/src/main/kotlin/com/fernandocejas/sample/features/movies/di/Movies.kt
index 418f9108..4954fab9 100644
--- a/app/src/main/kotlin/com/fernandocejas/sample/features/movies/Movies.kt
+++ b/app/src/main/kotlin/com/fernandocejas/sample/features/movies/di/Movies.kt
@@ -1,6 +1,6 @@
-package com.fernandocejas.sample.features.movies
+package com.fernandocejas.sample.features.movies.di
-import com.fernandocejas.sample.core.Feature
+import com.fernandocejas.sample.core.di.Feature
import com.fernandocejas.sample.features.movies.data.MoviesRepository
import com.fernandocejas.sample.features.movies.data.MoviesService
import com.fernandocejas.sample.features.movies.interactor.GetMovieDetails
diff --git a/app/src/test/kotlin/com/fernandocejas/sample/core/network/HttpClientXTest.kt b/app/src/test/kotlin/com/fernandocejas/sample/core/network/HttpClientXTest.kt
new file mode 100644
index 00000000..2c4b02e0
--- /dev/null
+++ b/app/src/test/kotlin/com/fernandocejas/sample/core/network/HttpClientXTest.kt
@@ -0,0 +1,177 @@
+package com.fernandocejas.sample.core.network
+
+import io.kotest.matchers.equals.shouldBeEqual
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.mock.MockEngine
+import io.ktor.client.engine.mock.respond
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.request.url
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.contentType
+import io.ktor.http.headersOf
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.coroutines.test.runTest
+import kotlinx.io.IOException
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import org.junit.Test
+
+class HttpClientXTest {
+
+ @Test
+ fun `map to ApiResponse Success when client returns OK`() = runTest {
+ val tested = FakeApi(httpClient(okEngine))
+
+ val expected = ApiResponse.Success(FakeApi.FakeData())
+ val actual = tested.endpoint()
+
+ actual shouldBeEqual expected
+ }
+
+ @Test
+ fun `map to ApiResponse HttpError with code when client returns 40x`() = runTest {
+ val tested = FakeApi(httpClient(badRequestEngine))
+
+ val expected = ApiResponse.Error.HttpError(400, FakeApi.FakeError("bad request"))
+ val actual = tested.endpoint()
+
+ actual shouldBeEqual expected
+ }
+
+ @Test
+ fun `map to ApiResponse HttpError with code and body when client returns 50x`() = runTest {
+ val tested = FakeApi(httpClient(serverErrorEngine))
+
+ val expected = ApiResponse.Error.HttpError(500, FakeApi.FakeError("internal server error"))
+ val actual = tested.endpoint()
+
+ actual shouldBeEqual expected
+ }
+
+ @Test
+ fun `map to ApiResponse HttpError with code and null body when client returns 50x and error doesn't match contract`() =
+ runTest {
+ val tested = FakeApi(httpClient(malformedServerErrorEngine))
+
+ val expected = ApiResponse.Error.HttpError(500, null)
+ val actual = tested.endpoint()
+
+ actual shouldBeEqual expected
+ }
+
+ @Test
+ fun `map to ApiResponse NetworkError when there is no internet`() = runTest {
+ val tested = FakeApi(httpClient(networkErrorEngine))
+
+ val expected = ApiResponse.Error.NetworkError
+ val actual = tested.endpoint()
+
+ actual shouldBeEqual expected
+ }
+
+ @Test
+ fun `map to ApiResponse SerialisationError when data format doesn't match`() = runTest {
+ val tested = FakeApi(httpClient(serialisationErrorEngine))
+
+ val expected = ApiResponse.Error.SerializationError
+ val actual = tested.endpoint()
+
+ actual shouldBeEqual expected
+ }
+
+ private fun httpClient(engine: MockEngine) = HttpClient(engine) {
+ expectSuccess = true
+ install(ContentNegotiation) {
+ json(
+ Json {
+ ignoreUnknownKeys = true
+ }
+ )
+ }
+ }
+
+ private class FakeApi(
+ private val client: HttpClient,
+ ) {
+
+ suspend fun endpoint(): ApiResponse =
+ client.safeRequest {
+ url("https://movies.com/api")
+ contentType(ContentType.Application.Json)
+ }
+
+ @Serializable
+ data class FakeData(
+ val id: Int = 1,
+ val name: String = "Movie",
+ )
+
+ @Serializable
+ data class FakeError(
+ val message: String
+ )
+ }
+
+ private val headers = headersOf(HttpHeaders.ContentType, "application/json")
+
+ private val serialisationErrorEngine = MockEngine {
+ respond(
+ content = "just a string",
+ status = HttpStatusCode.OK,
+ headers = headers,
+ )
+ }
+
+ private val badRequestEngine = MockEngine {
+ respond(
+ content = """
+ {
+ "message": "bad request"
+ }
+ """.trimIndent(),
+ status = HttpStatusCode.BadRequest,
+ headers = headers,
+ )
+ }
+
+ private val serverErrorEngine = MockEngine {
+ respond(
+ content = """
+ {
+ "message": "internal server error"
+ }
+ """.trimIndent(),
+ status = HttpStatusCode.InternalServerError,
+ headers = headers,
+ )
+ }
+
+ private val malformedServerErrorEngine = MockEngine {
+ respond(
+ content = """
+ {
+ "incorrect_field_name": "error"
+ }
+ """.trimIndent(),
+ status = HttpStatusCode.InternalServerError,
+ headers = headers,
+ )
+ }
+
+ private val networkErrorEngine = MockEngine { throw IOException("No internet") }
+
+ private val okEngine = MockEngine {
+ respond(
+ content = """
+ {
+ "id": 1,
+ "name": "Movie"
+ }
+ """.trimIndent(),
+ status = HttpStatusCode.OK,
+ headers = headers,
+ )
+ }
+}
diff --git a/app/src/test/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepositoryTest.kt b/app/src/test/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepositoryTest.kt
index f9cf116f..75fd138d 100644
--- a/app/src/test/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepositoryTest.kt
+++ b/app/src/test/kotlin/com/fernandocejas/sample/features/movies/data/MoviesRepositoryTest.kt
@@ -15,184 +15,269 @@
*/
package com.fernandocejas.sample.features.movies.data
-import com.fernandocejas.sample.UnitTest
-import com.fernandocejas.sample.core.extension.empty
-import com.fernandocejas.sample.core.failure.Failure
import com.fernandocejas.sample.core.failure.Failure.NetworkConnection
import com.fernandocejas.sample.core.failure.Failure.ServerError
-import com.fernandocejas.sample.core.functional.Either
-import com.fernandocejas.sample.core.functional.Either.Right
+import com.fernandocejas.sample.core.functional.toLeft
+import com.fernandocejas.sample.core.functional.toRight
+import com.fernandocejas.sample.core.network.ApiResponse
import com.fernandocejas.sample.core.network.NetworkHandler
-import com.fernandocejas.sample.features.movies.data.MovieDetailsEntity
-import com.fernandocejas.sample.features.movies.data.MovieEntity
-import com.fernandocejas.sample.features.movies.data.MoviesRepository
import com.fernandocejas.sample.features.movies.data.MoviesRepository.Network
-import com.fernandocejas.sample.features.movies.data.MoviesService
import com.fernandocejas.sample.features.movies.interactor.Movie
import com.fernandocejas.sample.features.movies.interactor.MovieDetails
import io.kotest.matchers.equals.shouldBeEqual
-import io.kotest.matchers.should
-import io.kotest.matchers.shouldBe
-import io.kotest.matchers.types.beInstanceOf
-import io.mockk.Called
-import io.mockk.every
+import io.mockk.coEvery
import io.mockk.mockk
-import io.mockk.verify
-import org.junit.Before
+import kotlinx.coroutines.test.runTest
import org.junit.Test
-import retrofit2.Call
-import retrofit2.Response
-class MoviesRepositoryTest : UnitTest() {
-
- private lateinit var networkRepository: MoviesRepository.Network
+class MoviesRepositoryTest {
private val networkHandler: NetworkHandler = mockk()
private val service: MoviesService = mockk()
-
- private val moviesCall: Call> = mockk()
- private val moviesResponse: Response> = mockk()
- private val movieDetailsCall: Call = mockk()
- private val movieDetailsResponse: Response = mockk()
-
- @Before
- fun setUp() {
- networkRepository = Network(networkHandler, service)
- }
+ private val networkRepository = Network(networkHandler, service)
+
+// private val moviesCall: Call> = mockk()
+// private val moviesResponse: Response> = mockk()
+// private val movieDetailsCall: Call = mockk()
+// private val movieDetailsResponse: Response = mockk()
+
+// @Test
+// fun `should return empty list by default`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { moviesResponse.body() } returns null
+// every { moviesResponse.isSuccessful } returns true
+// every { moviesCall.execute() } returns moviesResponse
+// every { service.movies() } returns moviesCall
+//
+// val movies = networkRepository.movies()
+//
+// movies shouldBeEqual Right(emptyList())
+// verify(exactly = 1) { service.movies() }
+// }
@Test
- fun `should return empty list by default`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { moviesResponse.body() } returns null
- every { moviesResponse.isSuccessful } returns true
- every { moviesCall.execute() } returns moviesResponse
- every { service.movies() } returns moviesCall
+ fun `movies returns list on success when network is available`() = runTest {
+ // Given
+ val movieEntities = listOf(MovieEntity(1, "poster"))
+ coEvery { networkHandler.isNetworkAvailable() } returns true
+ coEvery { service.movies() } returns ApiResponse.Success(movieEntities)
- val movies = networkRepository.movies()
+ // When
+ val result = networkRepository.movies()
- movies shouldBeEqual Right(emptyList())
- verify(exactly = 1) { service.movies() }
+ // Then
+ result shouldBeEqual listOf(Movie(1, "poster")).toRight()
}
- @Test
- fun `should get movie list from service`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { moviesResponse.body() } returns listOf(MovieEntity(1, "poster"))
- every { moviesResponse.isSuccessful } returns true
- every { moviesCall.execute() } returns moviesResponse
- every { service.movies() } returns moviesCall
-
- val movies = networkRepository.movies()
-
- movies shouldBeEqual Right(listOf(Movie(1, "poster")))
- verify(exactly = 1) { service.movies() }
- }
+// @Test
+// fun `should get movie list from service`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { moviesResponse.body() } returns listOf(MovieEntity(1, "poster"))
+// every { moviesResponse.isSuccessful } returns true
+// every { moviesCall.execute() } returns moviesResponse
+// every { service.movies() } returns moviesCall
+//
+// val movies = networkRepository.movies()
+//
+// movies shouldBeEqual Right(listOf(Movie(1, "poster")))
+// verify(exactly = 1) { service.movies() }
+// }
@Test
- fun `movies service should return network failure when no connection`() {
- every { networkHandler.isNetworkAvailable() } returns false
+ fun `movies returns NetworkConnection when network is not available`() = runTest {
+ // Given
+ coEvery { networkHandler.isNetworkAvailable() } returns false
- val movies = networkRepository.movies()
+ // When
+ val result = networkRepository.movies()
- movies should beInstanceOf>>()
- movies.isLeft shouldBeEqual true
- movies.fold({ failure -> failure should beInstanceOf() }, {})
- verify { service wasNot Called }
+ // Then
+ result shouldBeEqual NetworkConnection.toLeft()
}
- @Test
- fun `movies service should return server error if no successful response`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { moviesResponse.isSuccessful } returns false
- every { moviesCall.execute() } returns moviesResponse
- every { service.movies() } returns moviesCall
+// @Test
+// fun `movies service should return network failure when no connection`() {
+// every { networkHandler.isNetworkAvailable() } returns false
+//
+// val movies = networkRepository.movies()
+//
+// movies should beInstanceOf>>()
+// movies.isLeft shouldBeEqual true
+// movies.fold({ failure -> failure should beInstanceOf() }, {})
+// verify { service wasNot Called }
+// }
- val movies = networkRepository.movies()
-
- movies.isLeft shouldBeEqual true
- movies.fold({ failure -> failure should beInstanceOf() }, {})
- }
@Test
- fun `movies request should catch exceptions`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { moviesCall.execute() } returns moviesResponse
- every { service.movies() } returns moviesCall
+ fun `movies returns ServerError on HTTP error`() = runTest {
+ // Given
+ coEvery { networkHandler.isNetworkAvailable() } returns true
+ coEvery { service.movies() } returns ApiResponse.Error.HttpError(500, errorBody = null)
- val movies = networkRepository.movies()
+ // When
+ val result = networkRepository.movies()
- movies.isLeft shouldBe true
- movies.fold({ failure -> failure should beInstanceOf() }, {})
+ // Then
+ result shouldBeEqual ServerError.toLeft()
}
+// @Test
+// fun `movies service should return server error if no successful response`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { moviesResponse.isSuccessful } returns false
+// every { moviesCall.execute() } returns moviesResponse
+// every { service.movies() } returns moviesCall
+//
+// val movies = networkRepository.movies()
+//
+// movies.isLeft shouldBeEqual true
+// movies.fold({ failure -> failure should beInstanceOf() }, {})
+// }
+
+// @Test
+// fun `movies request should catch exceptions`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { moviesCall.execute() } returns moviesResponse
+// every { service.movies() } returns moviesCall
+//
+// val movies = networkRepository.movies()
+//
+// movies.isLeft shouldBe true
+// movies.fold({ failure -> failure should beInstanceOf() }, {})
+// }
+
+// @Test
+// fun `should return empty movie details by default`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { movieDetailsResponse.body() } returns null
+// every { movieDetailsResponse.isSuccessful } returns true
+// every { movieDetailsCall.execute() } returns movieDetailsResponse
+// every { service.movieDetails(1) } returns movieDetailsCall
+//
+// val movieDetails = networkRepository.movieDetails(1)
+//
+// movieDetails shouldBeEqual Right(MovieDetails.empty)
+// verify(exactly = 1) { service.movieDetails(1) }
+// }
+
@Test
- fun `should return empty movie details by default`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { movieDetailsResponse.body() } returns null
- every { movieDetailsResponse.isSuccessful } returns true
- every { movieDetailsCall.execute() } returns movieDetailsResponse
- every { service.movieDetails(1) } returns movieDetailsCall
+ fun `movieDetails returns details on success when network is available`() = runTest {
+ // Given
+ val responseEntity = MovieDetailsEntity(
+ id = 1,
+ title = "title",
+ poster = "desc",
+ summary = "summary",
+ cast = "cast",
+ director = "director",
+ year = 0,
+ trailer = "trailer"
+ )
+ val expected = MovieDetails(
+ id = 1,
+ title = "title",
+ poster = "desc",
+ summary = "summary",
+ cast = "cast",
+ director = "director",
+ year = 0,
+ trailer = "trailer"
+ )
- val movieDetails = networkRepository.movieDetails(1)
+ coEvery { networkHandler.isNetworkAvailable() } returns true
+ coEvery { service.movieDetails(1) } returns ApiResponse.Success(responseEntity)
- movieDetails shouldBeEqual Right(MovieDetails.empty)
- verify(exactly = 1) { service.movieDetails(1) }
- }
+ // When
+ val result = networkRepository.movieDetails(1)
- @Test
- fun `should get movie details from service`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { movieDetailsResponse.body() } returns
- MovieDetailsEntity(8, "title", String.empty(), String.empty(),
- String.empty(), String.empty(), 0, String.empty())
- every { movieDetailsResponse.isSuccessful } returns true
- every { movieDetailsCall.execute() } returns movieDetailsResponse
- every { service.movieDetails(1) } returns movieDetailsCall
-
- val movieDetails = networkRepository.movieDetails(1)
-
- movieDetails shouldBeEqual Right(
- MovieDetails(8, "title", String.empty(),
- String.empty(), String.empty(), String.empty(), 0, String.empty())
- )
- verify(exactly = 1) { service.movieDetails(1) }
+ // Then
+ result shouldBeEqual expected.toRight()
}
+// @Test
+// fun `should get movie details from service`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { movieDetailsResponse.body() } returns
+// MovieDetailsEntity(
+// 8, "title", String.empty(), String.empty(),
+// String.empty(), String.empty(), 0, String.empty()
+// )
+// every { movieDetailsResponse.isSuccessful } returns true
+// every { movieDetailsCall.execute() } returns movieDetailsResponse
+// every { service.movieDetails(1) } returns movieDetailsCall
+//
+// val movieDetails = networkRepository.movieDetails(1)
+//
+// movieDetails shouldBeEqual Right(
+// MovieDetails(
+// 8, "title", String.empty(),
+// String.empty(), String.empty(), String.empty(), 0, String.empty()
+// )
+// )
+// verify(exactly = 1) { service.movieDetails(1) }
+// }
+
@Test
- fun `movie details service should return network failure when no connection`() {
- every { networkHandler.isNetworkAvailable() } returns false
+ fun `movieDetails returns NetworkConnection when network is not available`() = runTest {
+ // Given
+ coEvery { networkHandler.isNetworkAvailable() } returns false
- val movieDetails = networkRepository.movieDetails(1)
+ // When
+ val result = networkRepository.movieDetails(1)
- movieDetails.isLeft shouldBeEqual true
- movieDetails.fold({ failure -> failure should beInstanceOf() }, {})
- verify { service wasNot Called }
+ // Then
+ result shouldBeEqual NetworkConnection.toLeft()
}
- @Test
- fun `movie details service should return server error if no successful response`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { movieDetailsResponse.body() } returns null
- every { movieDetailsResponse.isSuccessful } returns false
- every { movieDetailsCall.execute() } returns movieDetailsResponse
- every { service.movieDetails(1) } returns movieDetailsCall
-
- val movieDetails = networkRepository.movieDetails(1)
-
- movieDetails should beInstanceOf>()
- movieDetails.isLeft shouldBeEqual true
- movieDetails.fold({ failure -> failure should beInstanceOf() }, {})
- }
+// @Test
+// fun `movie details service should return network failure when no connection`() {
+// every { networkHandler.isNetworkAvailable() } returns false
+//
+// val movieDetails = networkRepository.movieDetails(1)
+//
+// movieDetails.isLeft shouldBeEqual true
+// movieDetails.fold({ failure -> failure should beInstanceOf() }, {})
+// verify { service wasNot Called }
+// }
@Test
- fun `movie details request should catch exceptions`() {
- every { networkHandler.isNetworkAvailable() } returns true
- every { movieDetailsCall.execute() } returns movieDetailsResponse
- every { service.movieDetails(1) } returns movieDetailsCall
+ fun `movie details returns ServerError on HTTP error`() = runTest {
+ // Given
+ coEvery { networkHandler.isNetworkAvailable() } returns true
+ coEvery { service.movieDetails(1) } returns
+ ApiResponse.Error.HttpError(code = 500, errorBody = null)
- val movieDetails = networkRepository.movieDetails(1)
+ // When
+ val result = networkRepository.movieDetails(1)
- movieDetails.isLeft shouldBeEqual true
- movieDetails.fold({ failure -> failure should beInstanceOf() }, {})
+ // Then
+ result shouldBeEqual ServerError.toLeft()
}
+
+// @Test
+// fun `movie details service should return server error if no successful response`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { movieDetailsResponse.body() } returns null
+// every { movieDetailsResponse.isSuccessful } returns false
+// every { movieDetailsCall.execute() } returns movieDetailsResponse
+// every { service.movieDetails(1) } returns movieDetailsCall
+//
+// val movieDetails = networkRepository.movieDetails(1)
+//
+// movieDetails should beInstanceOf>()
+// movieDetails.isLeft shouldBeEqual true
+// movieDetails.fold({ failure -> failure should beInstanceOf() }, {})
+// }
+
+// @Test
+// fun `movie details request should catch exceptions`() {
+// every { networkHandler.isNetworkAvailable() } returns true
+// every { movieDetailsCall.execute() } returns movieDetailsResponse
+// every { service.movieDetails(1) } returns movieDetailsCall
+//
+// val movieDetails = networkRepository.movieDetails(1)
+//
+// movieDetails.isLeft shouldBeEqual true
+// movieDetails.fold({ failure -> failure should beInstanceOf() }, {})
+// }
}
diff --git a/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMovieDetailsTest.kt b/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMovieDetailsTest.kt
index d83fc62b..8312c4f1 100644
--- a/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMovieDetailsTest.kt
+++ b/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMovieDetailsTest.kt
@@ -15,33 +15,26 @@
*/
package com.fernandocejas.sample.features.movies.interactor
-import com.fernandocejas.sample.UnitTest
-import com.fernandocejas.sample.core.functional.Either.Right
+import com.fernandocejas.sample.core.functional.toRight
import com.fernandocejas.sample.features.movies.data.MoviesRepository
-import io.mockk.every
+import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.mockk
-import io.mockk.verify
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
+import kotlinx.coroutines.test.runTest
import org.junit.Test
-class GetMovieDetailsTest : UnitTest() {
-
- private lateinit var getMovieDetails: GetMovieDetails
+class GetMovieDetailsTest {
private val moviesRepository: MoviesRepository = mockk()
-
- @Before
- fun setUp() {
- getMovieDetails = GetMovieDetails(moviesRepository)
- every { moviesRepository.movieDetails(MOVIE_ID) } returns Right(MovieDetails.empty)
- }
+ private val getMovieDetails = GetMovieDetails(moviesRepository)
@Test
- fun `should get data from repository`() {
- runBlocking { getMovieDetails.run(GetMovieDetails.Params(MOVIE_ID)) }
+ fun `should get data from repository`() = runTest {
+ coEvery { moviesRepository.movieDetails(MOVIE_ID) } returns MovieDetails.empty.toRight()
+
+ getMovieDetails.run(GetMovieDetails.Params(MOVIE_ID))
- verify(exactly = 1) { moviesRepository.movieDetails(MOVIE_ID) }
+ coVerify(exactly = 1) { moviesRepository.movieDetails(MOVIE_ID) }
}
companion object {
diff --git a/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMoviesTest.kt b/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMoviesTest.kt
index 5079a98d..826b6d54 100644
--- a/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMoviesTest.kt
+++ b/app/src/test/kotlin/com/fernandocejas/sample/features/movies/interactor/GetMoviesTest.kt
@@ -15,35 +15,26 @@
*/
package com.fernandocejas.sample.features.movies.interactor
-import com.fernandocejas.sample.UnitTest
-import com.fernandocejas.sample.core.functional.Either.Right
+import com.fernandocejas.sample.core.functional.toRight
import com.fernandocejas.sample.core.interactor.UseCase
import com.fernandocejas.sample.features.movies.data.MoviesRepository
-import com.fernandocejas.sample.features.movies.interactor.GetMovies
-import com.fernandocejas.sample.features.movies.interactor.Movie
-import io.mockk.every
+import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.mockk
-import io.mockk.verify
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
+import kotlinx.coroutines.test.runTest
import org.junit.Test
-class GetMoviesTest : UnitTest() {
-
- private lateinit var getMovies: GetMovies
+class GetMoviesTest {
private val moviesRepository: MoviesRepository = mockk()
-
- @Before
- fun setUp() {
- getMovies = GetMovies(moviesRepository)
- every { moviesRepository.movies() } returns Right(listOf(Movie.empty))
- }
+ private val getMovies = GetMovies(moviesRepository)
@Test
- fun `should get data from repository`() {
- runBlocking { getMovies.run(UseCase.None()) }
+ fun `should get data from repository`() = runTest {
+ coEvery { moviesRepository.movies() } returns listOf(Movie.empty).toRight()
+
+ getMovies.run(UseCase.None())
- verify(exactly = 1) { moviesRepository.movies() }
+ coVerify(exactly = 1) { moviesRepository.movies() }
}
}
diff --git a/app/src/test/kotlin/com/fernandocejas/sample/matchers/Android.kt b/app/src/test/kotlin/com/fernandocejas/sample/matchers/Android.kt
index 438a214c..2e22724d 100644
--- a/app/src/test/kotlin/com/fernandocejas/sample/matchers/Android.kt
+++ b/app/src/test/kotlin/com/fernandocejas/sample/matchers/Android.kt
@@ -1,5 +1,6 @@
package com.fernandocejas.sample.matchers
+import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import io.kotest.matchers.string.shouldBeEqualIgnoringCase
import org.robolectric.Robolectric
@@ -8,7 +9,7 @@ import kotlin.reflect.KClass
infix fun KClass.shouldNavigateTo(
- nextActivity: KClass
+ nextActivity: KClass
): () -> Unit = {
val originActivity = Robolectric.buildActivity(this.java).get()
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 352c31e8..91210769 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,7 +14,6 @@ appCompat = "1.7.0"
activityCompose = "1.10.1"
lifecycleViewmodelCompose = "2.8.7"
koinAndroid = "3.5.6"
-converterGson = "2.9.0"
coil = "3.1.0"
kermit = "2.0.4"
kotlinxSerializationJson = "1.8.1"
@@ -57,7 +56,6 @@ androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" }
koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koinAndroid" }
@@ -80,6 +78,7 @@ kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest"
junit = { module = "junit:junit", version.ref = "junit" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
+ktor-client-mock ={ module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
# main module ui test dependencies ---
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }