diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8027b81e9..b7569ee84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: check-latest: true - name: Build Quarkus 3.14 run: | - git clone -b 3.14 https://github.com/quarkusio/quarkus.git && cd quarkus && ./mvnw -B -s .github/mvn-settings.xml clean install -Dquickly -Dno-test-modules -Prelocations + git clone -b 3.14 https://github.com/quarkusio/quarkus.git && cd quarkus && ./mvnw -B --no-transfer-progress -s .github/mvn-settings.xml clean install -Dquickly -Dno-test-modules -Prelocations - name: Tar Maven Repo shell: bash run: tar -I 'pigz -9' -cf maven-repo.tgz -C ~ .m2/repository @@ -57,7 +57,7 @@ jobs: run: tar -xzf maven-repo.tgz -C ~ - name: Build with Maven run: | - mvn -V -B -s .github/mvn-settings.xml verify -Dall-modules -Dvalidate-format -DskipTests -DskipITs -Dquarkus.container-image.build=false -Dquarkus.container-image.push=false + mvn -V -B --no-transfer-progress -s .github/mvn-settings.xml verify -Dall-modules -Dvalidate-format -DskipTests -DskipITs -Dquarkus.container-image.build=false -Dquarkus.container-image.push=false detect-test-suite-modules: name: Detect Modules in PR runs-on: ubuntu-latest @@ -156,7 +156,7 @@ jobs: ./quarkus-dev-cli version - name: Build with Maven run: | - mvn -fae -V -B -s .github/mvn-settings.xml clean verify -Dinclude.quarkus-cli-tests -Dts.quarkus.cli.cmd="${PWD}/quarkus-dev-cli"${{ matrix.module-mvn-args }} -am -DexcludedGroups=long-running + mvn -fae -V -B --no-transfer-progress -s .github/mvn-settings.xml clean verify -Dinclude.quarkus-cli-tests -Dts.quarkus.cli.cmd="${PWD}/quarkus-dev-cli"${{ matrix.module-mvn-args }} -am -DexcludedGroups=long-running - name: Detect flaky tests id: flaky-test-detector if: ${{ hashFiles('**/flaky-run-report.json') != '' }} @@ -210,7 +210,7 @@ jobs: ./quarkus-dev-cli version - name: Build with Maven run: | - mvn -fae -V -B -s .github/mvn-settings.xml -fae \ + mvn -fae -V -B --no-transfer-progress -s .github/mvn-settings.xml -fae \ -Dquarkus.native.native-image-xmx=5g \ -Dinclude.quarkus-cli-tests -Dts.quarkus.cli.cmd="${PWD}/quarkus-dev-cli" \ ${{ matrix.module-mvn-args }} clean verify -Dnative -am -DexcludedGroups=long-running @@ -264,7 +264,7 @@ jobs: MODULES_MAVEN_PARAM="-pl ${MODULES_ARG}" fi - mvn -B -fae -s .github/mvn-settings.xml clean verify -Dall-modules $MODULES_MAVEN_PARAM -am -DexcludedGroups=long-running + mvn -B --no-transfer-progress -fae -s .github/mvn-settings.xml clean verify -Dall-modules $MODULES_MAVEN_PARAM -am -DexcludedGroups=long-running - name: Detect flaky tests id: flaky-test-detector if: ${{ hashFiles('**/flaky-run-report.json') != '' }} diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml index 7c29b7b24..57fb4a71f 100644 --- a/.github/workflows/daily.yaml +++ b/.github/workflows/daily.yaml @@ -24,7 +24,7 @@ jobs: check-latest: true - name: Build Quarkus main run: | - git clone https://github.com/quarkusio/quarkus.git && cd quarkus && ./mvnw -B -s .github/mvn-settings.xml clean install -Dquickly -Dno-test-modules -Prelocations + git clone https://github.com/quarkusio/quarkus.git && cd quarkus && ./mvnw -B --no-transfer-progress -s .github/mvn-settings.xml clean install -Dquickly -Dno-test-modules -Prelocations - name: Tar Maven Repo shell: bash run: tar -I 'pigz -9' -cf maven-repo.tgz -C ~ .m2/repository @@ -73,7 +73,7 @@ jobs: ./quarkus-dev-cli version - name: Test in JVM mode run: | - mvn -fae -V -B -s .github/mvn-settings.xml -fae clean verify -P ${{ matrix.profiles }} -Dinclude.quarkus-cli-tests -Dts.quarkus.cli.cmd="${PWD}/quarkus-dev-cli" + mvn -fae -V -B --no-transfer-progress -s .github/mvn-settings.xml -fae clean verify -P ${{ matrix.profiles }} -Dinclude.quarkus-cli-tests -Dts.quarkus.cli.cmd="${PWD}/quarkus-dev-cli" - name: Zip Artifacts if: failure() run: | @@ -127,7 +127,7 @@ jobs: ./quarkus-dev-cli version - name: Test in Native mode run: | - mvn -fae -V -B -s .github/mvn-settings.xml -P ${{ matrix.profiles }} -fae clean verify -Dnative \ + mvn -fae -V -B --no-transfer-progress -s .github/mvn-settings.xml -P ${{ matrix.profiles }} -fae clean verify -Dnative \ -Dquarkus.native.builder-image=quay.io/quarkus/${{ matrix.image }} \ -Dquarkus.native.native-image-xmx=5g \ -Dinclude.quarkus-cli-tests -Dts.quarkus.cli.cmd="${PWD}/quarkus-dev-cli" @@ -168,7 +168,7 @@ jobs: - name: Build in JVM mode shell: bash run: | - mvn -B -fae -s .github/mvn-settings.xml clean verify + mvn -B --no-transfer-progress -fae -s .github/mvn-settings.xml clean verify - name: Zip Artifacts shell: bash if: failure() @@ -222,7 +222,7 @@ jobs: shell: bash run: | # Running only http/http-minimum as after some time, it gives disk full in Windows when running on Native. - mvn -B -fae -s .github/mvn-settings.xml clean verify -Dall-modules -Dnative -Dquarkus.native.container-build=false -pl http/http-minimum + mvn -B --no-transfer-progress -fae -s .github/mvn-settings.xml clean verify -Dall-modules -Dnative -Dquarkus.native.container-build=false -pl http/http-minimum - name: Zip Artifacts shell: bash if: failure() diff --git a/build/podman/pom.xml b/build/podman/pom.xml index 36378ceff..817c3ea1a 100644 --- a/build/podman/pom.xml +++ b/build/podman/pom.xml @@ -55,5 +55,37 @@ + + openshift + + + openshift + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + + true + + + + + diff --git a/http/grpc/src/main/java/io/quarkus/ts/http/grpc/GrpcReflectionResponse.java b/http/grpc/src/main/java/io/quarkus/ts/http/grpc/GrpcReflectionResponse.java index bb86462ca..e20a24b06 100644 --- a/http/grpc/src/main/java/io/quarkus/ts/http/grpc/GrpcReflectionResponse.java +++ b/http/grpc/src/main/java/io/quarkus/ts/http/grpc/GrpcReflectionResponse.java @@ -3,8 +3,11 @@ import java.util.List; public final class GrpcReflectionResponse { - private final int serviceCount; - private final List serviceList; + private int serviceCount; + private List serviceList; + + public GrpcReflectionResponse() { + } public GrpcReflectionResponse(int serviceCount, List serviceList) { this.serviceCount = serviceCount; @@ -19,4 +22,11 @@ public int getServiceCount() { return serviceCount; } + public void setServiceCount(int serviceCount) { + this.serviceCount = serviceCount; + } + + public void setServiceList(List serviceList) { + this.serviceList = serviceList; + } } diff --git a/http/grpc/src/main/resources/application.properties b/http/grpc/src/main/resources/application.properties index f5b226b12..39fb43832 100644 --- a/http/grpc/src/main/resources/application.properties +++ b/http/grpc/src/main/resources/application.properties @@ -9,13 +9,31 @@ quarkus.grpc.server.enable-reflection-service=true quarkus.grpc.clients.reflection-service.port=${quarkus.grpc.clients.plain.port} quarkus.grpc.clients.streaming.port=${quarkus.grpc.clients.plain.port} -%ssl.quarkus.grpc.clients.plain.ssl.trust-store=tls/ca.pem -%ssl.quarkus.grpc.clients.reflection-service.ssl.trust-store=${quarkus.grpc.clients.plain.ssl.trust-store} -%ssl.quarkus.grpc.clients.streaming.ssl.trust-store=${quarkus.grpc.clients.plain.ssl.trust-store} +%ssl.quarkus.grpc.clients.plain.ssl.trust-store=${grpc.client.ca-cert} +%ssl.quarkus.grpc.clients.reflection-service.ssl.trust-store=${grpc.client.ca-cert} +%ssl.quarkus.grpc.clients.streaming.ssl.trust-store=${grpc.client.ca-cert} # See https://github.com/quarkusio/quarkus/issues/38965 to learn, why we use these parameters %ssl.quarkus.grpc.clients.plain.port=${quarkus.http.ssl-port} -%ssl.quarkus.http.ssl.certificate.files=tls/server.pem -%ssl.quarkus.http.ssl.certificate.key-files=tls/server.key -%ssl.quarkus.grpc.server.ssl.certificate=tls/server.pem -%ssl.quarkus.grpc.server.ssl.key=tls/server.key \ No newline at end of file +%ssl.quarkus.http.ssl.certificate.files=${grpc.server.cert} +%ssl.quarkus.http.ssl.certificate.key-files=${grpc.server.key} +%ssl.quarkus.grpc.server.ssl.certificate=${grpc.server.cert} +%ssl.quarkus.grpc.server.ssl.key=${grpc.server.key} + +%mtls.quarkus.http.insecure-requests=disabled +%mtls.quarkus.grpc.server.plain-text=false +%mtls.quarkus.grpc.clients.plain.tls-configuration-name=mtls-client +%mtls.quarkus.grpc.clients.reflection-service.tls-configuration-name=mtls-client +%mtls.quarkus.grpc.clients.streaming.tls-configuration-name=mtls-client +%mtls.quarkus.grpc.clients.plain.tls.enabled=true +%mtls.quarkus.grpc.clients.reflection-service.tls.enabled=true +%mtls.quarkus.grpc.clients.streaming.tls.enabled=true +%mtls.quarkus.grpc.clients.plain.plain-text=false +%mtls.quarkus.grpc.clients.reflection-service.plain-text=false +%mtls.quarkus.grpc.clients.streaming.plain-text=false +%mtls.quarkus.grpc.clients.reflection-service.use-quarkus-grpc-client=true +%mtls.quarkus.grpc.clients.streaming.use-quarkus-grpc-client=true +%mtls.quarkus.grpc.clients.plain.port=${quarkus.http.ssl-port} +%mtls.quarkus.tls.mtls-client.key-store.pem.0.cert=${grpc.client.crt} +%mtls.quarkus.tls.mtls-client.key-store.pem.0.key=${grpc.client.key} +%mtls.quarkus.tls.mtls-client.trust-store.pem.certs=${grpc.client.ca-crt} diff --git a/http/grpc/src/main/resources/tls/ca.cnf b/http/grpc/src/main/resources/tls/ca.cnf deleted file mode 100644 index 936c6c90f..000000000 --- a/http/grpc/src/main/resources/tls/ca.cnf +++ /dev/null @@ -1,6 +0,0 @@ -[req] -req_extensions = v3_req - -[v3_req] -basicConstraints = CA:true -keyUsage = critical, keyCertSign \ No newline at end of file diff --git a/http/grpc/src/main/resources/tls/ca.pem b/http/grpc/src/main/resources/tls/ca.pem deleted file mode 100644 index bb2986a4c..000000000 --- a/http/grpc/src/main/resources/tls/ca.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgIUTbmeWmNplPK5ZjMS7M+eSFtN2YkwDQYJKoZIhvcNAQEL -BQAwUDELMAkGA1UEBhMCQ1oxDDAKBgNVBAgMA0pNSzENMAsGA1UEBwwEQnJubzEP -MA0GA1UECgwGUmVkSGF0MRMwEQYDVQQLDApRdWFya3VzLVFFMB4XDTI0MDIyNzA4 -MzA1MloXDTM0MDIyNDA4MzA1MlowUDELMAkGA1UEBhMCQ1oxDDAKBgNVBAgMA0pN -SzENMAsGA1UEBwwEQnJubzEPMA0GA1UECgwGUmVkSGF0MRMwEQYDVQQLDApRdWFy -a3VzLVFFMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/0x9/YIoueQ -ckoItGRuW8CUc9kpdS35wfxcqrlyfQbo2i4idg5V9d4EYU3ONpvVl69O97t3DnXp -XC90Mt+ActinPXs//ulxVelP5uVWo4nQILk889GHRfoIvsEKe+YZKTBxT92PJEDh -qs1eDK5OJApX6ZRhHqZRjxVPRgwOhUE47qIHflDE0wX54zDPHGtwdSJtlDWSfbRg -BLLzni50XAAPoEmHRb557yjYR6A3SfAE/Oaz++kh5HyDTrufGRQpn8R6MR2X8lJG -FDSMqoCsf9AOtGzrpq8EoJb5hwBC7ko599beXb1A3PuYqQ4XU9gGCtTs3HHx2Am4 -t9fwS5OCmwIDAQABoz8wPTAMBgNVHRMEBTADAQH/MA4GA1UdDwEB/wQEAwICBDAd -BgNVHQ4EFgQUo9s3q9xO/KVtZ4hiDyCsHmXWI+kwDQYJKoZIhvcNAQELBQADggEB -AK/G72vu1xyITXJZeoi7pyj0iDk4nn0T6Znl5BaMZ5ukjxVH7EuIAOuCOOeDiYhe -3WdwWHbxGdaZhJH9SF4CoCKNapVL1567lT0MU9JxfNWot8ipZ1J5WCHpor5HNl1D -c072uEHQ7lTlErrvMDppPt6xb7dhXSVHneXve+RbyF8spzeKG31yg5DPkSiaIP8p -qNnGw++J1Il0CQA11hYlH4wUE80atWEugTx04BHiK/HtYLQsbHKVSNw4gj7eFr// -sQW0fliWPjmXLxAUU2efU/w6vs37LCuHApaN80yxTbK3J5LiPD8kH/OWRkPEcts9 -f2lZYZzQcMQvjbaSJ/cSzM0= ------END CERTIFICATE----- diff --git a/http/grpc/src/main/resources/tls/server.key b/http/grpc/src/main/resources/tls/server.key deleted file mode 100644 index 18714e74b..000000000 --- a/http/grpc/src/main/resources/tls/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPCYGGsbW0zG6I -TMvK85BF0jnd0L1U/XovBRCi3+VmrR2JovuazuKpmBwNyThsNLsj2KBLhYL6uwuB -Kw0BeBW+yOaNB8gE7ef6MrT5qlp53X4XudmsucvdJnCSB01GC3GjElYe9rXrSuRH -yPzU8QgpBAIJwFpbij/TWFUdynHUZ8oAjatzFlDzW4CoCTgo4NTdXhg5hsMSZvB0 -bkQ/jQF0PUvGCgbVthQGvSt75A69+ns8j2kxfb8DBrm94WpcxY1hMsVXP6/KZOyc -rHlzVELAXJA0p0mHvokfQuU3xJT/Y5NbLDkq+9sh9sYjT5lw3tf0v99CrEOQgTC6 -jejADT2zAgMBAAECggEACKelGD7ZhVKXX5TyAdpCAQ+K49KVGjbqNT0juA8/JLrV -3jWn7sKU8XkcPXNPADEin7UkYd12wvAdbpfpxgx7mFs0pBTz8+RnVHrL+41kwxn1 -Xr8mni5x2PRR/GwHr3TSz/C0mFQKRu31qShOsB3ThhPRgcCLVx2i5gliwRY2VAlK -hZVmbhyC+4qF/BFNpzIIjyqNZsu8yjBP920DzX9TrQ7q0yQ5oE+Pyg5TMJPxUOiN -dNiqNwVnQ77mz54eKJBDJbx431LS4Anp585AAruMj+7imGc7zbc4ZB4CtZ7x6f45 -3eBbiBhMepZ9wKg/Ym9aBdxxpy3gapZRs40DAJFC3QKBgQDBd1yINEnJ1ZZel/iA -3rp8j/7rITz/bYIFkRqxihjRMSrdBKzTx3n3dnRKYjOh4B6CkWToaqRP3HPBVhKd -pHjdz7Oo3PDrXnZcx41FUzYB7B0qkBD6vyqM9/U88da4ERtqNOczWCSHz/AQXfhc -X5OlYA1Ed4tdLcyVqQL1/0Y7HwKBgQC9RVDFBh8vzMA5nr/gaZOwDee935Dmei7s -AJqq6rRxkZEqSf7M090DUHaBTeg1jRMtGGNn6Q6LxaGe/jrRUvNTWVtOt1VfG8xZ -r/3fM71VnhZ/m8D/rCOmNOYekKTITxNlBPGfDV9wUFFATNhyQCWQTbg4XmFzWQG7 -hEdalmk+7QKBgCMHU4+tt/Z9X5588ZeTvDw1bjhwajTtRO9xGF4w3NFzj4k5AXnO -0jyGDAQzx5l1lNCbNqQGOv3ismq9BN3aG7A9nQ/kARL8pX2i++cja9HpSFaegxSD -bFbdxl9kgjYNkuMl9P6M5QBaG+M6wG8pNvhobb6JzofudO5cDZcwwyyNAoGAWdtw -rzlq0Py6PiDaI6a8ERdo8EIVvvY/FJhs1bw8ErbzXkpnB8OF6C7pNBZSqinh8sTj -XM/OshkP1DYKopppHycLLGHpzA+cgvAE7VTZDK7TK548kKWe/yeaIOS29spkAM/K -DqMArofTK13QXN2Ld+kODuTwCx00r1vrrFxAdzkCgYACF/hzTzT0BozFDT5Ve4sX -fQBa4oZEiFOVqJ40ok3ICdqe22/38y5bQEoZ19TNmrK4nz/W+hOCpID26x/fel14 -MegjDjBfH77uwFgoI9O0uJiX4U89ZCOWmMXNH5wqqySJG+C+kF5ACONmGfaYrkPc -8sjG1e5JXF2K5PxXHlRiIw== ------END PRIVATE KEY----- diff --git a/http/grpc/src/main/resources/tls/server.pem b/http/grpc/src/main/resources/tls/server.pem deleted file mode 100644 index 2c7e4694a..000000000 --- a/http/grpc/src/main/resources/tls/server.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDOzCCAiMCFH7N97KjoaAbXGiiEZBxz9YkgRvZMA0GCSqGSIb3DQEBCwUAMFAx -CzAJBgNVBAYTAkNaMQwwCgYDVQQIDANKTUsxDTALBgNVBAcMBEJybm8xDzANBgNV -BAoMBlJlZEhhdDETMBEGA1UECwwKUXVhcmt1cy1RRTAeFw0yNDAyMjcwODMxMjda -Fw0zNDAyMjQwODMxMjdaMGQxCzAJBgNVBAYTAkNaMQwwCgYDVQQIDANKTUsxDTAL -BgNVBAcMBEJybm8xDzANBgNVBAoMBlJlZEhhdDETMBEGA1UECwwKUXVhcmt1cy1R -RTESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAjwmBhrG1tMxuiEzLyvOQRdI53dC9VP16LwUQot/lZq0diaL7ms7iqZgc -Dck4bDS7I9igS4WC+rsLgSsNAXgVvsjmjQfIBO3n+jK0+apaed1+F7nZrLnL3SZw -kgdNRgtxoxJWHva160rkR8j81PEIKQQCCcBaW4o/01hVHcpx1GfKAI2rcxZQ81uA -qAk4KODU3V4YOYbDEmbwdG5EP40BdD1LxgoG1bYUBr0re+QOvfp7PI9pMX2/Awa5 -veFqXMWNYTLFVz+vymTsnKx5c1RCwFyQNKdJh76JH0LlN8SU/2OTWyw5KvvbIfbG -I0+ZcN7X9L/fQqxDkIEwuo3owA09swIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCM -Yl1CFnKqP2LF/zZvdwiLkdaTbvefh4W0C31tLd2OaIGDo18cCr0OWAia2XD9f9f7 -dlTmDJhRT230S96/aR7FzT30OoyGFeuNq+C4M5d7lcwllKlG5zXupLl7D3l30fnf -tUxUrFWbyh/xVGKRm4J2xP5MtIGOTfXBZsxqaawEN7U2bTmsA+/1vBWXJ+W2yCfs -IoQPNH125wsDOiCvXDacn2jd+GxxZXtfv4UoZ3LZLGko5Tv4dubu1SwaC6oek2bc -5trifNC9timIoKM0mqc4hdClB6YDDQ+pRLmy545B/EwP3xMmugnTkRo12miPLtGX -TayJV9LQkMXHgn5tKTOp ------END CERTIFICATE----- diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GRPCIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GRPCIT.java index e251cf25c..029f9020b 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GRPCIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GRPCIT.java @@ -1,51 +1,55 @@ package io.quarkus.ts.http.grpc; -import static org.hamcrest.CoreMatchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Iterator; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import org.apache.http.HttpStatus; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.grpc.Channel; -import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.bootstrap.CloseableManagedChannel; import io.quarkus.ts.grpc.GreeterGrpc; import io.quarkus.ts.grpc.HelloReply; import io.quarkus.ts.grpc.HelloRequest; import io.quarkus.ts.grpc.StreamingGrpc; +import io.vertx.mutiny.ext.web.client.WebClient; public interface GRPCIT { - RestService app(); + CloseableManagedChannel getChannel(); - Channel getChannel(); + WebClient getWebClient(); @Test default void grpcClient() { - app().given().get("/http/grpc").then() - .statusCode(HttpStatus.SC_OK) - .body(is("Hello grpc")); + var response = getWebClient().get("/http/grpc").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, response.statusCode()); + assertTrue(response.bodyAsString().startsWith("Hello grpc")); } @Test default void grpcServer() throws ExecutionException, InterruptedException { - HelloRequest request = HelloRequest.newBuilder().setName("server").build(); - HelloReply response = GreeterGrpc.newFutureStub(getChannel()).sayHello(request).get(); - Assertions.assertEquals("Hello server", response.getMessage()); + try (var channel = getChannel()) { + HelloRequest request = HelloRequest.newBuilder().setName("server").build(); + HelloReply response = GreeterGrpc.newFutureStub(channel).sayHello(request).get(); + assertEquals("Hello server", response.getMessage()); + } } @Test default void serverStream() { - HelloRequest request = HelloRequest.newBuilder().setName("ServerStream").build(); - Iterator stream = StreamingGrpc.newBlockingStub(getChannel()).serverStream(request); - AtomicInteger counter = new AtomicInteger(0); - stream.forEachRemaining((reply) -> { - Assertions.assertEquals("Hello ServerStream", reply.getMessage()); - counter.incrementAndGet(); - }); - Assertions.assertEquals(GrpcStreamingService.SERVER_STREAM_MESSAGES_COUNT, counter.get()); + try (var channel = getChannel()) { + HelloRequest request = HelloRequest.newBuilder().setName("ServerStream").build(); + Iterator stream = StreamingGrpc.newBlockingStub(channel).serverStream(request); + AtomicInteger counter = new AtomicInteger(0); + stream.forEachRemaining((reply) -> { + assertEquals("Hello ServerStream", reply.getMessage()); + counter.incrementAndGet(); + }); + assertEquals(GrpcStreamingService.SERVER_STREAM_MESSAGES_COUNT, counter.get()); + } } } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMutualTlsSeparateServerIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMutualTlsSeparateServerIT.java new file mode 100644 index 000000000..2bf897f49 --- /dev/null +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcMutualTlsSeparateServerIT.java @@ -0,0 +1,79 @@ +package io.quarkus.ts.http.grpc; + +import static io.quarkus.test.security.certificate.CertificateBuilder.INSTANCE_KEY; +import static io.quarkus.test.services.Certificate.Format.PEM; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.CloseableManagedChannel; +import io.quarkus.test.bootstrap.GrpcService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.security.certificate.CertificateBuilder; +import io.quarkus.test.security.certificate.PemClientCertificate; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.Certificate.ClientCertificate; +import io.quarkus.test.services.QuarkusApplication; +import io.vertx.mutiny.ext.web.client.WebClient; + +@Tag("QUARKUS-4592") +@QuarkusScenario +public class GrpcMutualTlsSeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { + + private static final String CERT_PREFIX = "grpc-mtls-separate-server"; + private static final String CLIENT_CN_NAME = "mtls-client-name"; + private static WebClient webClient = null; + + @QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(prefix = CERT_PREFIX, clientCertificates = { + @ClientCertificate(cnAttribute = CLIENT_CN_NAME) + }, format = PEM, configureKeystore = true, configureTruststore = true, tlsConfigName = "mtls-server", configureHttpServer = true)) + static final GrpcService app = (GrpcService) new GrpcService() + .withProperty("quarkus.http.ssl.client-auth", "required") + .withProperty("quarkus.profile", "mtls") + .withProperty("grpc.client.crt", GrpcMutualTlsSeparateServerIT::getClientCert) + .withProperty("grpc.client.ca-crt", GrpcMutualTlsSeparateServerIT::getClientCaCert) + .withProperty("grpc.client.key", GrpcMutualTlsSeparateServerIT::getClientKey); + + public CloseableManagedChannel getChannel() { + return app.securedGrpcChannel(); + } + + @Override + public WebClient getWebClient() { + if (webClient == null) { + // HINT: we don't need to close HTTPS client as FW takes care of it + webClient = app.mutinyHttps(CLIENT_CN_NAME); + } + return webClient; + } + + private static String getClientCert() { + return addEscapes(getClientCertificate().certPath()); + } + + private static String getClientCaCert() { + return addEscapes(getClientCertificate().truststorePath()); + } + + private static String getClientKey() { + return addEscapes(getClientCertificate().keyPath()); + } + + private static CertificateBuilder getCertificateBuilder() { + return app.getPropertyFromContext(CertificateBuilder.INSTANCE_KEY); + } + + private static PemClientCertificate getClientCertificate() { + return (PemClientCertificate) getCertificateBuilder().findCertificateByPrefix(CERT_PREFIX) + .getClientCertificateByCn(CLIENT_CN_NAME); + } + + static String addEscapes(String path) { + if (OS.WINDOWS.isCurrentOs()) { + // TODO: move this to the FW + // back-slashes have special meaning in Cygwin etc. + return path.replace("\\", "\\\\"); + } + return path; + } +} diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcTlsSeparateServerIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcTlsSeparateServerIT.java new file mode 100644 index 000000000..132237b05 --- /dev/null +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/GrpcTlsSeparateServerIT.java @@ -0,0 +1,68 @@ +package io.quarkus.ts.http.grpc; + +import static io.quarkus.test.services.Certificate.Format.PEM; +import static io.quarkus.ts.http.grpc.GrpcMutualTlsSeparateServerIT.addEscapes; + +import org.junit.jupiter.api.AfterAll; + +import io.quarkus.test.bootstrap.CloseableManagedChannel; +import io.quarkus.test.bootstrap.GrpcService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.security.certificate.Certificate.PemCertificate; +import io.quarkus.test.security.certificate.CertificateBuilder; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.QuarkusApplication; +import io.vertx.mutiny.ext.web.client.WebClient; + +@QuarkusScenario +public class GrpcTlsSeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { + + private static final String CERT_PREFIX = "grpc-tls-separate-server"; + private static WebClient webClient = null; + + @QuarkusApplication(grpc = true, ssl = true, certificates = @Certificate(prefix = CERT_PREFIX, format = PEM, configureKeystore = true, configureTruststore = true)) + static final GrpcService app = (GrpcService) new GrpcService() + .withProperty("quarkus.profile", "ssl") + .withProperty("grpc.client.ca-cert", GrpcTlsSeparateServerIT::getClientCaCert) + .withProperty("grpc.server.cert", GrpcTlsSeparateServerIT::getServerCert) + .withProperty("grpc.server.key", GrpcTlsSeparateServerIT::getServerKey); + + public CloseableManagedChannel getChannel() { + return app.securedGrpcChannel(); + } + + @Override + public WebClient getWebClient() { + if (webClient == null) { + webClient = app.mutiny(); + } + return webClient; + } + + @AfterAll + static void afterAll() { + if (webClient != null) { + webClient.close(); + } + } + + private static String getClientCaCert() { + return addEscapes(getPemCertificate().truststorePath()); + } + + private static String getServerCert() { + return addEscapes(getPemCertificate().certPath()); + } + + private static String getServerKey() { + return addEscapes(getPemCertificate().keyPath()); + } + + private static PemCertificate getPemCertificate() { + return (PemCertificate) getCertificateBuilder().findCertificateByPrefix(CERT_PREFIX); + } + + private static CertificateBuilder getCertificateBuilder() { + return app.getPropertyFromContext(CertificateBuilder.INSTANCE_KEY); + } +} diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftExtensionGRPCIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftExtensionGRPCIT.java index 5a89f396e..42b29d3a1 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftExtensionGRPCIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftExtensionGRPCIT.java @@ -3,10 +3,10 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; -import io.grpc.Channel; -import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.bootstrap.CloseableManagedChannel; import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy; import io.quarkus.test.scenarios.OpenShiftScenario; +import io.vertx.mutiny.ext.web.client.WebClient; @Tag("use-quarkus-openshift-extension") @OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtension) @@ -14,12 +14,12 @@ public class OpenShiftExtensionGRPCIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { @Override - public RestService app() { + public CloseableManagedChannel getChannel() { return null; } @Override - public Channel getChannel() { + public WebClient getWebClient() { return null; } } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftGRPCIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftGRPCIT.java index e6be7d08d..5361f5494 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftGRPCIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/OpenShiftGRPCIT.java @@ -2,21 +2,21 @@ import org.junit.jupiter.api.Disabled; -import io.grpc.Channel; -import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.bootstrap.CloseableManagedChannel; import io.quarkus.test.scenarios.OpenShiftScenario; +import io.vertx.mutiny.ext.web.client.WebClient; @OpenShiftScenario @Disabled("https://github.com/quarkus-qe/quarkus-test-framework/issues/1052+1053") public class OpenShiftGRPCIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { @Override - public RestService app() { + public CloseableManagedChannel getChannel() { return null; } @Override - public Channel getChannel() { + public WebClient getWebClient() { return null; } } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/ReflectionHttpIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/ReflectionHttpIT.java index 2b544b547..0a59fb21d 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/ReflectionHttpIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/ReflectionHttpIT.java @@ -1,38 +1,33 @@ package io.quarkus.ts.http.grpc; import static org.apache.http.HttpStatus.SC_OK; -import static org.hamcrest.CoreMatchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; -import org.hamcrest.Matchers; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import com.google.protobuf.Descriptors; import com.google.protobuf.InvalidProtocolBufferException; -import io.grpc.Channel; import io.grpc.reflection.v1.FileDescriptorResponse; -import io.quarkus.test.bootstrap.RestService; import io.quarkus.ts.grpc.GreeterGrpc; import io.quarkus.ts.grpc.HelloWorldProto; import io.quarkus.ts.grpc.StreamingGrpc; +import io.vertx.mutiny.ext.web.client.WebClient; public interface ReflectionHttpIT { - RestService app(); - - Channel getChannel(); + WebClient getWebClient(); @Test default void testReflectionServices() { - GrpcReflectionResponse response = app().given().when().get("/http/reflection/service/info") - .then().statusCode(SC_OK).extract().response() - .jsonPath().getObject(".", GrpcReflectionResponse.class); + var httpResponse = getWebClient().get("/http/reflection/service/info").sendAndAwait(); + assertEquals(SC_OK, httpResponse.statusCode()); + GrpcReflectionResponse response = httpResponse.bodyAsJson(GrpcReflectionResponse.class); assertEquals(3, response.getServiceCount()); @@ -46,8 +41,9 @@ default void testReflectionServices() { @Test default void testReflectionMethods() throws InvalidProtocolBufferException { - byte[] responseByteArray = app().given().when().get("/http/reflection/descriptor/greeting") - .then().statusCode(SC_OK).extract().body().asByteArray(); + var httpResponse = getWebClient().get("/http/reflection/descriptor/greeting").sendAndAwait(); + assertEquals(SC_OK, httpResponse.statusCode()); + byte[] responseByteArray = httpResponse.body().getBytes(); String fileDescriptor = FileDescriptorResponse.parseFrom(responseByteArray).toString(); @@ -75,8 +71,9 @@ default void testReflectionMethods() throws InvalidProtocolBufferException { @Test @DisplayName("GRPC reflection test - check service messages types") default void testReflectionMessages() throws InvalidProtocolBufferException { - byte[] responseByteArray = app().given().when().get("/http/reflection/descriptor/greeting") - .then().statusCode(SC_OK).extract().body().asByteArray(); + var httpResponse = getWebClient().get("/http/reflection/descriptor/greeting").sendAndAwait(); + assertEquals(SC_OK, httpResponse.statusCode()); + byte[] responseByteArray = httpResponse.body().getBytes(); String fileDescriptor = FileDescriptorResponse.parseFrom(responseByteArray).toString(); var messageTypes = HelloWorldProto.getDescriptor().getMessageTypes(); @@ -90,8 +87,9 @@ default void testReflectionMessages() throws InvalidProtocolBufferException { @Test @DisplayName("GRPC reflection test - check method SayHello of Greeter service exists and then call it") default void testReflectionCallMethod() throws InvalidProtocolBufferException { - byte[] responseByteArray = app().given().when().get("/http/reflection/descriptor/greeting") - .then().statusCode(SC_OK).extract().body().asByteArray(); + var httpResponse = getWebClient().get("/http/reflection/descriptor/greeting").sendAndAwait(); + assertEquals(SC_OK, httpResponse.statusCode()); + byte[] responseByteArray = httpResponse.body().getBytes(); String fileDescriptor = FileDescriptorResponse.parseFrom(responseByteArray).toString(); @@ -114,6 +112,8 @@ default void testReflectionCallMethod() throws InvalidProtocolBufferException { assertTrue(fileDescriptor.contains("SayHello")); // Call sayHello method and compare context - app().given().when().get("/http/tester").then().statusCode(SC_OK).body(Matchers.is("Hello tester")); + httpResponse = getWebClient().get("/http/tester").sendAndAwait(); + assertEquals(SC_OK, httpResponse.statusCode()); + assertEquals("Hello tester", httpResponse.bodyAsString()); } } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/SameServerIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/SameServerIT.java index 2f288b7cc..5766f7b8a 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/SameServerIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/SameServerIT.java @@ -2,40 +2,37 @@ import org.junit.jupiter.api.AfterAll; -import io.grpc.Channel; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; +import io.quarkus.test.bootstrap.CloseableManagedChannel; import io.quarkus.test.bootstrap.GrpcService; -import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; import io.quarkus.test.services.QuarkusApplication; +import io.vertx.mutiny.ext.web.client.WebClient; @QuarkusScenario public class SameServerIT implements GRPCIT, ReflectionHttpIT, StreamingHttpIT { + private static WebClient webClient = null; + @QuarkusApplication(grpc = true) static final GrpcService app = new GrpcService(); - private static ManagedChannel channel; @Override - public Channel getChannel() { - if (channel == null) { - channel = ManagedChannelBuilder.forAddress( - app.getURI().getHost(), - app.getURI().getPort()) - .usePlaintext() - .build(); - } - return channel; + public CloseableManagedChannel getChannel() { + return app.grpcChannel(); } @Override - public RestService app() { - return app; + public WebClient getWebClient() { + if (webClient == null) { + webClient = app.mutiny(); + } + return webClient; } @AfterAll static void afterAll() { - channel.shutdown(); + if (webClient != null) { + webClient.close(); + } } } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/XeparateServerIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/SeparateServerIT.java similarity index 50% rename from http/grpc/src/test/java/io/quarkus/ts/http/grpc/XeparateServerIT.java rename to http/grpc/src/test/java/io/quarkus/ts/http/grpc/SeparateServerIT.java index 63a30477a..e9f679b12 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/XeparateServerIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/SeparateServerIT.java @@ -1,17 +1,17 @@ package io.quarkus.ts.http.grpc; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.AfterAll; -import io.grpc.Channel; +import io.quarkus.test.bootstrap.CloseableManagedChannel; import io.quarkus.test.bootstrap.GrpcService; -import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; import io.quarkus.test.services.QuarkusApplication; +import io.vertx.mutiny.ext.web.client.WebClient; @QuarkusScenario -@DisplayName("SeparateServer") -//This test should be the last, or we get complains, that the channel was not shut down before closure. This is a bug in our framework. -public class XeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { +public class SeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { + + private static WebClient webClient = null; @QuarkusApplication(grpc = true) static final GrpcService app = (GrpcService) new GrpcService() @@ -19,13 +19,22 @@ public class XeparateServerIT implements GRPCIT, StreamingHttpIT, ReflectionHttp .withProperty("quarkus.grpc.clients.plain.port", "${quarkus.grpc.server.port}"); @Override - public Channel getChannel() { + public CloseableManagedChannel getChannel() { return app.grpcChannel(); } @Override - public RestService app() { - return app; + public WebClient getWebClient() { + if (webClient == null) { + webClient = app.mutiny(); + } + return webClient; } + @AfterAll + static void afterAll() { + if (webClient != null) { + webClient.close(); + } + } } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/StreamingHttpIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/StreamingHttpIT.java index 7019e5d6f..c0832066e 100644 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/StreamingHttpIT.java +++ b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/StreamingHttpIT.java @@ -5,24 +5,18 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.quarkus.test.bootstrap.RestService; -import io.restassured.http.ContentType; -import io.restassured.response.Response; -import io.restassured.specification.RequestSpecification; +import io.vertx.core.http.HttpHeaders; +import io.vertx.mutiny.ext.web.client.WebClient; public interface StreamingHttpIT { - RestService app(); - - default RequestSpecification given() { - return app().given(); - } + WebClient getWebClient(); @Test default void serverStreaming() { - Response response = given().when().get("/http/streaming/server/ServerStreaming"); + var response = getWebClient().get("/http/streaming/server/ServerStreaming").sendAndAwait(); Assertions.assertEquals(200, response.statusCode()); - List responses = response.jsonPath().getList("."); + List responses = response.bodyAsJsonArray().getList(); Assertions.assertEquals(GrpcStreamingService.SERVER_STREAM_MESSAGES_COUNT, responses.size()); responses.forEach(message -> Assertions.assertEquals("Hello ServerStreaming", message)); } @@ -30,23 +24,21 @@ default void serverStreaming() { @Test default void clientStreaming() { List names = List.of("Alice", "Bob", "Charlie"); - Response response = given().when() - .contentType(ContentType.JSON) - .body(names) - .post("/http/streaming/client"); + var response = getWebClient().post("/http/streaming/client") + .putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json") + .sendJsonAndAwait(names); Assertions.assertEquals(200, response.statusCode()); - Assertions.assertEquals("Total names submitted: " + names.size(), response.body().asString()); + Assertions.assertEquals("Total names submitted: " + names.size(), response.bodyAsString()); } @Test default void bidirectional() { List names = List.of("Alice", "Bob", "Charlie"); - Response response = given().when() - .contentType(ContentType.JSON) - .body(names) - .post("/http/streaming/bi"); + var response = getWebClient().post("/http/streaming/bi") + .putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json") + .sendJsonAndAwait(names); Assertions.assertEquals(200, response.statusCode()); - List messages = response.jsonPath().getList("."); + var messages = response.bodyAsJsonArray().getList(); Assertions.assertEquals(names.size() + 1, messages.size()); Assertions.assertEquals("Hello: Alice;Bob;Charlie;", messages.get(names.size())); } diff --git a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/TLSIT.java b/http/grpc/src/test/java/io/quarkus/ts/http/grpc/TLSIT.java deleted file mode 100644 index bf985cd8c..000000000 --- a/http/grpc/src/test/java/io/quarkus/ts/http/grpc/TLSIT.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.quarkus.ts.http.grpc; - -import java.io.IOException; -import java.io.InputStream; - -import org.junit.jupiter.api.AfterAll; - -import io.grpc.Channel; -import io.grpc.ChannelCredentials; -import io.grpc.Grpc; -import io.grpc.ManagedChannel; -import io.grpc.TlsChannelCredentials; -import io.quarkus.test.bootstrap.GrpcService; -import io.quarkus.test.bootstrap.Protocol; -import io.quarkus.test.bootstrap.RestService; -import io.quarkus.test.scenarios.QuarkusScenario; -import io.quarkus.test.services.QuarkusApplication; -import io.restassured.specification.RequestSpecification; - -@QuarkusScenario -public class TLSIT implements GRPCIT, StreamingHttpIT, ReflectionHttpIT { - - private static ManagedChannel channel; - @QuarkusApplication(grpc = true, ssl = true) - static final GrpcService app = (GrpcService) new GrpcService() - .withProperty("quarkus.profile", "ssl"); - - public Channel getChannel() { - if (channel != null) { - return channel; - } - try (InputStream caCertificate = app.getClass().getClassLoader().getResourceAsStream("tls/ca.pem")) { - ChannelCredentials credentials = TlsChannelCredentials.newBuilder() - .trustManager(caCertificate) - .build(); - channel = Grpc.newChannelBuilderForAddress(app().getURI(Protocol.GRPC).getHost(), - app().getURI(Protocol.HTTPS).getPort(), credentials) - .build(); - return channel; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public RestService app() { - return app; - } - - @Override - public RequestSpecification given() { - return app().relaxedHttps().given(); - } - - @AfterAll - static void afterAll() { - channel.shutdown(); - } -} diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CustomHeaderResponse.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CustomHeaderResponse.java new file mode 100644 index 000000000..6db2a053e --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CustomHeaderResponse.java @@ -0,0 +1,13 @@ +package io.quarkus.ts.http.advanced.reactive; + +public class CustomHeaderResponse { + private final String content; + + public CustomHeaderResponse(String content) { + this.content = content; + } + + public String getContent() { + return content; + } +} diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/DownloadResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/DownloadResource.java new file mode 100644 index 000000000..833d6d25c --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/DownloadResource.java @@ -0,0 +1,59 @@ +package io.quarkus.ts.http.advanced.reactive; + +import java.io.File; +import java.util.UUID; + +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.smallrye.mutiny.Uni; +import io.vertx.core.file.OpenOptions; +import io.vertx.mutiny.core.Vertx; + +@Path("/download") +public class DownloadResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(DownloadResource.class); + private static final String TEST_FILE = System.getProperty("java.io.tmpdir") + + File.separator + "DownloadResource-" + UUID.randomUUID().toString() + "-test.txt"; + private static final OpenOptions READ_ONLY = new OpenOptions().setWrite(false).setCreate(false); + + @Inject + Vertx vertx; + + @POST + @Path("/create") + public Uni createFile() { + LOGGER.info("Creating test file: {}", TEST_FILE); + return vertx.fileSystem() + .createFile(TEST_FILE) + .onItem().transform(it -> Response.ok(TEST_FILE).build()); + } + + @DELETE + @Path("/delete") + public Uni deleteFile() { + LOGGER.info("Deleting test file: {}", TEST_FILE); + return vertx.fileSystem() + .delete(TEST_FILE) + .onItem().transform(it -> Response.noContent().build()); + } + + @GET + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Uni downloadFile() { + LOGGER.info("Downloading test file: {}", TEST_FILE); + return vertx.fileSystem() + .open(TEST_FILE, READ_ONLY) + .onItem().transform(it -> Response.ok(it).build()); + } +} diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/GreetingAbstractResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/GreetingAbstractResource.java new file mode 100644 index 000000000..484db1f29 --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/GreetingAbstractResource.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.http.advanced.reactive; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/greeting") +public abstract class GreetingAbstractResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public abstract String hello(); +} diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/GreetingResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/GreetingResource.java new file mode 100644 index 000000000..7d4a99884 --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/GreetingResource.java @@ -0,0 +1,8 @@ +package io.quarkus.ts.http.advanced.reactive; + +public class GreetingResource extends GreetingAbstractResource { + @Override + public String hello() { + return "Hello from Quarkus REST"; + } +} diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersMessageBodyWriter.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersMessageBodyWriter.java new file mode 100644 index 000000000..ef0659767 --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersMessageBodyWriter.java @@ -0,0 +1,29 @@ +package io.quarkus.ts.http.advanced.reactive; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class HeadersMessageBodyWriter implements MessageBodyWriter { + + @Override + public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return CustomHeaderResponse.class.isAssignableFrom(aClass) && MediaType.TEXT_PLAIN_TYPE.isCompatible(mediaType); + } + + @Override + public void writeTo(CustomHeaderResponse customHeaderResponse, Class aClass, Type type, Annotation[] annotations, + MediaType mediaType, MultivaluedMap multivaluedMap, OutputStream outputStream) + throws IOException, WebApplicationException { + final String content = "Headers response: " + customHeaderResponse.getContent(); + outputStream.write(content.getBytes()); + } +} diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersResource.java index 985646487..38544b37d 100644 --- a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersResource.java +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/HeadersResource.java @@ -2,6 +2,8 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import io.smallrye.mutiny.Uni; @@ -28,4 +30,11 @@ public Uni headersOverride() { return Uni.createFrom().item(response); } + @GET + @Path("/no-accept") + @Produces(MediaType.TEXT_PLAIN) + public Uni noAcceptHeaders() { + return Uni.createFrom().item(Response.ok(new CustomHeaderResponse("ok headers")).build()); + } + } diff --git a/http/http-advanced-reactive/src/main/resources/application.properties b/http/http-advanced-reactive/src/main/resources/application.properties index d0ff80adb..ab80dc955 100644 --- a/http/http-advanced-reactive/src/main/resources/application.properties +++ b/http/http-advanced-reactive/src/main/resources/application.properties @@ -42,6 +42,8 @@ quarkus.keycloak.policy-enforcer.paths.multipart-form-data.path=/api/multipart-f quarkus.keycloak.policy-enforcer.paths.multipart-form-data.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.hello.path=/api/hello/* quarkus.keycloak.policy-enforcer.paths.hello.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.greeting.path=/api/greeting/* +quarkus.keycloak.policy-enforcer.paths.greeting.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/* quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/* diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java index fd8f9168a..c1f582705 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java @@ -62,6 +62,7 @@ import io.quarkus.test.bootstrap.Protocol; import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.annotations.DisabledOnNative; import io.quarkus.test.scenarios.annotations.EnabledOnQuarkusVersion; import io.quarkus.test.security.certificate.CertificateBuilder; import io.restassured.http.Header; @@ -79,6 +80,7 @@ public abstract class BaseHttpAdvancedReactiveIT { private static final String ROOT_PATH = "/api"; private static final String HELLO_ENDPOINT = ROOT_PATH + "/hello"; + private static final String GREETING_ENDPOINT = ROOT_PATH + "/greeting"; private static final int TIMEOUT_SEC = 3; private static final int RETRY = 3; private static final String PASSWORD = "password"; @@ -95,6 +97,15 @@ public void httpServer() { .body("content", is("Hello, World!")); } + @Test + @DisplayName("Test Quarkus REST abstract resource with @Path") + @DisabledOnNative(reason = "https://github.com/quarkusio/quarkus/issues/42976") + public void abstractResourceWithPath() { + getApp().given().get(GREETING_ENDPOINT) + .then().statusCode(SC_OK) + .body(is("Hello from Quarkus REST")); + } + @Test @DisplayName("GRPC Server test") public void testGrpc() { diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeGrpcIntegrationReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeGrpcIntegrationReactiveIT.java index 9517aae89..53227b6e5 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeGrpcIntegrationReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeGrpcIntegrationReactiveIT.java @@ -24,7 +24,6 @@ import io.quarkus.test.bootstrap.Protocol; import io.quarkus.test.scenarios.QuarkusScenario; import io.quarkus.test.services.DevModeQuarkusApplication; -import io.quarkus.test.services.URILike; @Tag("QUARKUS-1026") @Tag("QUARKUS-1094") @@ -49,13 +48,7 @@ public class DevModeGrpcIntegrationReactiveIT { }; @DevModeQuarkusApplication(grpc = true) - static final GrpcService app = (GrpcService) new GrpcService() { - @Override - public URILike getGrpcHost() { - // TODO: make app.grpcChannel() support gRPC on same HTTP server - return super.getGrpcHost().withPort(app.getURI().getPort()); - } - } + static final GrpcService app = (GrpcService) new GrpcService() .withProperty("quarkus.oidc.enabled", "false") .withProperty("quarkus.keycloak.policy-enforcer.enable", "false") .withProperty("quarkus.keycloak.devservices.enabled", "false"); @@ -63,9 +56,11 @@ public URILike getGrpcHost() { @Test public void testGrpcAsClient() throws ExecutionException, InterruptedException { HelloRequest request = HelloRequest.newBuilder().setName(NAME).build(); - HelloReply response = GreeterGrpc.newFutureStub(app.grpcChannel()).sayHello(request).get(); + try (var channel = app.grpcChannel()) { + HelloReply response = GreeterGrpc.newFutureStub(channel).sayHello(request).get(); - assertEquals("Hello " + NAME, response.getMessage()); + assertEquals("Hello " + NAME, response.getMessage()); + } } @Test diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java index a0880978e..cf4d1af08 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DevModeHttpsIT.java @@ -13,7 +13,7 @@ @QuarkusScenario public class DevModeHttpsIT extends AbstractDevModeIT { - @DevModeQuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true)) + @DevModeQuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, useTlsRegistry = false, configureHttpServer = true)) static RestService app = new DevModeQuarkusService() .withProperty("quarkus.oidc.enabled", "false") .withProperty("quarkus.keycloak.policy-enforcer.enable", "false") diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DownloadResourceIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DownloadResourceIT.java new file mode 100644 index 000000000..1d190f89e --- /dev/null +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/DownloadResourceIT.java @@ -0,0 +1,58 @@ +package io.quarkus.ts.http.advanced.reactive; + +import static io.restassured.RestAssured.given; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.scenarios.annotations.DisabledOnNative; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.response.Response; + +/** + * Test makes sure AsyncFile gets closed, coverage triggered by https://github.com/quarkusio/quarkus/issues/41811 + */ +@QuarkusScenario +@DisabledOnNative(reason = "To save resources on CI") +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "No lsof command on Windows") +class DownloadResourceIT { + @QuarkusApplication(classes = { DownloadResource.class }, properties = "oidcdisable.properties") + static RestService app = new RestService(); + + @Test + void ensureAsyncFileGetsClosed() throws IOException { + Response response = app.given() + .when().post("/download/create") + .then() + .statusCode(200) + .extract().response(); + String file = response.getBody().asString(); + + app.given() + .when().get("/download") + .then() + .statusCode(200); + + ProcessBuilder lsofBuilder = new ProcessBuilder("lsof", file); + Process lsofProcess = lsofBuilder.start(); + String lsofOutput = new BufferedReader(new InputStreamReader(lsofProcess.getInputStream())).lines() + .collect(Collectors.joining("\n")); + + app.given() + .when().delete("/download/delete") + .then() + .statusCode(204); + + Assert.assertEquals("AsyncFile is not closed, details:\n" + lsofOutput + "\n", 0, lsofOutput.length()); + } + +} diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HeadersIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HeadersIT.java index 5be721a99..1633ec1a6 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HeadersIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HeadersIT.java @@ -16,12 +16,15 @@ import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; import io.quarkus.test.services.QuarkusApplication; +import io.restassured.http.Header; import io.restassured.response.ValidatableResponse; @QuarkusScenario public class HeadersIT { @QuarkusApplication(classes = { PathSpecificHeadersResource.class, + HeadersMessageBodyWriter.class, + CustomHeaderResponse.class, HeadersResource.class }, properties = "headers.properties") static RestService app = new RestService(); @@ -100,6 +103,19 @@ private ValidatableResponse whenGet(String path) { .body(is("ok")); } + @Test + @Tag("https://github.com/quarkusio/quarkus/pull/41411") + void testWithNoAcceptHeader() { + Header header = new Header("Accept", null); + given() + .when() + .header(header) + .get("/headers/no-accept") + .then() + .statusCode(200) + .body(is("Headers response: ok headers")); + } + /** * Cache-Control header may be present multiple times in the response, e.g. in an OpenShift deployment. That is why we need * to look for a specific value among all headers of the same name, and not just match the last one of them, which is what diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/Http2IT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/Http2IT.java index 49319ba76..60be936cb 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/Http2IT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/Http2IT.java @@ -43,7 +43,7 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class Http2IT { @QuarkusApplication(ssl = true, classes = { MorningResource.class, - CustomFramesResource.class }, properties = "http2.properties", certificates = @Certificate(configureKeystore = true)) + CustomFramesResource.class }, properties = "http2.properties", certificates = @Certificate(configureKeystore = true, configureHttpServer = true, useTlsRegistry = false)) static RestService app = new RestService(); private static URILike baseUri; diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HttpAdvancedReactiveIT.java index 06fb08a72..544b8cdba 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HttpAdvancedReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/HttpAdvancedReactiveIT.java @@ -19,7 +19,7 @@ public class HttpAdvancedReactiveIT extends BaseHttpAdvancedReactiveIT { static KeycloakService keycloak = new KeycloakService(DEFAULT_REALM_FILE, DEFAULT_REALM, DEFAULT_REALM_BASE_PATH) .withProperty("JAVA_OPTS", "-Dcom.redhat.fips=false"); - @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true)) + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureHttpServer = true, useTlsRegistry = false)) static RestService app = new RestService().withProperty("quarkus.oidc.auth-server-url", keycloak::getRealmUrl); diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/OpenShiftHttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/OpenShiftHttpAdvancedReactiveIT.java index 515e55968..f7ec17339 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/OpenShiftHttpAdvancedReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/OpenShiftHttpAdvancedReactiveIT.java @@ -23,7 +23,7 @@ public class OpenShiftHttpAdvancedReactiveIT extends BaseHttpAdvancedReactiveIT static KeycloakService keycloak = new KeycloakService(DEFAULT_REALM_FILE, DEFAULT_REALM, DEFAULT_REALM_BASE_PATH) .withProperty("JAVA_OPTS", "-Dcom.redhat.fips=false"); - @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true)) + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureHttpServer = true, useTlsRegistry = false)) static RestService app = new RestService().withProperty("quarkus.oidc.auth-server-url", keycloak::getRealmUrl); diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/CustomHeaderResponse.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/CustomHeaderResponse.java new file mode 100644 index 000000000..d6a21773b --- /dev/null +++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/CustomHeaderResponse.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.http.advanced; + +public class CustomHeaderResponse { + + private final String content; + + public CustomHeaderResponse(String content) { + this.content = content; + } + + public String getContent() { + return content; + } +} diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersMessageBodyWriter.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersMessageBodyWriter.java new file mode 100644 index 000000000..b3cbbb8fc --- /dev/null +++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersMessageBodyWriter.java @@ -0,0 +1,29 @@ +package io.quarkus.ts.http.advanced; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class HeadersMessageBodyWriter implements MessageBodyWriter { + + @Override + public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return CustomHeaderResponse.class.isAssignableFrom(aClass) && MediaType.TEXT_PLAIN_TYPE.isCompatible(mediaType); + } + + @Override + public void writeTo(CustomHeaderResponse customHeaderResponse, Class aClass, Type type, Annotation[] annotations, + MediaType mediaType, MultivaluedMap multivaluedMap, OutputStream outputStream) + throws IOException, WebApplicationException { + final String content = "Headers response: " + customHeaderResponse.getContent(); + outputStream.write(content.getBytes()); + } +} diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersResource.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersResource.java index 64d8aeadd..8ce1d1b97 100644 --- a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersResource.java +++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/HeadersResource.java @@ -2,6 +2,8 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @Path("/headers") @@ -25,4 +27,10 @@ public Response headersOverride() { return Response.ok("ok").header("foo", "abc").build(); } + @GET + @Path("/no-accept") + @Produces(MediaType.TEXT_PLAIN) + public Response noAcceptHeaders() { + return Response.ok(new CustomHeaderResponse("ok headers")).build(); + } } diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/DevModeGrpcIntegrationIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/DevModeGrpcIntegrationIT.java index 7a3d5b323..e1a057dca 100644 --- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/DevModeGrpcIntegrationIT.java +++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/DevModeGrpcIntegrationIT.java @@ -23,7 +23,6 @@ import io.quarkus.test.bootstrap.Protocol; import io.quarkus.test.scenarios.QuarkusScenario; import io.quarkus.test.services.DevModeQuarkusApplication; -import io.quarkus.test.services.URILike; @Tag("QUARKUS-1026") @Tag("QUARKUS-1094") @@ -47,13 +46,7 @@ public class DevModeGrpcIntegrationIT { }; @DevModeQuarkusApplication(grpc = true) - static final GrpcService app = (GrpcService) new GrpcService() { - @Override - public URILike getGrpcHost() { - // TODO: make app.grpcChannel() support gRPC on same HTTP server - return super.getGrpcHost().withPort(app.getURI().getPort()); - } - } + static final GrpcService app = (GrpcService) new GrpcService() .withProperty("quarkus.oidc.enabled", "false") .withProperty("quarkus.keycloak.policy-enforcer.enable", "false") .withProperty("quarkus.keycloak.devservices.enabled", "false"); @@ -61,9 +54,11 @@ public URILike getGrpcHost() { @Test public void testGrpcAsClient() { HelloRequest request = HelloRequest.newBuilder().setName(NAME).build(); - HelloReply response = GreeterGrpc.newBlockingStub(app.grpcChannel()).sayHello(request); + try (var channel = app.grpcChannel()) { + HelloReply response = GreeterGrpc.newBlockingStub(channel).sayHello(request); - assertEquals("Hello " + NAME, response.getMessage()); + assertEquals("Hello " + NAME, response.getMessage()); + } } @Test diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HeadersIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HeadersIT.java index 741d94622..4e8ba99f1 100644 --- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HeadersIT.java +++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HeadersIT.java @@ -16,12 +16,15 @@ import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; import io.quarkus.test.services.QuarkusApplication; +import io.restassured.http.Header; import io.restassured.response.ValidatableResponse; @QuarkusScenario public class HeadersIT { @QuarkusApplication(classes = { PathSpecificHeadersResource.class, + HeadersMessageBodyWriter.class, + CustomHeaderResponse.class, HeadersResource.class }, properties = "headers.properties") static RestService app = new RestService(); @@ -93,6 +96,21 @@ void testPathSpecificHeaderRulesOrder() { cacheControlMatches(response, "max-age=1"); } + /** + * Coverage for https://github.com/quarkusio/quarkus/issues/41354 in RESTEasy classic + */ + @Test + void testWithNoAcceptHeader() { + Header header = new Header("Accept", null); + given() + .when() + .header(header) + .get("/headers/no-accept") + .then() + .statusCode(200) + .body(is("Headers response: ok headers")); + } + private ValidatableResponse whenGet(String path) { return given() .get(path) diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HttpAdvancedIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HttpAdvancedIT.java index 7a9c33ade..ec5e8d810 100644 --- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HttpAdvancedIT.java +++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/HttpAdvancedIT.java @@ -19,7 +19,7 @@ public class HttpAdvancedIT extends BaseHttpAdvancedIT { static KeycloakService keycloak = new KeycloakService(DEFAULT_REALM_FILE, DEFAULT_REALM, DEFAULT_REALM_BASE_PATH) .withProperty("JAVA_OPTS", "-Dcom.redhat.fips=false"); - @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true)) + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureHttpServer = true, useTlsRegistry = false)) static RestService app = new RestService().withProperty("quarkus.oidc.auth-server-url", keycloak::getRealmUrl); @Override diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/OpenShiftHttpAdvancedIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/OpenShiftHttpAdvancedIT.java index 1cfcd7199..f115e38b4 100644 --- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/OpenShiftHttpAdvancedIT.java +++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/OpenShiftHttpAdvancedIT.java @@ -23,7 +23,7 @@ public class OpenShiftHttpAdvancedIT extends BaseHttpAdvancedIT { static KeycloakService keycloak = new KeycloakService(DEFAULT_REALM_FILE, DEFAULT_REALM, DEFAULT_REALM_BASE_PATH) .withProperty("JAVA_OPTS", "-Dcom.redhat.fips=false"); - @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true)) + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureHttpServer = true, useTlsRegistry = false)) static RestService app = new RestService().withProperty("quarkus.oidc.auth-server-url", keycloak::getRealmUrl); @Override diff --git a/http/http-minimum-reactive/src/main/java/io/quarkus/ts/http/minimum/reactive/AbstractApplication.java b/http/http-minimum-reactive/src/main/java/io/quarkus/ts/http/minimum/reactive/AbstractApplication.java new file mode 100644 index 000000000..48575d600 --- /dev/null +++ b/http/http-minimum-reactive/src/main/java/io/quarkus/ts/http/minimum/reactive/AbstractApplication.java @@ -0,0 +1,9 @@ +package io.quarkus.ts.http.minimum.reactive; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/rest") +public abstract class AbstractApplication extends Application { + // Abstract jakarta.ws.rs.core.Application classes should be ignored +} diff --git a/http/http-minimum/src/main/java/io/quarkus/ts/http/minimum/AbstractApplication.java b/http/http-minimum/src/main/java/io/quarkus/ts/http/minimum/AbstractApplication.java new file mode 100644 index 000000000..175a6e2ca --- /dev/null +++ b/http/http-minimum/src/main/java/io/quarkus/ts/http/minimum/AbstractApplication.java @@ -0,0 +1,10 @@ +package io.quarkus.ts.http.minimum; + +//import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +// TODO uncomment when https://github.com/quarkusio/quarkus/issues/42963 is fixed +//@ApplicationPath("/rest") +public abstract class AbstractApplication extends Application { + // Abstract jakarta.ws.rs.core.Application classes should be ignored +} diff --git a/http/management/src/main/java/io/quarkus/qe/ManagementRoute.java b/http/management/src/main/java/io/quarkus/qe/ManagementRoute.java new file mode 100644 index 000000000..6a3d29fdb --- /dev/null +++ b/http/management/src/main/java/io/quarkus/qe/ManagementRoute.java @@ -0,0 +1,13 @@ +package io.quarkus.qe; + +import jakarta.enterprise.event.Observes; + +import io.quarkus.vertx.http.ManagementInterface; + +public class ManagementRoute { + + void setupCustomManagementRoute(@Observes ManagementInterface mi) { + mi.router().route("/management-ping").handler(ctx -> ctx.response().end("pong")); + } + +} diff --git a/http/management/src/test/java/io/quarkus/qe/LocalOptionsIT.java b/http/management/src/test/java/io/quarkus/qe/LocalOptionsIT.java index 331ea58af..50ca8e2f8 100644 --- a/http/management/src/test/java/io/quarkus/qe/LocalOptionsIT.java +++ b/http/management/src/test/java/io/quarkus/qe/LocalOptionsIT.java @@ -24,7 +24,7 @@ public class LocalOptionsIT { static final RestService custom = new RestService() .withProperty("quarkus.management.port", "9002"); - @QuarkusApplication(certificates = @Certificate(configureKeystoreForManagementInterface = true)) + @QuarkusApplication(certificates = @Certificate(configureManagementInterface = true, configureKeystore = true, useTlsRegistry = false)) static final RestService tls = new RestService() .withProperty("quarkus.management.port", "9003"); diff --git a/http/management/src/test/java/io/quarkus/qe/MutualTlsManagementInterfaceIT.java b/http/management/src/test/java/io/quarkus/qe/MutualTlsManagementInterfaceIT.java new file mode 100644 index 000000000..bf71a31fa --- /dev/null +++ b/http/management/src/test/java/io/quarkus/qe/MutualTlsManagementInterfaceIT.java @@ -0,0 +1,91 @@ +package io.quarkus.qe; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.security.certificate.CertificateBuilder; +import io.quarkus.test.security.certificate.ClientCertificateRequest; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.Certificate.ClientCertificate; +import io.quarkus.test.services.QuarkusApplication; +import io.quarkus.test.utils.AwaitilityUtils; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@QuarkusScenario +public class MutualTlsManagementInterfaceIT { + + private static final String CERT_PREFIX = "qe-test"; + private static final String CLIENT_CN_1 = "client-cn-1"; + private static final String CLIENT_CN_2 = "client-cn-2"; + private static final String TLS_CONFIG_NAME = "mtls-management"; + + @QuarkusApplication(certificates = @Certificate(prefix = CERT_PREFIX, clientCertificates = { + @ClientCertificate(cnAttribute = CLIENT_CN_1), + @ClientCertificate(cnAttribute = CLIENT_CN_2, unknownToServer = true) + }, configureKeystore = true, configureTruststore = true, tlsConfigName = TLS_CONFIG_NAME, configureManagementInterface = true)) + static final RestService app = new RestService() + .withProperty("quarkus.management.ssl.client-auth", "required") + .withProperty("quarkus.tls." + TLS_CONFIG_NAME + ".reload-period", "2s"); + + @Order(1) + @Test + public void testMutualTlsForManagementInterface() { + // test health probe + var client1 = app.mutinyHttps(CLIENT_CN_1); + var httpResponse = client1.get("/q/health").sendAndAwait(); + assertEquals(HttpStatus.SC_OK, httpResponse.statusCode()); + + // test custom management endpoint + httpResponse = client1.get("/management-ping").sendAndAwait(); + assertEquals("pong", httpResponse.bodyAsString()); + assertEquals(HttpStatus.SC_OK, httpResponse.statusCode()); + + callManagementPingRouteAndExpectFailure(CLIENT_CN_2); + } + + @Order(2) + @Test + public void testCertificateReloading() { + app + . getPropertyFromContext(CertificateBuilder.INSTANCE_KEY) + .regenerateCertificate(CERT_PREFIX, certRequest -> { + // regenerate client certificates + // make the first client invalid + var clientOneCert = new ClientCertificateRequest(CLIENT_CN_1, true); + // make the second client valid + var clientTwoCert = new ClientCertificateRequest(CLIENT_CN_2, false); + certRequest.withClientRequests(clientOneCert, clientTwoCert); + }); + + // now we expect opposite from what we tested in step one: client 1 must fail and client 2 must succeed + AwaitilityUtils.untilAsserted(() -> { + var httpResponse = app.mutinyHttps(CLIENT_CN_2).get("/management-ping").sendAndAwait(); + assertEquals("pong", httpResponse.bodyAsString()); + assertEquals(HttpStatus.SC_OK, httpResponse.statusCode()); + + callManagementPingRouteAndExpectFailure(CLIENT_CN_1); + }); + } + + private static void callManagementPingRouteAndExpectFailure(String clientCn) { + // this client certs are not in the server truststore, therefore they cannot be trusted + try { + app.mutinyHttps(clientCn).get("/management-ping").sendAndAwait(); + // this must never happen, basically as SSL handshake must throw exception + Assertions.fail("SSL handshake didn't fail even though certificate host is unknown"); + } catch (Exception e) { + // failure is expected + assertTrue(e.getMessage().contains("Received fatal alert: bad_certificate"), + "Expected failure over bad certificate, but got: " + e.getMessage()); + } + } +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/HeaderResource.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/HeaderResource.java new file mode 100644 index 000000000..20baaa6df --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/HeaderResource.java @@ -0,0 +1,37 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; + +/** + * Stores request headers from incoming server requests + * and exposes an endpoint that returns these headers + * It is used for testing purposes to verify if headers are correctly propagated. + */ +@Path("/fault/headers") +@ApplicationScoped +public class HeaderResource { + + private final List> headerList = new ArrayList<>(); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getStoredHeaders() { + return Response.ok(headerList).build(); + } + + public void storeHeaders(MultivaluedMap headers) { + headerList.add(headers.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)))); + } +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdClientRequestFilter.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdClientRequestFilter.java new file mode 100644 index 000000000..dc36c5e24 --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdClientRequestFilter.java @@ -0,0 +1,28 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.ext.Provider; + +/** + * Injects `REQUEST_ID` into the headers of every outgoing REST client call. + * It is used in combination with the `RequestIdManager` class to ensure + * request context propagation between client and server calls. + */ +@Provider +@ApplicationScoped +public class RequestIdClientRequestFilter implements ClientRequestFilter { + + private final RequestIdManager requestIdManager; + + public RequestIdClientRequestFilter(RequestIdManager requestIdManager) { + this.requestIdManager = requestIdManager; + } + + @Override + public void filter(ClientRequestContext requestContext) { + int requestId = requestIdManager.currentRequestId(); + requestContext.getHeaders().putSingle("REQUEST_ID", requestId); + } +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdContainerRequestFilter.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdContainerRequestFilter.java new file mode 100644 index 000000000..e68d5df48 --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdContainerRequestFilter.java @@ -0,0 +1,43 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.Provider; + +/** + * This server-side filter is used to extract the `REQUEST_ID` header + * from incoming requests and pass it to the `RequestIdManagerImpl` to manage request context. + * It also stores the headers via the `HeaderResource` for test verification. + */ +@Provider +@Produces +@PreMatching +@ConstrainedTo(RuntimeType.SERVER) +public class RequestIdContainerRequestFilter implements ContainerRequestFilter { + + @Inject + RequestIdManagerImpl requestIdManagerImpl; + + @Inject + HeaderResource headerResource; + + @Override + @ActivateRequestContext + public void filter(ContainerRequestContext requestContext) { + MultivaluedMap headers = requestContext.getHeaders(); + if (headers.containsKey("REQUEST_ID")) { + int requestId = Integer.valueOf(headers.getFirst("REQUEST_ID")); + requestIdManagerImpl.overrideRequestId(requestId); + } + + // Store the headers in the HeaderResource for later retrieval in tests + headerResource.storeHeaders(headers); + } +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdManager.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdManager.java new file mode 100644 index 000000000..f734b55a3 --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdManager.java @@ -0,0 +1,6 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +public interface RequestIdManager { + + int currentRequestId(); +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdManagerImpl.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdManagerImpl.java new file mode 100644 index 000000000..2a95fecfb --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RequestIdManagerImpl.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +import jakarta.enterprise.context.RequestScoped; + +/** + * Manages the `REQUEST_ID` for each request, allowing it + * to be overridden when the `REQUEST_ID` is passed via headers from client requests. + * Handles a unique id per request. + */ +@RequestScoped +public class RequestIdManagerImpl implements RequestIdManager { + + private int requestID; + + public int currentRequestId() { + return requestID; + } + + public void overrideRequestId(int inboundRequestId) { + this.requestID = inboundRequestId; + } +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RetryClient.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RetryClient.java new file mode 100644 index 000000000..9b3d829de --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/RetryClient.java @@ -0,0 +1,32 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +import java.util.concurrent.CompletionStage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +/** + * Interface defines a REST client making asynchronous calls + * to the `/client/async` endpoint. It uses the `@Retry` annotation + * to retry in case of a `ProcessingException`. It is called by the + * server resource `ServerRetryResource`. + */ +@ApplicationScoped +@RegisterRestClient(configKey = "client.endpoint") +@Path("/client") +public interface RetryClient { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Retry(retryOn = ProcessingException.class) + @Path("async") + CompletionStage getItemsAsync(); +} diff --git a/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/ServerRetryResource.java b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/ServerRetryResource.java new file mode 100644 index 000000000..94ea58b00 --- /dev/null +++ b/http/rest-client-reactive/src/main/java/io/quarkus/ts/http/restclient/reactive/fault/tolerance/ServerRetryResource.java @@ -0,0 +1,39 @@ +package io.quarkus.ts.http.restclient.reactive.fault.tolerance; + +import java.util.concurrent.ExecutionException; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +/** + * Server resource that calling `RetryClient` to trigger + * an asynchronous client request. It is used to simulate and test + * retry logic when the client fails to complete a request. + */ +@ApplicationScoped +@Path("/server") +public class ServerRetryResource { + + private final RetryClient retryClient; + + @Inject + public ServerRetryResource(@RestClient RetryClient retryClient) { + this.retryClient = retryClient; + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("async") + public Response getItemsAsync() throws ExecutionException, InterruptedException { + return retryClient.getItemsAsync() + .toCompletableFuture() + .get(); + } +} diff --git a/http/rest-client-reactive/src/main/resources/application.properties b/http/rest-client-reactive/src/main/resources/application.properties index 5e25ddc66..eebf6c47e 100644 --- a/http/rest-client-reactive/src/main/resources/application.properties +++ b/http/rest-client-reactive/src/main/resources/application.properties @@ -8,3 +8,6 @@ quarkus.rest-client.encoder-mode-rfc1738.url=${vertx-server-url} quarkus.rest-client.encoder-mode-rfc1738.multipart-post-encoder-mode=RFC1738 quarkus.rest-client.encoder-mode-rfc3986.url=${vertx-server-url} quarkus.rest-client.encoder-mode-rfc3986.multipart-post-encoder-mode=RFC3986 + +# Set the Client endpoint to a non-existing domain to trigger a fault +client.endpoint/mp-rest/url=http://unknown-domain:8080 diff --git a/http/rest-client-reactive/src/test/java/hero/Hero.java b/http/rest-client-reactive/src/test/java/hero/Hero.java new file mode 100644 index 000000000..82fc6102a --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/Hero.java @@ -0,0 +1,4 @@ +package hero; + +public record Hero(Long id, String name, String otherName, int level, String picture, String powers) { +} diff --git a/http/rest-client-reactive/src/test/java/hero/HeroClient.java b/http/rest-client-reactive/src/test/java/hero/HeroClient.java new file mode 100644 index 000000000..171212a6a --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/HeroClient.java @@ -0,0 +1,15 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "hero") +public interface HeroClient { + + @GET + @Path("/api/heroes/random") + Hero getRandomHero(); + +} diff --git a/http/rest-client-reactive/src/test/java/hero/HeroClientResource.java b/http/rest-client-reactive/src/test/java/hero/HeroClientResource.java new file mode 100644 index 000000000..726d96017 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/HeroClientResource.java @@ -0,0 +1,19 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("hero-client-resource") +public class HeroClientResource { + + @RestClient + HeroClient heroClient; + + @GET + public Hero triggerClientToServerCommunication() { + return heroClient.getRandomHero(); + } + +} diff --git a/http/rest-client-reactive/src/test/java/hero/HeroResource.java b/http/rest-client-reactive/src/test/java/hero/HeroResource.java new file mode 100644 index 000000000..f1a51a7e2 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/HeroResource.java @@ -0,0 +1,17 @@ +package hero; + +import java.util.random.RandomGenerator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api/heroes/random") +public class HeroResource { + + @GET + public Hero getRandomHero() { + long random = RandomGenerator.getDefault().nextLong(); + return new Hero(random, "Name-" + random, "Other-" + random, 1, "placeholder", "root"); + } + +} diff --git a/http/rest-client-reactive/src/test/java/hero/Villain.java b/http/rest-client-reactive/src/test/java/hero/Villain.java new file mode 100644 index 000000000..448be6689 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/Villain.java @@ -0,0 +1,4 @@ +package hero; + +public record Villain(Long id, String name, String otherName, int level, String picture, String powers) { +} diff --git a/http/rest-client-reactive/src/test/java/hero/VillainClient.java b/http/rest-client-reactive/src/test/java/hero/VillainClient.java new file mode 100644 index 000000000..aeb18810c --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/VillainClient.java @@ -0,0 +1,15 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "villain") +public interface VillainClient { + + @GET + @Path("/api/villain/random") + Villain getRandomVillain(); + +} diff --git a/http/rest-client-reactive/src/test/java/hero/VillainClientResource.java b/http/rest-client-reactive/src/test/java/hero/VillainClientResource.java new file mode 100644 index 000000000..2d9bf9f36 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/VillainClientResource.java @@ -0,0 +1,19 @@ +package hero; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("villain-client-resource") +public class VillainClientResource { + + @RestClient + VillainClient villainClient; + + @GET + public Villain triggerClientToServerCommunication() { + return villainClient.getRandomVillain(); + } + +} diff --git a/http/rest-client-reactive/src/test/java/hero/VillainResource.java b/http/rest-client-reactive/src/test/java/hero/VillainResource.java new file mode 100644 index 000000000..6acb6fc66 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/hero/VillainResource.java @@ -0,0 +1,17 @@ +package hero; + +import java.util.random.RandomGenerator; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api/villain/random") +public class VillainResource { + + @GET + public Villain getRandomVillain() { + long random = RandomGenerator.getDefault().nextLong(); + return new Villain(random, "Name-" + random, "Other-" + random, 1, "placeholder", "root"); + } + +} diff --git a/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java b/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java new file mode 100644 index 000000000..317bbfe10 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/OpenShiftServingCertificatesIT.java @@ -0,0 +1,81 @@ +package io.quarkus.ts.http.restclient.reactive; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import io.quarkus.test.bootstrap.Protocol; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.OpenShiftScenario; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.Certificate.ServingCertificates; +import io.quarkus.test.services.QuarkusApplication; +import io.quarkus.test.utils.AwaitilityUtils; + +import hero.Hero; +import hero.HeroClient; +import hero.HeroClientResource; +import hero.HeroResource; +import hero.Villain; +import hero.VillainClient; +import hero.VillainClientResource; +import hero.VillainResource; + +/** + * Test OpenShift serving certificate support and Quarkus REST client. + */ +@Tag("QUARKUS-4592") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@OpenShiftScenario +public class OpenShiftServingCertificatesIT { + + private static final String HERO_CLIENT = "hero-client"; + private static final String SERVER_TLS_CONFIG_NAME = "cert-serving-test-server"; + + @QuarkusApplication(ssl = true, certificates = @Certificate(tlsConfigName = SERVER_TLS_CONFIG_NAME, servingCertificates = { + @ServingCertificates(addServiceCertificate = true) + }), classes = { HeroResource.class, Hero.class, Villain.class, + VillainResource.class }, properties = "certificate-serving-server.properties") + static final RestService server = new RestService(); + + @QuarkusApplication(certificates = @Certificate(tlsConfigName = HERO_CLIENT, servingCertificates = @ServingCertificates(injectCABundle = true)), classes = { + HeroClient.class, Hero.class, HeroClientResource.class, Villain.class, VillainClient.class, + VillainClientResource.class }, properties = "certificate-serving-client.properties") + static final RestService client = new RestService() + .withProperty("quarkus.rest-client.hero.uri", () -> server.getURI(Protocol.HTTPS).getRestAssuredStyleUri()); + + @Order(1) + @Test + public void testSecuredCommunicationBetweenClientAndServer() { + // REST client use OpenShift internal CA + // server is configured with OpenShift serving certificates + // ad "untilAsserted": we experienced unknown SAN, so to avoid flakiness I am adding here retry: + AwaitilityUtils.untilAsserted(() -> { + var hero = client.given().get("hero-client-resource").then().statusCode(200).extract().as(Hero.class); + assertNotNull(hero); + assertNotNull(hero.name()); + assertTrue(hero.name().startsWith("Name-")); + assertNotNull(hero.otherName()); + assertTrue(hero.otherName().startsWith("Other-")); + }, AwaitilityUtils.AwaitilitySettings.usingTimeout(Duration.ofSeconds(50))); + } + + @Order(2) + @Test + public void testConfiguredTlsProtocolEnforced() { + // verifies that protocol version set in TLS config is obliged by both HTTP server and client + // REST client requires TLSv1.2 + // HTTP server requires TLSv1.3 + client.logs().assertDoesNotContain("Received fatal alert: protocol_version"); + client.given().get("villain-client-resource").then().statusCode(500); + client.logs().assertContains("Received fatal alert: protocol_version"); + } + +} diff --git a/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/ReactiveRestClientRetryIT.java b/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/ReactiveRestClientRetryIT.java new file mode 100644 index 000000000..055aab193 --- /dev/null +++ b/http/rest-client-reactive/src/test/java/io/quarkus/ts/http/restclient/reactive/ReactiveRestClientRetryIT.java @@ -0,0 +1,80 @@ +package io.quarkus.ts.http.restclient.reactive; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.core.Options.ChunkedEncodingPolicy.NEVER; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.http.Fault; + +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.QuarkusApplication; +import io.restassured.common.mapper.TypeRef; +import io.restassured.http.ContentType; + +/** + * Verifies SmallRye fault-tolerance retry logic and header propagation between the client and server. + */ +@QuarkusScenario +public class ReactiveRestClientRetryIT { + + private static final WireMockServer mockServer; + private final int headerId = 121; + + static { + mockServer = new WireMockServer(WireMockConfiguration.options() + .dynamicPort() + .useChunkedTransferEncoding(NEVER)); + mockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/client/async")) + .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); + + mockServer.start(); + } + + @QuarkusApplication + static RestService app = new RestService() + .withProperty("client.endpoint/mp-rest/url", mockServer::baseUrl); + + @Test + @Tag("QUARKUS-4477") + // Commit that fixes the issue + // https://github.com/quarkusio/quarkus/pull/39988/commits/b9cc3c2dc65a6f61641c83a940e13c116ce6cd0c + void shouldPerformRetryOfFailingBlockingClientCall() { + app.given().header("REQUEST_ID", headerId) + .get("/server/async") + .then() + .statusCode(500); + + // Check number of server events, one failing call plus 3 retries by default + Assertions.assertEquals(4, mockServer.getServeEvents().getServeEvents().stream().count()); + + List> headers = app.given() + .get("/fault/headers") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .body() + .as(new TypeRef<>() { + }); + + // Check if REQUEST_ID header was propagated and stored + Assertions.assertTrue(headers.stream().anyMatch(header -> header.containsKey("REQUEST_ID") + && headerId == (Integer.parseInt(header.get("REQUEST_ID"))))); + } + + @AfterAll + static void afterAll() { + mockServer.stop(); + } +} diff --git a/http/rest-client-reactive/src/test/resources/certificate-serving-client.properties b/http/rest-client-reactive/src/test/resources/certificate-serving-client.properties new file mode 100644 index 000000000..9cc0eb88e --- /dev/null +++ b/http/rest-client-reactive/src/test/resources/certificate-serving-client.properties @@ -0,0 +1,6 @@ +quarkus.rest-client.hero.tls-configuration-name=hero-client +quarkus.rest-client.villain.tls-configuration-name=villain-client +quarkus.rest-client.villain.uri=${quarkus.rest-client.hero.uri} +quarkus.tls.villain-client.trust-store.pem.certs=${quarkus.tls.hero-client.trust-store.pem.certs} +quarkus.tls.hero-client.protocols=TLSv1.3 +quarkus.tls.villain-client.protocols=TLSv1.2 diff --git a/http/rest-client-reactive/src/test/resources/certificate-serving-server.properties b/http/rest-client-reactive/src/test/resources/certificate-serving-server.properties new file mode 100644 index 000000000..4a09cefd5 --- /dev/null +++ b/http/rest-client-reactive/src/test/resources/certificate-serving-server.properties @@ -0,0 +1,4 @@ +# the REST client use HTTPS but not mTLS +quarkus.http.ssl.client-auth=request +quarkus.http.insecure-requests=disabled +quarkus.tls.cert-serving-test-server.protocols=TLSv1.3 diff --git a/lifecycle-application/pom.xml b/lifecycle-application/pom.xml index 50b05e441..b37b27015 100644 --- a/lifecycle-application/pom.xml +++ b/lifecycle-application/pom.xml @@ -11,9 +11,9 @@ jar Quarkus QE TS: Lifecycle Application - + - 9999999-SNAPSHOT + non-existing-bom @@ -26,8 +26,8 @@ io.quarkus - quarkus-spring-di - 3.8.3.redhat-00003 + quarkus-spring-core-api + 5.2.0.SP7-redhat-00001 @@ -39,8 +39,7 @@ - - 3.8.3.redhat-00003 + quarkus-bom @@ -48,12 +47,6 @@ https://maven.repository.redhat.com/ga/ - - - red-hat-enterprise-repository - https://maven.repository.redhat.com/ga/ - - diff --git a/monitoring/micrometer-prometheus-kafka-reactive/src/test/java/io/quarkus/ts/micrometer/prometheus/kafka/reactive/KafkaAndMetricsIT.java b/monitoring/micrometer-prometheus-kafka-reactive/src/test/java/io/quarkus/ts/micrometer/prometheus/kafka/reactive/KafkaAndMetricsIT.java new file mode 100644 index 000000000..d14c0d301 --- /dev/null +++ b/monitoring/micrometer-prometheus-kafka-reactive/src/test/java/io/quarkus/ts/micrometer/prometheus/kafka/reactive/KafkaAndMetricsIT.java @@ -0,0 +1,40 @@ +package io.quarkus.ts.micrometer.prometheus.kafka.reactive; + +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.KafkaService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.KafkaContainer; +import io.quarkus.test.services.QuarkusApplication; + +/** + * Tests for Kafka and Metrics scenarios + */ +@QuarkusScenario +public class KafkaAndMetricsIT { + @KafkaContainer + static final KafkaService kafka = new KafkaService(); + + @QuarkusApplication + static RestService app = new RestService().withProperty("kafka.bootstrap.servers", kafka::getBootstrapUrl); + + /** + * Test to ensure Kafka version is visible in metrics, especially in native mode + * Issues: https://github.com/quarkusio/quarkus/pull/41278 and https://github.com/quarkusio/quarkus/issues/42865 + */ + @Test + public void testKafkaVersionInMetrics() { + String metrics = app.given().when().get("/q/metrics").then().statusCode(200).extract().asString(); + + boolean isKafkaVersionPresent = metrics.contains("kafka_version"); + boolean isKafkaVersionUnknown = metrics.contains("kafka_version=\"unknown\""); + + assertTrue(isKafkaVersionPresent, "'kafka_version' string is not present in the metrics response"); + assertFalse(isKafkaVersionUnknown, "'kafka_version' is 'unknown' in the metrics response"); + } +} diff --git a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryManagementIT.java b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryManagementIT.java index 81846b115..9b768ed06 100644 --- a/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryManagementIT.java +++ b/monitoring/opentelemetry/src/test/java/io/quarkus/ts/opentelemetry/OpenTelemetryManagementIT.java @@ -1,5 +1,7 @@ package io.quarkus.ts.opentelemetry; +import static io.quarkus.test.services.containers.JaegerGenericDockerContainerManagedResource.CERTIFICATE_CONTEXT_KEY; +import static io.quarkus.test.services.containers.JaegerGenericDockerContainerManagedResource.JAEGER_CLIENT_CERT_CN; import static io.restassured.RestAssured.given; import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.containsString; @@ -10,23 +12,32 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; import io.quarkus.test.bootstrap.JaegerService; +import io.quarkus.test.bootstrap.Protocol; import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.security.certificate.Certificate; +import io.quarkus.test.security.certificate.PemClientCertificate; import io.quarkus.test.services.JaegerContainer; import io.quarkus.test.services.QuarkusApplication; +@Tag("QUARKUS-4592") @QuarkusScenario public class OpenTelemetryManagementIT { - @JaegerContainer + @JaegerContainer(tls = true) static final JaegerService jaeger = new JaegerService(); @QuarkusApplication static RestService pong = new RestService() .withProperty("quarkus.application.name", "pong") .withProperty("quarkus.management.enabled", "true") - .withProperty("quarkus.otel.exporter.otlp.traces.endpoint", jaeger::getCollectorUrl); + .withProperty("quarkus.otel.exporter.otlp.traces.endpoint", () -> jaeger.getCollectorUrl(Protocol.HTTPS)) + .withProperty("quarkus.otel.exporter.otlp.traces.tls-configuration-name", "jaeger") + .withProperty("quarkus.tls.jaeger.key-store.pem.0.cert", OpenTelemetryManagementIT::getTlsCertPath) + .withProperty("quarkus.tls.jaeger.key-store.pem.0.key", OpenTelemetryManagementIT::getTlsKeyPath) + .withProperty("quarkus.tls.jaeger.trust-store.pem.certs", OpenTelemetryManagementIT::getTlsCaCertPath); private static final String PONG_ENDPOINT = "/hello"; private static final String MANAGEMENT_ENDPOINT = "/q/health/ready"; @@ -66,4 +77,30 @@ public void managementEndpointExcludedFromTracesTest() { Assertions.assertTrue(traces.contains(PONG_ENDPOINT), "Pong endpoint should be logged in traces"); Assertions.assertFalse(traces.contains(MANAGEMENT_ENDPOINT), "Management endpoint should not be logged in traces"); } + + private static String getTlsKeyPath() { + return addEscapes(getClientCertificate().keyPath()); + } + + private static String getTlsCertPath() { + return addEscapes(getClientCertificate().certPath()); + } + + private static String getTlsCaCertPath() { + return addEscapes(getClientCertificate().truststorePath()); + } + + private static PemClientCertificate getClientCertificate() { + return (PemClientCertificate) jaeger. getPropertyFromContext(CERTIFICATE_CONTEXT_KEY) + .getClientCertificateByCn(JAEGER_CLIENT_CERT_CN); + } + + static String addEscapes(String path) { + if (OS.WINDOWS.isCurrentOs()) { + // TODO: move this to the FW + // back-slashes have special meaning in Cygwin etc. + return path.replace("\\", "\\\\"); + } + return path; + } } diff --git a/pom.xml b/pom.xml index b5fa7f100..d8cc6017e 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ io.quarkus 3.14.999-SNAPSHOT 3.14.0 - 1.5.0 + 1.5.1 2.6.1 7.5.1 2.24.1 diff --git a/quarkus-cli/pom.xml b/quarkus-cli/pom.xml index 08a0434c2..d645d78f8 100644 --- a/quarkus-cli/pom.xml +++ b/quarkus-cli/pom.xml @@ -16,6 +16,13 @@ quarkus-test-cli test + + io.quarkus + quarkus-tls-registry + + + provided + @@ -41,7 +48,8 @@ - + + true diff --git a/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliTlsCommandIT.java b/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliTlsCommandIT.java new file mode 100644 index 000000000..93098ad44 --- /dev/null +++ b/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/QuarkusCliTlsCommandIT.java @@ -0,0 +1,140 @@ +package io.quarkus.ts.quarkus.cli; + +import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.CERT_NAME; +import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.CN; +import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.PASSWORD; +import static io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest.TRUST_STORE_PATH; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.util.function.Function; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import io.quarkus.logging.Log; +import io.quarkus.test.bootstrap.QuarkusCliCommandResult; +import io.quarkus.test.bootstrap.tls.GenerateCertOptions; +import io.quarkus.test.bootstrap.tls.GenerateQuarkusCaOptions; +import io.quarkus.test.bootstrap.tls.QuarkusTlsCommand; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.scenarios.annotations.DisabledOnNative; +import io.quarkus.ts.quarkus.cli.tls.surefire.TlsCommandTest; + +@Tag("QUARKUS-4592") +@Tag("quarkus-cli") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@QuarkusScenario +@DisabledOnNative // Only for JVM verification +public class QuarkusCliTlsCommandIT { + + // locations are expected according to the Quarkus docs describes generation target + private static final File QUARKUS_BASE_DIR = new File(System.getenv("HOME"), ".quarkus"); + private static final File DEV_CA_CERT_FILE = new File(QUARKUS_BASE_DIR, "quarkus-dev-root-ca.pem"); + private static final File DEV_CA_PK_FILE = new File(QUARKUS_BASE_DIR, "quarkus-dev-root-key.pem"); + + @Inject + static QuarkusTlsCommand tlsCommand; + + @Order(1) + @Test + public void generateQuarkusCa() { + // also prepares state for assertion in TlsCommandTest + deleteFileIfExists(DEV_CA_CERT_FILE); + deleteFileIfExists(DEV_CA_PK_FILE); + tlsCommand + .generateQuarkusCa() + .withOption(GenerateQuarkusCaOptions.TRUSTSTORE_LONG) + .withOption(GenerateQuarkusCaOptions.RENEW_SHORT) + .executeCommand() + .assertCommandOutputContains("Root CA certificate generated successfully") + .assertCommandOutputContains("Quarkus Development CA generated and installed") + .assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Truststore generated successfully:")); + assertTrue(DEV_CA_CERT_FILE.exists(), + "Quarkus CLI subcommand 'tls generate-quarkus-ca' didn't generate Quarkus DEV CA certificate"); + assertTrue(DEV_CA_PK_FILE.exists(), + "Quarkus CLI subcommand 'tls generate-quarkus-ca' didn't generate Quarkus DEV CA private key"); + } + + @Order(2) + @Test + public void generateCertificate() { + // also prepares state for assertion in TlsCommandTest + String appSvcDir = tlsCommand.getApp().getServiceFolder().toAbsolutePath().toString(); + tlsCommand + .generateCertificate() + .withOption(GenerateCertOptions.COMMON_NAME_LONG, CN) + .withOption(GenerateCertOptions.NAME_SHORT, CERT_NAME) + .withOption(GenerateCertOptions.PASSWORD_SHORT, PASSWORD) + .withOption(GenerateCertOptions.DIRECTORY_LONG, appSvcDir) + .executeCommand() + .assertCommandOutputContains("Quarkus Dev CA certificate found at " + DEV_CA_CERT_FILE.getAbsolutePath()) + .assertCommandOutputContains("PKCS12 keystore and truststore generated successfully!") + .assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Key Store File:")) + .assertFileExistsStr(cmd -> cmd.getOutputLineRemainder("Trust Store File:")) + // save truststore path in application properties so that we can use it in TlsCommandTest + .addToAppProps(cmd -> TRUST_STORE_PATH + "=" + cmd.getOutputLineRemainder("Trust Store File:")) + .assertCommandOutputContains( + "Signed Certificate generated successfully and exported into `%s-keystore.p12`".formatted(CERT_NAME)) + // following properties are set by this tls command, and we want to use them TlsCommandTest as well + .addToAppProps(getPropertyFromEnvFileAndChangeProfileToTest("quarkus.tls.key-store.p12.path")) + .addToAppProps(getPropertyFromEnvFileAndChangeProfileToTest("quarkus.tls.key-store.p12.password")); + } + + @Order(3) + @Test + public void runTestsUsingGeneratedCerts() { + tlsCommand.buildAppAndExpectSuccess(TlsCommandTest.class); + } + + private static void deleteFileIfExists(File file) { + if (file.exists()) { + // better inform so that user know his local Quarkus DEV CA is gone + Log.info("Deleting file: " + file); + if (!file.delete()) { + throw new IllegalStateException("Failed to delete file: " + file); + } + } + } + + @Test + public void testHelpOption() { + tlsCommand.generateQuarkusCa() + .withOption(GenerateQuarkusCaOptions.HELP_LONG) + .executeCommand() + .assertCommandOutputContains("--install") + .assertCommandOutputContains("--renew") + .assertCommandOutputContains("--truststore") + .assertCommandOutputContains("Generate Quarkus Dev CA certificate and private key") + .assertCommandOutputContains("Install the generated CA into the system keychain") + .assertCommandOutputContains("Update certificate if already created") + .assertCommandOutputContains("Generate a PKCS12"); + tlsCommand.generateCertificate() + .withOption(GenerateCertOptions.HELP_SHORT) + .executeCommand() + .assertCommandOutputContains("--directory") + .assertCommandOutputContains("--name") + .assertCommandOutputContains("--password") + .assertCommandOutputContains("--renew") + .assertCommandOutputContains("Generate a TLS certificate with the Quarkus Dev CA if available") + .assertCommandOutputContains("The common name of the certificate") + .assertCommandOutputContains("The directory in which the certificates will be created") + .assertCommandOutputContains("Name of the certificate") + .assertCommandOutputContains("The password of the keystore") + .assertCommandOutputContains("Whether existing certificates will need to be replaced"); + } + + private static Function getPropertyFromEnvFileAndChangeProfileToTest(String propertyKey) { + return cmd -> { + // add generated env vars also to application properties under test profile + // so that we can also use them in TlsCommandTest + var propertyValue = cmd.getPropertyValueFromEnvFile("%dev." + propertyKey); + return "%test." + propertyKey + "=" + propertyValue; + }; + } +} diff --git a/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/tls/surefire/TlsCommandTest.java b/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/tls/surefire/TlsCommandTest.java new file mode 100644 index 000000000..af5f5e9a8 --- /dev/null +++ b/quarkus-cli/src/test/java/io/quarkus/ts/quarkus/cli/tls/surefire/TlsCommandTest.java @@ -0,0 +1,90 @@ +package io.quarkus.ts.quarkus.cli.tls.surefire; + +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.net.URL; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.HashSet; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.tls.TlsConfigurationRegistry; +import io.restassured.RestAssured; +import io.smallrye.config.SmallRyeConfig; + +/** + * This test is only support to run inside QuarkusCliTlsCommandIT. + */ +@QuarkusTest +public class TlsCommandTest { + + public static final String CN = "quarkus-qe-cn"; + public static final String CERT_NAME = "quarkus-qe-cert"; + public static final String PASSWORD = "quarkus-qe-password"; + public static final String TRUST_STORE_PATH = "quarkus-qe-test.trust-store-path"; + + @Inject + TlsConfigurationRegistry registry; + + @Inject + SmallRyeConfig config; + + @TestHTTPResource(value = "/hello", tls = true) + URL helloEndpointUrl; + + @Test + void testKeystoreInDefaultTlsRegistry() throws KeyStoreException { + var defaultRegistry = registry.getDefault() + .orElseThrow(() -> new AssertionError("Default TLS Registry is not configured")); + var ks = defaultRegistry.getKeyStore(); + var ksAliasesSet = new HashSet(); + var ksAliases = ks.aliases(); + while (ksAliases.hasMoreElements()) { + ksAliasesSet.add(ksAliases.nextElement()); + } + assertTrue(ksAliasesSet.contains(CERT_NAME)); + assertTrue(ksAliasesSet.contains("issuer-" + CERT_NAME)); + + try { + var key = ks.getKey(CERT_NAME, PASSWORD.toCharArray()); + assertNotNull(key); + // this is not set in the stone so we don't mind if it changes to something sensible + // the point here is that we get Key and work with it ... + assertEquals("RSA", key.getAlgorithm()); + } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new RuntimeException(e); + } + } + + @Test + void testCommunicationUsingGeneratedCerts() { + try { + // failure test: make sure that generated truststore is really required + // so that we know that the truststore generated with Quarkus CLI TLS command works + RestAssured + .given() + .get(helloEndpointUrl).then().statusCode(200); + Assertions.fail("Truststore is not required, therefore we cannot verify generated truststore"); + } catch (Exception ignored) { + // failure expected + } + + var truststorePath = config.getValue(TRUST_STORE_PATH, String.class); + RestAssured + .given() + .trustStore(new File(truststorePath), PASSWORD) + .get(helloEndpointUrl).then().statusCode(200).body(is("Hello from Quarkus REST")); + } + +} diff --git a/security/https/src/test/java/io/quarkus/ts/security/https/enabled/EnabledHttpsSecurityIT.java b/security/https/src/test/java/io/quarkus/ts/security/https/enabled/EnabledHttpsSecurityIT.java index 801932120..bd3073a3c 100644 --- a/security/https/src/test/java/io/quarkus/ts/security/https/enabled/EnabledHttpsSecurityIT.java +++ b/security/https/src/test/java/io/quarkus/ts/security/https/enabled/EnabledHttpsSecurityIT.java @@ -20,7 +20,7 @@ public class EnabledHttpsSecurityIT { @QuarkusApplication(ssl = true, certificates = { - @Certificate(configureKeystore = true, configureTruststore = true, password = CLIENT_PASSWORD, clientCertificates = { + @Certificate(configureKeystore = true, configureTruststore = true, useTlsRegistry = false, configureHttpServer = true, password = CLIENT_PASSWORD, clientCertificates = { @Certificate.ClientCertificate(cnAttribute = CLIENT_CN), @Certificate.ClientCertificate(cnAttribute = UNKNOWN_CLIENT_CN, unknownToServer = true) }) diff --git a/security/https/src/test/java/io/quarkus/ts/security/https/redirect/RedirectHttpsSecurityIT.java b/security/https/src/test/java/io/quarkus/ts/security/https/redirect/RedirectHttpsSecurityIT.java index 107707941..6fa01e278 100644 --- a/security/https/src/test/java/io/quarkus/ts/security/https/redirect/RedirectHttpsSecurityIT.java +++ b/security/https/src/test/java/io/quarkus/ts/security/https/redirect/RedirectHttpsSecurityIT.java @@ -18,7 +18,7 @@ @QuarkusScenario public class RedirectHttpsSecurityIT { - @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureTruststore = true)) + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureTruststore = true, configureHttpServer = true, useTlsRegistry = false)) static RestService app = new RestService() .withProperty("quarkus.http.insecure-requests", HttpConfiguration.InsecureRequests.REDIRECT.name()); diff --git a/security/https/src/test/java/io/quarkus/ts/security/https/secured/AuthzHttpsSecurityIT.java b/security/https/src/test/java/io/quarkus/ts/security/https/secured/AuthzHttpsSecurityIT.java index 57fcf88e9..0e00351d4 100644 --- a/security/https/src/test/java/io/quarkus/ts/security/https/secured/AuthzHttpsSecurityIT.java +++ b/security/https/src/test/java/io/quarkus/ts/security/https/secured/AuthzHttpsSecurityIT.java @@ -24,7 +24,7 @@ public class AuthzHttpsSecurityIT { private static final String SECURED_PATH = "/secured"; private static final String HELLO_FULL_PATH = "/hello/full"; - @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, configureTruststore = true, password = CLIENT_PASSWORD, clientCertificates = { + @QuarkusApplication(ssl = true, certificates = @Certificate(useTlsRegistry = false, configureHttpServer = true, configureKeystore = true, configureTruststore = true, password = CLIENT_PASSWORD, clientCertificates = { @Certificate.ClientCertificate(cnAttribute = CLIENT_CN), @Certificate.ClientCertificate(cnAttribute = GUEST_CLIENT_CN), @Certificate.ClientCertificate(cnAttribute = UNKNOWN_CLIENT_CN, unknownToServer = true) diff --git a/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/AbstractCommonIT.java b/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/AbstractCommonIT.java index a8f9e9e49..c5415a672 100644 --- a/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/AbstractCommonIT.java +++ b/security/vertx-jwt/src/test/java/io/quarkus/ts/security/vertx/AbstractCommonIT.java @@ -43,8 +43,8 @@ public abstract class AbstractCommonIT { static DefaultService redis = new DefaultService().withProperty("ALLOW_EMPTY_PASSWORD", "YES"); @QuarkusApplication(certificates = { - @Certificate(format = Certificate.Format.PEM, prefix = VALID_PEM_PREFIX), - @Certificate(format = Certificate.Format.PEM, prefix = INVALID_PEM_PREFIX) + @Certificate(format = Certificate.Format.PEM, prefix = VALID_PEM_PREFIX, useTlsRegistry = false, configureTruststore = true), + @Certificate(format = Certificate.Format.PEM, prefix = INVALID_PEM_PREFIX, useTlsRegistry = false, configureTruststore = true), }) static RestService app = new RestService() .withProperty("quarkus.redis.hosts", @@ -52,7 +52,7 @@ public abstract class AbstractCommonIT { String redisHost = redis.getURI().withScheme("redis").getRestAssuredStyleUri(); return String.format("%s:%d", redisHost, redis.getURI().getPort()); }) - .withProperty("authN.cert-path", AbstractCommonIT::getCertificatePath); + .withProperty("authN.cert-path", CertificateBuilder.INSTANCE_KEY, AbstractCommonIT::getValidClientCert); @BeforeEach public void setup() { @@ -202,10 +202,6 @@ private static String getPrivateKeyPath(String prefix) { return getPemCertificate(prefix).keyPath(); } - private static String getCertificatePath() { - return getPemCertificate(VALID_PEM_PREFIX).certPath(); - } - private static String getPrivateKey(String prefix) { return CertUtils.loadKey(getPrivateKeyPath(prefix)); } @@ -221,4 +217,8 @@ private static PemCertificate getPemCertificate(String certPrefix) { private static CertificateBuilder getCertBuilder() { return app. getPropertyFromContext(CertificateBuilder.INSTANCE_KEY); } + + private static String getValidClientCert(CertificateBuilder cb) { + return ((PemCertificate) cb.findCertificateByPrefix(VALID_PEM_PREFIX)).certPath(); + } }