Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
28 changes: 17 additions & 11 deletions src/jvmMain/kotlin/dev/sargunv/pokekotlin/client/PokeApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
21 changes: 7 additions & 14 deletions src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/EndpointTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -39,19 +35,16 @@ class EndpointTest {
expectedSingleResources.map { it + "List" }.toSet() + "PokemonEncounterList"

// use reflection to determine the actual resources in the client

val actualResources =
PokeApi::class
.declaredMemberFunctions
.map { it.name.removePrefix("get") }
.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())
}
Expand Down
81 changes: 33 additions & 48 deletions src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/MockServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonObject>(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<JsonObject>(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)
}
}