diff --git a/build.gradle.kts b/build.gradle.kts index 3ea7e5c..e561ef7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "2.0.0" + kotlin("jvm") version "2.2.21" id("io.github.gradle-nexus.publish-plugin") version "1.1.0" } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 43aadfe..386b229 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,10 +1,11 @@ import com.vanniktech.maven.publish.JavadocJar import com.vanniktech.maven.publish.KotlinJvm +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") - kotlin("plugin.serialization") version "2.0.0" + kotlin("plugin.serialization") version "2.2.21" id("com.vanniktech.maven.publish") version "0.34.0" id("org.jlleitschuh.gradle.ktlint") version "12.1.1" id("org.jetbrains.dokka") version "1.9.20" @@ -20,7 +21,11 @@ java { } tasks { - withType { kotlinOptions { jvmTarget = "17" } } + withType { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } } // Defined in gradle.properties diff --git a/core/src/main/kotlin/cohort/CohortApi.kt b/core/src/main/kotlin/cohort/CohortApi.kt index 311296d..8b03168 100644 --- a/core/src/main/kotlin/cohort/CohortApi.kt +++ b/core/src/main/kotlin/cohort/CohortApi.kt @@ -16,7 +16,6 @@ import io.ktor.client.request.headers import io.ktor.client.request.parameter import io.ktor.client.statement.bodyAsChannel import io.ktor.http.HttpStatusCode -import io.ktor.util.toByteArray import io.ktor.utils.io.jvm.javaio.toInputStream import kotlinx.serialization.Serializable import okio.buffer diff --git a/core/src/test/kotlin/project/ProjectApiTest.kt b/core/src/test/kotlin/project/ProjectApiTest.kt index a9653df..c022727 100644 --- a/core/src/test/kotlin/project/ProjectApiTest.kt +++ b/core/src/test/kotlin/project/ProjectApiTest.kt @@ -3,6 +3,7 @@ package project import com.amplitude.project.DeploymentsResponse import com.amplitude.project.ProjectApiV1 import com.amplitude.util.json +import com.sun.net.httpserver.HttpServer import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpMethod @@ -13,6 +14,7 @@ import kotlinx.serialization.encodeToString import org.junit.Test import test.deployment import test.toSerialDeployment +import java.net.InetSocketAddress import kotlin.test.assertEquals class ProjectApiTest { @@ -68,4 +70,28 @@ class ProjectApiTest { assertEquals("/api/1/deployments", request.url.encodedPath) assertEquals("Bearer $managementKey", request.headers["Authorization"]) } + + @Test + fun `get deployments over okhttp engine with coroutines 1_11`(): Unit = + runBlocking { + val expected = listOf(deployment("okhttp")) + val responseBody = + json.encodeToString( + DeploymentsResponse(expected.map { it.toSerialDeployment() }), + ).toByteArray() + val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + server.createContext("/api/1/deployments") { exchange -> + exchange.responseHeaders.add("Content-Type", "application/json") + exchange.sendResponseHeaders(200, responseBody.size.toLong()) + exchange.responseBody.use { it.write(responseBody) } + } + server.start() + + try { + val api = ProjectApiV1("http://127.0.0.1:${server.address.port}/", managementKey) + assertEquals(expected, api.getDeployments()) + } finally { + server.stop(0) + } + } } diff --git a/gradle.properties b/gradle.properties index 28aeefc..c1e4b3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ # kotlin kotlin.code.style=official -ktorVersion=2.3.12 -kotlinVersion=2.0.0 -coroutinesVersion=1.9.0-RC -serializationVersion=1.7.1 +ktorVersion=3.3.3 +kotlinVersion=2.2.21 +coroutinesVersion=1.11.0 +serializationVersion=1.9.0 # logging & metrics logbackVersion=1.4.6 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cb..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +134,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 6689b85..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/service/build.gradle.kts b/service/build.gradle.kts index dbfb997..ef92394 100644 --- a/service/build.gradle.kts +++ b/service/build.gradle.kts @@ -1,10 +1,11 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { application - id("io.ktor.plugin") version "2.3.4" - kotlin("jvm") version "2.0.0" - kotlin("plugin.serialization") version "2.0.0" + id("io.ktor.plugin") version "3.3.3" + kotlin("jvm") version "2.2.21" + kotlin("plugin.serialization") version "2.2.21" id("org.jlleitschuh.gradle.ktlint") version "12.1.1" } @@ -20,7 +21,11 @@ java { } tasks { - withType { kotlinOptions { jvmTarget = "17" } } + withType { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } } // Defined in gradle.properties @@ -44,6 +49,6 @@ dependencies { implementation("io.ktor:ktor-client-okhttp:$ktorVersion") implementation("io.micrometer:micrometer-registry-prometheus:$prometheusVersion") implementation("ch.qos.logback:logback-classic:$logbackVersion") - testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion") + testImplementation("io.ktor:ktor-server-test-host-jvm:$ktorVersion") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion") } diff --git a/service/src/main/kotlin/Server.kt b/service/src/main/kotlin/Server.kt index 200a9c0..4b9838b 100644 --- a/service/src/main/kotlin/Server.kt +++ b/service/src/main/kotlin/Server.kt @@ -19,6 +19,7 @@ import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.request.ApplicationRequest +import io.ktor.server.request.receiveText import io.ktor.server.request.uri import io.ktor.server.response.respond import io.ktor.server.response.respondBytes @@ -26,7 +27,6 @@ import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.routing import io.ktor.util.decodeBase64String -import io.ktor.util.toByteArray import io.micrometer.prometheus.PrometheusConfig import io.micrometer.prometheus.PrometheusMeterRegistry import kotlinx.coroutines.runBlocking @@ -121,7 +121,7 @@ fun Application.proxyServer(evaluationProxy: EvaluationProxy) { } install( createApplicationPlugin("shutdown") { - val plugin = ShutDownUrl("/shutdown") { 0 } + val plugin = ShutDownUrl("/shutdown", exitCode = { 0 }) onCall { call -> if (call.request.uri == plugin.url) { evaluationProxy.shutdown() @@ -314,8 +314,9 @@ private fun ApplicationRequest.getUserFromHeader(): Map { /** * Get the user from the body. Used for SDK/REST POST requests. */ -private suspend fun ApplicationRequest.getUserFromBody(): Map { - val userJson = this.receiveChannel().toByteArray().toString(Charsets.UTF_8) +@VisibleForTesting +internal suspend fun ApplicationRequest.getUserFromBody(): Map { + val userJson = call.receiveText() return json.decodeFromString(userJson).toAnyMap() } diff --git a/service/src/main/kotlin/plugins/Monitoring.kt b/service/src/main/kotlin/plugins/Monitoring.kt index c94e43b..5ed7e71 100644 --- a/service/src/main/kotlin/plugins/Monitoring.kt +++ b/service/src/main/kotlin/plugins/Monitoring.kt @@ -36,7 +36,7 @@ import io.ktor.server.application.Application import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.metrics.micrometer.MicrometerMetrics -import io.ktor.server.plugins.callloging.CallLogging +import io.ktor.server.plugins.calllogging.CallLogging import io.ktor.server.request.path import io.ktor.server.routing.get import io.micrometer.prometheus.PrometheusMeterRegistry diff --git a/service/src/test/kotlin/ServerTest.kt b/service/src/test/kotlin/ServerTest.kt index 918eb7a..9105daa 100644 --- a/service/src/test/kotlin/ServerTest.kt +++ b/service/src/test/kotlin/ServerTest.kt @@ -1,5 +1,14 @@ import com.amplitude.getApiAndSecretKey +import com.amplitude.getUserFromBody +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText import io.ktor.http.Headers +import io.ktor.server.application.call +import io.ktor.server.response.respondText +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.testing.testApplication import io.ktor.util.encodeBase64 import kotlin.test.Test import kotlin.test.assertEquals @@ -18,4 +27,23 @@ class ServerTest { assertEquals(apiKey, result.first) assertEquals(secretKey, result.second) } + + @Test + fun `test get user from body`() = + testApplication { + application { + routing { + post("/") { + val user = call.request.getUserFromBody() + call.respondText(user["user_id"].toString()) + } + } + } + + val response = + client.post("/") { + setBody("""{"user_id":"test-user"}""") + } + assertEquals("test-user", response.bodyAsText()) + } }