diff --git a/build-image.sh b/build-image.sh index a8e49cb..f37a236 100755 --- a/build-image.sh +++ b/build-image.sh @@ -2,7 +2,7 @@ set -e -VERSION=$(grep 'version' build.gradle | cut -f 2 -d "'") +VERSION=$(grep 'version =' build.gradle.kts | cut -f 2 -d '"') if [ ! -f "build/httpbucket-$VERSION-runner" ] then diff --git a/build.gradle.kts b/build.gradle.kts index 7d0a552..00bcea8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,8 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { - kotlin("jvm") version "1.9.24" - kotlin("plugin.allopen") version "1.9.24" + kotlin("jvm") version "2.0.21" + kotlin("plugin.allopen") version "2.0.21" id("io.quarkus") } @@ -14,7 +16,11 @@ val quarkusPlatformArtifactId: String by project val quarkusPlatformVersion: String by project dependencies { - implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation( + enforcedPlatform( + "$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion", + ), + ) implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("io.smallrye.config:smallrye-config-source-file-system") implementation("io.quarkus:quarkus-micrometer-registry-prometheus") @@ -33,7 +39,7 @@ dependencies { } group = "com.testainers" -version = "0.1.1" +version = "0.1.2" java { sourceCompatibility = JavaVersion.VERSION_21 @@ -43,7 +49,7 @@ java { tasks.withType { systemProperty( "java.util.logging.manager", - "org.jboss.logmanager.LogManager" + "org.jboss.logmanager.LogManager", ) testLogging { @@ -52,7 +58,7 @@ tasks.withType { "SKIPPED", "FAILED", "STANDARD_OUT", - "STANDARD_ERROR" + "STANDARD_ERROR", ) } } @@ -64,7 +70,9 @@ allOpen { annotation("io.quarkus.test.junit.QuarkusTest") } -tasks.withType { - kotlinOptions.jvmTarget = JavaVersion.VERSION_21.toString() - kotlinOptions.javaParameters = true +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + javaParameters = true + } } diff --git a/gradle.properties b/gradle.properties index b2d908d..d5994ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ #Gradle properties quarkusPluginId=io.quarkus -quarkusPluginVersion=3.11.0 +quarkusPluginVersion=3.18.3 quarkusPlatformGroupId=io.quarkus.platform quarkusPlatformArtifactId=quarkus-bom -quarkusPlatformVersion=3.11.0 +quarkusPlatformVersion=3.18.3 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48c0a02..19cfad9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index fa9be2a..1093a7f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,4 +10,4 @@ pluginManagement { id(quarkusPluginId) version quarkusPluginVersion } } -rootProject.name="httpbucket" +rootProject.name = "httpbucket" diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm index 12619b7..89041cb 100644 --- a/src/main/docker/Dockerfile.jvm +++ b/src/main/docker/Dockerfile.jvm @@ -77,7 +77,7 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-21:1.18 +FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 ENV LANGUAGE='en_US:en' @@ -88,7 +88,9 @@ COPY --chown=185 build/quarkus-app/app/ /deployments/app/ COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ EXPOSE 8080 + USER 185 + ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar index 35f4f5b..8a2f8c3 100644 --- a/src/main/docker/Dockerfile.legacy-jar +++ b/src/main/docker/Dockerfile.legacy-jar @@ -77,16 +77,17 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-21:1.18 +FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 ENV LANGUAGE='en_US:en' - COPY build/lib/* /deployments/lib/ COPY build/*-runner.jar /deployments/quarkus-run.jar EXPOSE 8080 + USER 185 + ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native index aec7401..2072720 100644 --- a/src/main/docker/Dockerfile.native +++ b/src/main/docker/Dockerfile.native @@ -14,14 +14,18 @@ # docker run -i --rm -p 8080:8080 quarkus/httpbucket # ### -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 + WORKDIR /work/ + RUN chown 1001 /work \ && chmod "g+rwX" /work \ && chown 1001:root /work + COPY --chown=1001:root build/*-runner /work/application EXPOSE 8080 + USER 1001 ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro index 8585bfb..dfcbab3 100644 --- a/src/main/docker/Dockerfile.native-micro +++ b/src/main/docker/Dockerfile.native-micro @@ -18,7 +18,9 @@ # ### FROM quay.io/quarkus/quarkus-micro-image:2.0 + WORKDIR /work/ + RUN chown 1001 /work \ && chmod "g+rwX" /work \ && chown 1001:root /work diff --git a/src/main/kotlin/com/testainers/BasicAuthResource.kt b/src/main/kotlin/com/testainers/BasicAuthResource.kt index f26bd13..dcf06f2 100644 --- a/src/main/kotlin/com/testainers/BasicAuthResource.kt +++ b/src/main/kotlin/com/testainers/BasicAuthResource.kt @@ -64,6 +64,13 @@ class BasicAuthResource( body: Any?, ): Response = getResponse(auth, user, pass, body) + @HEAD + fun head( + @RestHeader(HttpHeaders.AUTHORIZATION) auth: String?, + user: String, + pass: String, + ): Response = getResponse(auth, user, pass, null) + private fun getResponse( auth: String?, user: String, diff --git a/src/main/kotlin/com/testainers/DelayResource.kt b/src/main/kotlin/com/testainers/DelayResource.kt index 724f2b4..2610b6d 100644 --- a/src/main/kotlin/com/testainers/DelayResource.kt +++ b/src/main/kotlin/com/testainers/DelayResource.kt @@ -66,6 +66,14 @@ class DelayResource( body: Any?, ): Response = internal(delay, body) + @HEAD + fun head( + @Parameter( + description = "Delay must be between 0 and 10 seconds.", + schema = Schema(minimum = "0", maximum = "10", defaultValue = "10"), + ) delay: Int, + ): Response = internal(delay, null) + private fun internal( delay: Int, body: Any?, diff --git a/src/main/kotlin/com/testainers/RedirectResource.kt b/src/main/kotlin/com/testainers/RedirectResource.kt index e32e577..125b1ef 100644 --- a/src/main/kotlin/com/testainers/RedirectResource.kt +++ b/src/main/kotlin/com/testainers/RedirectResource.kt @@ -82,6 +82,18 @@ class RedirectResource { ) @QueryParam("code") @DefaultValue("302") code: Int, ): Response = internal(url, code) + @HEAD + fun head( + @Parameter( + description = "URL to redirect.", + required = true, + ) @QueryParam("url") @DefaultValue("") url: String, + @Parameter( + description = "Response status code.", + schema = Schema(minimum = "300", maximum = "399"), + ) @QueryParam("code") @DefaultValue("302") code: Int, + ): Response = internal(url, code) + private fun internal( url: String, code: Int, diff --git a/src/main/kotlin/com/testainers/StatusResource.kt b/src/main/kotlin/com/testainers/StatusResource.kt index d380a62..1986429 100644 --- a/src/main/kotlin/com/testainers/StatusResource.kt +++ b/src/main/kotlin/com/testainers/StatusResource.kt @@ -69,6 +69,16 @@ class StatusResource( body: Any?, ): Response = internal(code, body) + @HEAD + fun head( + @Parameter( + description = + "Code must be between 200 and 599. " + + "Informational responses (1XX) are not supported.", + schema = Schema(minimum = "200", maximum = "599"), + ) code: Int, + ): Response = internal(code, null) + private fun internal( code: Int, body: Any?, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e9ffbae..49e7a4e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,7 +18,6 @@ quarkus.http.cors.access-control-max-age=24H # quarkus.info.path=/info # -quarkus.health.openapi.included=true quarkus.smallrye-health.root-path=/health # quarkus.micrometer.export.prometheus.path=/metrics diff --git a/src/test/kotlin/com/testainers/BaseResourceTest.kt b/src/test/kotlin/com/testainers/BaseResourceTest.kt index dcdd21b..637a019 100644 --- a/src/test/kotlin/com/testainers/BaseResourceTest.kt +++ b/src/test/kotlin/com/testainers/BaseResourceTest.kt @@ -22,6 +22,7 @@ abstract class BaseResourceTest { Method.PUT, Method.DELETE, Method.PATCH, + Method.HEAD, ) @JvmStatic @@ -32,12 +33,17 @@ abstract class BaseResourceTest { argumentGenerator(listOf(null, "", " ", "a", "1.8")) @JvmStatic - fun argumentGenerator(list: List): List { + fun argumentGenerator( + list: List, + remove: List = listOf(), + ): List { val result = mutableListOf() methods.forEach { method -> - list.forEach { status -> - result.add(Arguments.of(method, status)) + if(method !in remove) { + list.forEach { status -> + result.add(Arguments.of(method, status)) + } } } diff --git a/src/test/kotlin/com/testainers/BasicAuthResourceTest.kt b/src/test/kotlin/com/testainers/BasicAuthResourceTest.kt index e06d727..c7fe1d4 100644 --- a/src/test/kotlin/com/testainers/BasicAuthResourceTest.kt +++ b/src/test/kotlin/com/testainers/BasicAuthResourceTest.kt @@ -22,22 +22,30 @@ class BasicAuthResourceTest : BaseResourceTest() { .request(method, "/basic-auth/$user/$pass") .then() .statusCode(401) - .body( - "body.body", - if (method == Method.GET) nullValue() else equalTo(body), - *bodyMatchers( - method, - mapOf( - "body.auth" to equalTo(false), - "body.user" to equalTo(user), - "body.pass" to equalTo(pass), - "body.message" to - equalTo( - "Authorization header not present.", + .apply { + if (method != Method.HEAD) { + body( + "body.body", + if (method == Method.GET) { + nullValue() + } else { + equalTo(body) + }, + *bodyMatchers( + method, + mapOf( + "body.auth" to equalTo(false), + "body.user" to equalTo(user), + "body.pass" to equalTo(pass), + "body.message" to + equalTo( + "Authorization header not present.", + ), ), - ), - ), - ) + ), + ) + } + } } @ParameterizedTest @@ -48,22 +56,30 @@ class BasicAuthResourceTest : BaseResourceTest() { .request(method, "/basic-auth/$user/$pass") .then() .statusCode(401) - .body( - "body.body", - if (method == Method.GET) nullValue() else equalTo(body), - *bodyMatchers( - method, - mapOf( - "body.auth" to equalTo(false), - "body.user" to equalTo(user), - "body.pass" to equalTo(pass), - "body.message" to - equalTo( - "Authorization header not present.", + .apply { + if (method != Method.HEAD) { + body( + "body.body", + if (method == Method.GET) { + nullValue() + } else { + equalTo(body) + }, + *bodyMatchers( + method, + mapOf( + "body.auth" to equalTo(false), + "body.user" to equalTo(user), + "body.pass" to equalTo(pass), + "body.message" to + equalTo( + "Authorization header not present.", + ), ), - ), - ), - ) + ), + ) + } + } } @ParameterizedTest @@ -76,19 +92,27 @@ class BasicAuthResourceTest : BaseResourceTest() { .request(method, "/basic-auth/$user/$pass") .then() .statusCode(200) - .body( - "body.body", - if (method == Method.GET) nullValue() else equalTo(body), - *bodyMatchers( - method, - mapOf( - "body.auth" to equalTo(true), - "body.user" to equalTo(user), - "body.pass" to equalTo(pass), - "body.message" to equalTo("Success."), - ), - ), - ) + .apply { + if (method != Method.HEAD) { + body( + "body.body", + if (method == Method.GET) { + nullValue() + } else { + equalTo(body) + }, + *bodyMatchers( + method, + mapOf( + "body.auth" to equalTo(true), + "body.user" to equalTo(user), + "body.pass" to equalTo(pass), + "body.message" to equalTo("Success."), + ), + ), + ) + } + } } @ParameterizedTest @@ -101,18 +125,26 @@ class BasicAuthResourceTest : BaseResourceTest() { .request(method, "/basic-auth/$user/$pass") .then() .statusCode(403) - .body( - "body.body", - if (method == Method.GET) nullValue() else equalTo(body), - *bodyMatchers( - method, - mapOf( - "body.auth" to equalTo(false), - "body.user" to equalTo(user), - "body.pass" to equalTo(pass), - "body.message" to equalTo("Forbidden."), - ), - ), - ) + .apply { + if (method != Method.HEAD) { + body( + "body.body", + if (method == Method.GET) { + nullValue() + } else { + equalTo(body) + }, + *bodyMatchers( + method, + mapOf( + "body.auth" to equalTo(false), + "body.user" to equalTo(user), + "body.pass" to equalTo(pass), + "body.message" to equalTo("Forbidden."), + ), + ), + ) + } + } } } diff --git a/src/test/kotlin/com/testainers/DelayResourceTest.kt b/src/test/kotlin/com/testainers/DelayResourceTest.kt index 4f2eb02..e9057b0 100644 --- a/src/test/kotlin/com/testainers/DelayResourceTest.kt +++ b/src/test/kotlin/com/testainers/DelayResourceTest.kt @@ -52,27 +52,35 @@ class DelayResourceTest : BaseResourceTest() { .request(method, "/delay/$delay") .then() .statusCode(400) - .body( - "body", - equalTo("Invalid delay: $delay"), - *bodyMatchers(method), - ) + .apply { + if (method != Method.HEAD) { + body( + "body", + equalTo("Invalid delay: $delay"), + *bodyMatchers(method), + ) + } + } } @ParameterizedTest @MethodSource("validDelay") fun valid( - it: Method, + method: Method, delay: Int, ) { - json(it) - .request(it, "/delay/$delay") + json(method) + .request(method, "/delay/$delay") .then() .statusCode(200) - .body( - "body", - equalTo("Slept for $delay seconds."), - *bodyMatchers(it), - ) + .apply { + if (method != Method.HEAD) { + body( + "body", + equalTo("Slept for $delay seconds."), + *bodyMatchers(method), + ) + } + } } } diff --git a/src/test/kotlin/com/testainers/LengthResourceTest.kt b/src/test/kotlin/com/testainers/LengthResourceTest.kt index 5fa88c9..c598928 100644 --- a/src/test/kotlin/com/testainers/LengthResourceTest.kt +++ b/src/test/kotlin/com/testainers/LengthResourceTest.kt @@ -18,15 +18,15 @@ class LengthResourceTest : BaseResourceTest() { companion object : BaseResourceTest() { @JvmStatic fun invalidLength(): List = - argumentGenerator(listOf(-1, 0, 2049)) + argumentGenerator(listOf(-1, 0, 2049), remove = listOf(Method.HEAD)) @JvmStatic fun successTextLength(): List = - argumentGenerator(listOf(1, 10)) + argumentGenerator(listOf(1, 10), remove = listOf(Method.HEAD)) @JvmStatic fun successOctetLength(): List = - argumentGenerator(listOf(512, 2048)) + argumentGenerator(listOf(512, 2048), remove = listOf(Method.HEAD)) } @ParameterizedTest diff --git a/src/test/kotlin/com/testainers/MethodsResourceTest.kt b/src/test/kotlin/com/testainers/MethodsResourceTest.kt index 14411f1..cacc875 100644 --- a/src/test/kotlin/com/testainers/MethodsResourceTest.kt +++ b/src/test/kotlin/com/testainers/MethodsResourceTest.kt @@ -3,68 +3,33 @@ package com.testainers import io.quarkus.test.junit.QuarkusTest import io.restassured.http.Method import org.hamcrest.Matchers.* -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource /** * @author Eduardo Folly */ @QuarkusTest class MethodsResourceTest : BaseResourceTest() { - @Test - fun methodGet() { - val method = Method.GET + @ParameterizedTest + @MethodSource("onlyMethods") + fun methodGet(method: Method) { json(method) .request(method, "/methods") .then() .statusCode(200) - .body("", notNullValue(), *bodyMatchers(method)) - } - - @Test - fun methodHead() { - json(Method.HEAD) - .head("/methods") - .then() - .statusCode(200) - } - - @Test - fun methodPost() { - val method = Method.POST - json(method) - .request(method, "/methods") - .then() - .statusCode(200) - .body("body", equalTo(body), *bodyMatchers(method)) - } - - @Test - fun methodPut() { - val method = Method.PUT - json(method) - .request(method, "/methods") - .then() - .statusCode(200) - .body("body", equalTo(body), *bodyMatchers(method)) - } - - @Test - fun methodPatch() { - val method = Method.PATCH - json(method) - .request(method, "/methods") - .then() - .statusCode(200) - .body("body", equalTo(body), *bodyMatchers(method)) - } - - @Test - fun methodDelete() { - val method = Method.DELETE - json(method) - .request(method, "/methods") - .then() - .statusCode(200) - .body("body", equalTo(body), *bodyMatchers(method)) + .apply { + if (method != Method.HEAD) { + body( + "body", + if (method == Method.GET) { + nullValue() + } else { + equalTo(body) + }, + *bodyMatchers(method), + ) + } + } } } diff --git a/src/test/kotlin/com/testainers/RedirectResourceTest.kt b/src/test/kotlin/com/testainers/RedirectResourceTest.kt index bb8bbd2..83ac5a0 100644 --- a/src/test/kotlin/com/testainers/RedirectResourceTest.kt +++ b/src/test/kotlin/com/testainers/RedirectResourceTest.kt @@ -49,7 +49,7 @@ class RedirectResourceTest : BaseResourceTest() { @ParameterizedTest @MethodSource("invalidRedirect") fun invalid( - it: Method, + method: Method, code: Int, ) { Given { @@ -57,18 +57,20 @@ class RedirectResourceTest : BaseResourceTest() { } When { queryParam("url", LOCATION) queryParam("code", code) - request(it, "/redirect") + request(method, "/redirect") } Then { statusCode(500) - contentType(ContentType.TEXT) - body(equalTo("Invalid status code: $code")) + if (method != Method.HEAD) { + contentType(ContentType.TEXT) + body(equalTo("Invalid status code: $code")) + } } } @ParameterizedTest @MethodSource("validRedirect") fun valid( - it: Method, + method: Method, code: String?, ) { Given { @@ -78,18 +80,20 @@ class RedirectResourceTest : BaseResourceTest() { if (!code.isNullOrBlank()) { queryParam("code", code) } - request(it, "/redirect") + request(method, "/redirect") } Then { statusCode(code?.toIntOrNull() ?: 302) header(HttpHeaders.LOCATION, LOCATION) - header(HttpHeaders.CONTENT_LENGTH, "0") + if (method != Method.HEAD) { + header(HttpHeaders.CONTENT_LENGTH, "0") + } } } @ParameterizedTest @MethodSource("requiredUrl") fun required( - it: Method, + method: Method, url: String?, ) { Given { @@ -98,18 +102,20 @@ class RedirectResourceTest : BaseResourceTest() { if (url != null) { queryParam("url", url) } - request(it, "/redirect") + request(method, "/redirect") } Then { statusCode(500) - contentType(ContentType.TEXT) - body(equalTo("URL is required")) + if (method != Method.HEAD) { + contentType(ContentType.TEXT) + body(equalTo("URL is required")) + } } } @ParameterizedTest @MethodSource("invalidUrl") fun invalid( - it: Method, + method: Method, url: String?, ) { Given { @@ -118,18 +124,20 @@ class RedirectResourceTest : BaseResourceTest() { if (url != null) { queryParam("url", url) } - request(it, "/redirect") + request(method, "/redirect") } Then { statusCode(500) - contentType(ContentType.TEXT) - body(equalTo("Invalid URL: $url")) + if (method != Method.HEAD) { + contentType(ContentType.TEXT) + body(equalTo("Invalid URL: $url")) + } } } @ParameterizedTest @MethodSource("invalidScheme") fun scheme( - it: Method, + method: Method, url: String?, ) { Given { @@ -138,11 +146,13 @@ class RedirectResourceTest : BaseResourceTest() { if (url != null) { queryParam("url", url) } - request(it, "/redirect") + request(method, "/redirect") } Then { statusCode(500) - contentType(ContentType.TEXT) - body(equalTo("Invalid URL Scheme: $url")) + if (method != Method.HEAD) { + contentType(ContentType.TEXT) + body(equalTo("Invalid URL Scheme: $url")) + } } } } diff --git a/src/test/kotlin/com/testainers/StatusResourceTest.kt b/src/test/kotlin/com/testainers/StatusResourceTest.kt index 47ead91..6d8ed20 100644 --- a/src/test/kotlin/com/testainers/StatusResourceTest.kt +++ b/src/test/kotlin/com/testainers/StatusResourceTest.kt @@ -52,11 +52,15 @@ class StatusResourceTest : BaseResourceTest() { .request(method, "/status/$status") .then() .statusCode(500) - .body( - "body", - equalTo("Unknown status code: $status"), - *bodyMatchers(method), - ) + .apply { + if (method != Method.HEAD) { + body( + "body", + equalTo("Unknown status code: $status"), + *bodyMatchers(method), + ) + } + } } @ParameterizedTest @@ -69,11 +73,17 @@ class StatusResourceTest : BaseResourceTest() { .request(method, "/status/$status") .then() .statusCode(500) - .body( - "body", - equalTo("Informational responses are not supported: $status"), - *bodyMatchers(method), - ) + .apply { + if (method != Method.HEAD) { + body( + "body", + equalTo( + "Informational responses are not supported: $status", + ), + *bodyMatchers(method), + ) + } + } } @ParameterizedTest @@ -86,11 +96,15 @@ class StatusResourceTest : BaseResourceTest() { .request(method, "/status/$status") .then() .statusCode(status) - .body( - "body", - if (method == Method.GET) not(body) else equalTo(body), - *bodyMatchers(method), - ) + .apply { + if (method != Method.HEAD) { + body( + "body", + if (method == Method.GET) not(body) else equalTo(body), + *bodyMatchers(method), + ) + } + } } @ParameterizedTest