diff --git a/build.gradle.kts b/build.gradle.kts index 2aa8fb71..c787923a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,7 +43,7 @@ kotlin { implementation(kotlin("test")) implementation(kotlin("reflect")) implementation(libs.kotlinx.coroutines.test) - implementation(libs.okhttp.mockwebserver) + implementation(libs.ktor.client.mock) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aae78200..371bdb51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,12 +19,10 @@ tool-prettier = "3.5.3" kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktorfit = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "gradle-ktorfit" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -# Deprecated -okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.8" } - [plugins] dokka = { id = "org.jetbrains.dokka", version.ref = "gradle-dokka" } jgitver = { id = "fr.brouillard.oss.gradle.jgitver", version.ref = "gradle-jgitver" } diff --git a/src/jvmMain/kotlin/dev/sargunv/pokekotlin/client/PokeApiClient.kt b/src/jvmMain/kotlin/dev/sargunv/pokekotlin/client/PokeApiClient.kt index a2acfe89..48e402b0 100644 --- a/src/jvmMain/kotlin/dev/sargunv/pokekotlin/client/PokeApiClient.kt +++ b/src/jvmMain/kotlin/dev/sargunv/pokekotlin/client/PokeApiClient.kt @@ -3,22 +3,28 @@ package dev.sargunv.pokekotlin.client import de.jensklingenberg.ktorfit.Ktorfit.Builder import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json class PokeApiClient( baseUrl: String = "https://pokeapi.co/api/v2/", + engine: HttpClientEngine? = null, configure: HttpClientConfig<*>.() -> Unit = {}, ) : - PokeApi by (Builder() - .apply { - baseUrl(baseUrl) - httpClient( - HttpClient { - install(ContentNegotiation) { json(PokeApiJson) } - configure() - } - ) + PokeApi by (run { + fun HttpClientConfig<*>.fullyConfigure() { + install(ContentNegotiation) { json(PokeApiJson) } + configure() } - .build() - .createPokeApi()) + Builder() + .apply { + baseUrl(baseUrl) + httpClient( + if (engine != null) HttpClient(engine) { fullyConfigure() } + else HttpClient { fullyConfigure() } + ) + } + .build() + .createPokeApi() + }) diff --git a/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/EndpointTest.kt b/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/EndpointTest.kt index 7935f1de..371ebcf3 100644 --- a/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/EndpointTest.kt +++ b/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/EndpointTest.kt @@ -2,27 +2,23 @@ package dev.sargunv.pokekotlin.test import dev.sargunv.pokekotlin.client.PokeApi import dev.sargunv.pokekotlin.client.PokeApiJson +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText import kotlin.collections.HashMap import kotlin.reflect.full.declaredMemberFunctions import kotlin.test.Test import kotlin.test.assertEquals -import okhttp3.OkHttpClient -import okhttp3.Request +import kotlinx.coroutines.test.runTest class EndpointTest { - private val httpClient = OkHttpClient() + private val httpClient = HttpClient() @Test - fun checkAllEndpoints() { + fun checkAllEndpoints() = runTest { // call the mock API to get a list of resource endpoints - - val json = - httpClient - .newCall(Request.Builder().get().url(MockServer.url).build()) - .execute() - .body()!! - .string() + val json = httpClient.get("https://pokeapi.co/api/v2/").bodyAsText() // parse the expected resources using the list val expectedSingleResources = @@ -39,7 +35,6 @@ class EndpointTest { expectedSingleResources.map { it + "List" }.toSet() + "PokemonEncounterList" // use reflection to determine the actual resources in the client - val actualResources = PokeApi::class .declaredMemberFunctions @@ -47,11 +42,9 @@ class EndpointTest { .groupBy { it.endsWith("List") } val actualSingleResources = actualResources.getValue(false).toSet() - val actualListResources = actualResources.getValue(true).toSet() // make sure the resources in the client match the ones in the API - assertEquals(expectedSingleResources.sorted(), actualSingleResources.sorted()) assertEquals(expectedListResources.sorted(), actualListResources.sorted()) } diff --git a/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/MockServer.kt b/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/MockServer.kt index 7eecf2b3..e2ca52a0 100644 --- a/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/MockServer.kt +++ b/src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/MockServer.kt @@ -2,64 +2,49 @@ package dev.sargunv.pokekotlin.test import dev.sargunv.pokekotlin.client.PokeApiClient import dev.sargunv.pokekotlin.client.PokeApiJson +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.MockRequestHandleScope +import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.respondError +import io.ktor.client.request.HttpRequestData +import io.ktor.client.request.HttpResponseData +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf import java.io.File import java.io.FileReader -import java.nio.charset.Charset import java.nio.file.Paths -import java.util.logging.Level -import java.util.logging.LogManager import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonArray -import okhttp3.mockwebserver.Dispatcher -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest -import okio.Buffer object MockServer { + val mockEngine = MockEngine { request -> dispatch(request) } + val client = PokeApiClient(engine = mockEngine) + + private val sampleArchivePath = Paths.get(MockServer::class.java.getResource("/data")!!.toURI()) + + private fun limit(text: String, limit: Int): String { + val fullObj = PokeApiJson.decodeFromString(text) + val fullResults = fullObj["results"]!!.jsonArray + val newResults = buildJsonArray { fullResults.take(limit).forEach { add(it) } } + val newObj = buildJsonObject { + fullObj.entries.forEach { (key, value) -> put(key = key, element = value) } + put(key = "results", element = newResults) + if (fullResults.size > limit) put(key = "next", element = JsonPrimitive("DUMMY")) + } + return PokeApiJson.encodeToString(newObj) + } - private val server = MockWebServer() - - val url = server.url("/api/v2/")!! - val client = PokeApiClient(url.url().toString()) - - init { - // disable MockWebServer logging - LogManager.getLogManager().getLogger(MockWebServer::class.qualifiedName).level = Level.OFF - - // get the path to the sample API responses archive - val sampleArchivePath = Paths.get(MockServer::class.java.getResource("/data")!!.toURI()) - - // set up the dispatcher to use files in the archive as the mock responses - server.dispatcher = - object : Dispatcher() { - private fun limit(text: String, limit: Int): String { - val fullObj = PokeApiJson.decodeFromString(text) - val fullResults = fullObj["results"]!!.jsonArray - val newResults = buildJsonArray { fullResults.take(limit).forEach { add(it) } } - val newObj = buildJsonObject { - fullObj.entries.forEach { (key, value) -> put(key, value) } - put("results", newResults) - if (fullResults.size > limit) put("next", JsonPrimitive("DUMMY")) - } - return PokeApiJson.encodeToString(newObj) - } - - override fun dispatch(request: RecordedRequest): MockResponse { - val basePath = request.path.dropLastWhile { it != '/' } - val limit = server.url(request.path).queryParameter("limit")?.toInt() - val file = File(sampleArchivePath.toString() + basePath + "index.json") - return if (file.exists()) { - var text = FileReader(file).use { it.readText() } - if (limit != null) text = limit(text, limit) - MockResponse() - .setHeader("content-type", "application/json") - .setBody(Buffer().writeString(text, Charset.defaultCharset())) - } else MockResponse().setResponseCode(404) - } - } + private fun MockRequestHandleScope.dispatch(request: HttpRequestData): HttpResponseData { + val basePath = request.url.encodedPath.dropLastWhile { it != '/' } + val limit = request.url.parameters["limit"]?.toInt() + val file = File(sampleArchivePath.toString() + basePath + "index.json") + return if (file.exists()) { + val text = FileReader(file).use { it.readText() } + val content = if (limit != null) limit(text, limit) else text + respond(content = content, headers = headersOf("content-type", "application/json")) + } else respondError(HttpStatusCode.NotFound) } }