From cbc47eda4d0e1a1fea435600b151e4e628259338 Mon Sep 17 00:00:00 2001 From: Michal Karm Babacek Date: Sat, 9 Nov 2024 14:32:34 +0100 Subject: [PATCH] Adds decompressor tests (cherry picked from commit 1aa22234b20c1cd27d624f59ddd823f4839ab430) --- .../compressors/it/AllDecompressResource.java | 7 ++ .../compressors/it/RESTEndpointsTest.java | 92 +++++++++++++- .../src/test/resources/application.properties | 3 + .../compressors/it/DecompressResource.java | 90 ++++++++++++++ .../io/quarkus/compressors/it/Testflow.java | 113 ++++++++++++++---- .../compressors/it/RESTEndpointsTest.java | 4 +- 6 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java create mode 100644 integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java diff --git a/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java b/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java new file mode 100644 index 0000000000000..43f8794d8f3cd --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllDecompressResource.java @@ -0,0 +1,7 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.Path; + +@Path("/decompressed") +public class AllDecompressResource extends DecompressResource { +} diff --git a/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java index 1786ff5b8e7f2..e2a59400de177 100644 --- a/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java +++ b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java @@ -1,6 +1,7 @@ package io.quarkus.compressors.it; -import static io.quarkus.compressors.it.Testflow.runTest; +import static io.quarkus.compressors.it.Testflow.runCompressorsTest; +import static io.quarkus.compressors.it.Testflow.runDecompressorsTest; import java.net.URL; @@ -14,7 +15,10 @@ public class RESTEndpointsTest { @TestHTTPResource(value = "/compressed") - URL url; + URL urlCompressed; + + @TestHTTPResource(value = "/decompressed") + URL urlDEcompressed; @ParameterizedTest @CsvSource(value = { @@ -31,6 +35,88 @@ public class RESTEndpointsTest { //@formatter:on }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") public void testCompressors(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { - runTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + runCompressorsTest(urlCompressed.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + } + + @ParameterizedTest + @CsvSource(value = { + //@formatter:off + // Context | Accept-Encoding | Content-Encoding | Method + "/text | identity | br | POST", + "/text | identity | gzip | POST", + "/text | identity | deflate | POST", + "/text | identity | br | PUT", + "/text | identity | gzip | PUT", + "/text | identity | deflate | PUT", + "/text | deflate | br | POST", + "/text | deflate | gzip | POST", + "/text | deflate | deflate | POST", + "/text | gzip | br | PUT", + "/text | gzip | gzip | PUT", + "/text | gzip | deflate | PUT", + "/text | br | br | POST", + "/text | br | gzip | POST", + "/text | br | deflate | POST", + "/text | br | br | PUT", + "/text | br | gzip | PUT", + "/text | gzip,br,deflate | deflate | PUT", + "/json | identity | br | POST", + "/json | identity | gzip | POST", + "/json | identity | deflate | POST", + "/json | identity | br | PUT", + "/json | identity | gzip | PUT", + "/json | identity | deflate | PUT", + "/json | deflate | br | POST", + "/json | deflate | gzip | POST", + "/json | deflate | deflate | POST", + "/json | gzip | br | PUT", + "/json | gzip | gzip | PUT", + "/json | gzip | deflate | PUT", + "/json | br | br | POST", + "/json | br | gzip | POST", + "/json | br | deflate | POST", + "/json | br | br | PUT", + "/json | br | gzip | PUT", + "/json | gzip,br,deflate | deflate | PUT", + "/xml | identity | br | POST", + "/xml | identity | gzip | POST", + "/xml | identity | deflate | POST", + "/xml | identity | br | PUT", + "/xml | identity | gzip | PUT", + "/xml | identity | deflate | PUT", + "/xml | deflate | br | POST", + "/xml | deflate | gzip | POST", + "/xml | deflate | deflate | POST", + "/xml | gzip | br | PUT", + "/xml | gzip | gzip | PUT", + "/xml | gzip | deflate | PUT", + "/xml | br | br | POST", + "/xml | br | gzip | POST", + "/xml | br | deflate | POST", + "/xml | br | br | PUT", + "/xml | br | gzip | PUT", + "/xml | gzip,br,deflate | deflate | PUT", + "/xhtml | identity | br | POST", + "/xhtml | identity | gzip | POST", + "/xhtml | identity | deflate | POST", + "/xhtml | identity | br | PUT", + "/xhtml | identity | gzip | PUT", + "/xhtml | identity | deflate | PUT", + "/xhtml | deflate | br | POST", + "/xhtml | deflate | gzip | POST", + "/xhtml | deflate | deflate | POST", + "/xhtml | gzip | br | PUT", + "/xhtml | gzip | gzip | PUT", + "/xhtml | gzip | deflate | PUT", + "/xhtml | br | br | POST", + "/xhtml | br | gzip | POST", + "/xhtml | br | deflate | POST", + "/xhtml | br | br | PUT", + "/xhtml | br | gzip | PUT", + "/xhtml | gzip,br,deflate | deflate | PUT" + //@formatter:on + }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") + public void testDecompressors(String endpoint, String acceptEncoding, String contentEncoding, String method) { + runDecompressorsTest(urlDEcompressed.toString() + endpoint, acceptEncoding, contentEncoding, method); } } diff --git a/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties b/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties index d7bbc1ca381a0..9e910a8390a20 100644 --- a/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties +++ b/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties @@ -1,4 +1,7 @@ +# Enables sending clients compressed responses. quarkus.http.enable-compression=true +# Enables decompressing requests from clients. +quarkus.http.enable-decompression=true # Brotli is not present by default, so we add it all here: quarkus.http.compressors=deflate,gzip,br # This test the level actually makes impact. When left to default, diff --git a/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java b/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java new file mode 100644 index 0000000000000..6e1a19bd6f18f --- /dev/null +++ b/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/DecompressResource.java @@ -0,0 +1,90 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * Resource with endpoints that consume compressed data + * in POST and PUT bodies from the client. + * Depending on the accept-encoding, the data is then + * compressed again and sent to the client + *
+ * e.g. Client sends a gzipped POST body and receives + * a brotli compressed response body. + *
+ * The endpoint looks like a dummy echo service, but + * there is compression and decompression going on behind + * the scenes in Vert.x. -> Netty. + *
+ * See: https://github.com/quarkusio/quarkus/pull/44348 + */ +public class DecompressResource { + + @POST + @Path("/text") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String textPost(String text) { + return text; + } + + @PUT + @Path("/text") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String textPut(String text) { + return text; + } + + @POST + @Path("/json") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public String jsonPost(String json) { + return json; + } + + @PUT + @Path("/json") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public String jsonPut(String json) { + return json; + } + + @POST + @Path("/xml") + @Produces(MediaType.TEXT_XML) + @Consumes(MediaType.TEXT_XML) + public String xmlPost(String xml) { + return xml; + } + + @PUT + @Path("/xml") + @Produces(MediaType.TEXT_XML) + @Consumes(MediaType.TEXT_XML) + public String xmlPut(String xml) { + return xml; + } + + @POST + @Path("/xhtml") + @Produces(MediaType.APPLICATION_XHTML_XML) + @Consumes(MediaType.APPLICATION_XHTML_XML) + public String xhtmlPost(String xhtml) { + return xhtml; + } + + @PUT + @Path("/xhtml") + @Produces(MediaType.APPLICATION_XHTML_XML) + @Consumes(MediaType.APPLICATION_XHTML_XML) + public String xhtmlPut(String xhtml) { + return xhtml; + } +} diff --git a/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java b/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java index 378486123faf9..f8a60d61c60ad 100644 --- a/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java +++ b/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java @@ -9,9 +9,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPOutputStream; + +import com.aayushatharva.brotli4j.encoder.BrotliOutputStream; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -34,15 +40,15 @@ public class Testflow { public static final int COMPRESSION_TOLERANCE_PERCENT = 2; /** - * This test logic is shared by both "all" module and "some" module. - * See their RESTEndpointsTest classes. + * This test logic is shared by both "all" module and "some" module. See their RESTEndpointsTest classes. * * @param endpoint * @param acceptEncoding * @param contentEncoding * @param contentLength */ - public static void runTest(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { + public static void runCompressorsTest(String endpoint, String acceptEncoding, String contentEncoding, + String contentLength) { LOG.infof("Endpoint %s; Accept-Encoding: %s; Content-Encoding: %s; Content-Length: %s", endpoint, acceptEncoding, contentEncoding, contentLength); // RestAssured @@ -97,31 +103,88 @@ public static void runTest(String endpoint, String acceptEncoding, String conten expectedLength + " plus " + COMPRESSION_TOLERANCE_PERCENT + "% tolerance, i.e. " + expectedLengthWithTolerance + "."); } + assertEquals(TEXT, decompress(actualEncoding, response.body().getBytes()), "Unexpected body text."); + } catch (InterruptedException | ExecutionException e) { + fail(e); + } + } - final String body; - if (actualEncoding != null && !"identity".equalsIgnoreCase(actualEncoding)) { - EmbeddedChannel channel = null; - if ("gzip".equalsIgnoreCase(actualEncoding)) { - channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.GZIP)); - } else if ("deflate".equalsIgnoreCase(actualEncoding)) { - channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.ZLIB)); - } else if ("br".equalsIgnoreCase(actualEncoding)) { - channel = new EmbeddedChannel(new BrotliDecoder()); - } else { - fail("Unexpected compression used by server: " + actualEncoding); - } - channel.writeInbound(Unpooled.copiedBuffer(response.body().getBytes())); - channel.finish(); - final ByteBuf decompressed = channel.readInbound(); - body = decompressed.readCharSequence(decompressed.readableBytes(), StandardCharsets.UTF_8).toString(); - } else { - body = response.body().toString(StandardCharsets.UTF_8); - } - - assertEquals(TEXT, body, - "Unexpected body text."); + public static void runDecompressorsTest(String endpoint, String acceptEncoding, String contentEncoding, + String method) { + LOG.infof("Endpoint %s; Accept-Encoding: %s; Content-Encoding: %s; Method: %s", + endpoint, acceptEncoding, contentEncoding, method); + final WebClient client = WebClient.create(Vertx.vertx(), new WebClientOptions() + .setLogActivity(true) + .setFollowRedirects(true) + .setDecompressionSupported(false)); + final CompletableFuture> future = new CompletableFuture<>(); + client.postAbs(endpoint) + .putHeader(HttpHeaders.CONTENT_ENCODING.toString(), contentEncoding) + .putHeader(HttpHeaders.ACCEPT.toString(), "*/*") + .sendBuffer(compress(contentEncoding, TEXT), ar -> { + if (ar.succeeded()) { + future.complete(ar.result()); + } else { + future.completeExceptionally(ar.cause()); + } + }); + try { + final HttpResponse response = future.get(); + final String actualEncoding = response.headers().get("content-encoding"); + final String body = decompress(actualEncoding, response.body().getBytes()); + assertEquals(OK.code(), response.statusCode(), "Http status must be OK."); + assertEquals(TEXT, body, "Unexpected body text."); } catch (InterruptedException | ExecutionException e) { fail(e); } } + + public static Buffer compress(String algorithm, String payload) { + final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + if ("gzip".equalsIgnoreCase(algorithm)) { + try (GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream)) { + gzipStream.write(payload.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Gzip compression failed", e); + } + return Buffer.buffer(byteStream.toByteArray()); + } else if ("br".equalsIgnoreCase(algorithm)) { + try (BrotliOutputStream brotliStream = new BrotliOutputStream(byteStream)) { + brotliStream.write(payload.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Brotli compression failed", e); + } + return Buffer.buffer(byteStream.toByteArray()); + } else if ("deflate".equalsIgnoreCase(algorithm)) { + try (DeflaterOutputStream deflateStream = new DeflaterOutputStream(byteStream)) { + deflateStream.write(payload.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Deflate compression failed", e); + } + return Buffer.buffer(byteStream.toByteArray()); + } else { + throw new IllegalArgumentException("Unsupported encoding: " + algorithm); + } + } + + public static String decompress(String algorithm, byte[] payload) { + if (algorithm != null && !"identity".equalsIgnoreCase(algorithm)) { + final EmbeddedChannel channel; + if ("gzip".equalsIgnoreCase(algorithm)) { + channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.GZIP)); + } else if ("deflate".equalsIgnoreCase(algorithm)) { + channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.ZLIB)); + } else if ("br".equalsIgnoreCase(algorithm)) { + channel = new EmbeddedChannel(new BrotliDecoder()); + } else { + throw new RuntimeException("Unexpected compression used by server: " + algorithm); + } + channel.writeInbound(Unpooled.copiedBuffer(payload)); + channel.finish(); + final ByteBuf decompressed = channel.readInbound(); + return decompressed.readCharSequence(decompressed.readableBytes(), StandardCharsets.UTF_8).toString(); + } else { + return new String(payload, StandardCharsets.UTF_8); + } + } } diff --git a/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java index d2f9efc94e39c..9763e151fd69f 100644 --- a/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java +++ b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java @@ -1,6 +1,6 @@ package io.quarkus.compressors.it; -import static io.quarkus.compressors.it.Testflow.runTest; +import static io.quarkus.compressors.it.Testflow.runCompressorsTest; import java.net.URL; @@ -30,6 +30,6 @@ public class RESTEndpointsTest { //@formatter:on }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") public void testCompressors(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { - runTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + runCompressorsTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); } }