BasicHelidonConnectorTest.java

/*
 * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.helidon.connector;

import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.JerseyCompletionStageRxInvoker;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.spi.TestHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class BasicHelidonConnectorTest {

    @Path("basic")
    public static class BasicResource {
        @Path("get")
        @GET
        public String get() {
            return "ok";
        }

        @Path("getquery")
        @GET
        public String getQuery(@QueryParam("first") String first, @QueryParam("second") String second) {
            return first + second;
        }

        @POST
        @Path("post")
        public String post(String entity) {
            return entity + entity;
        }

        @GET
        @Path("headers")
        public Response headers(@Context HttpHeaders headers) {
            Response.ResponseBuilder response = Response.ok("ok");
            for (Map.Entry<String, List<String>> set : headers.getRequestHeaders().entrySet()) {
                if (set.getKey().toUpperCase(Locale.ROOT).startsWith("X-TEST")) {
                    response.header(set.getKey(), set.getValue().iterator().next());
                }
            }
            return response.build();
        }

        @PUT
        @Consumes("test/x-test")
        @Produces("test/y-test")
        @Path("produces/consumes")
        public String putConsumesProduces(String content) {
            return content + content;
        }
    }

    public static List<String> data() {
        return Arrays.asList("BYTE_ARRAY_OUTPUT_STREAM", "READABLE_BYTE_CHANNEL", "OUTPUT_STREAM_PUBLISHER");
    }

    @Path("async")
    public static class AsyncResource {
        private static CountDownLatch shortLong = null;

        @GET
        @Path("reset")
        public void reset() {
            shortLong = new CountDownLatch(1);
        }

        @Path("long")
        @GET
        public String longGet() throws InterruptedException {
            shortLong.await(10000, TimeUnit.MILLISECONDS);
            return shortLong.getCount() == 0 ? "long" : "shortLong CountDownLatch has not been hit";
        }

        @Path("short")
        @GET
        public String shortGet() {
            shortLong.countDown();
            return "short";
        }
    }

    @TestFactory
    public Collection<DynamicContainer> generateTests() {
        Collection<DynamicContainer> tests = new ArrayList<>();
        data().forEach(entityType -> {
            BasicHelidonConnectorTemplateTest test = new BasicHelidonConnectorTemplateTest(entityType) {};
            tests.add(TestHelper.toTestContainer(test, entityType));
        });
        return tests;
    }

    public abstract static class BasicHelidonConnectorTemplateTest extends JerseyTest {

        private final String entityType;

        public BasicHelidonConnectorTemplateTest(String entityType) {
            this.entityType = entityType;
        }

        @Override
        protected Application configure() {
            return new ResourceConfig(BasicResource.class, AsyncResource.class)
                    .property(LoggingFeature.LOGGING_FEATURE_LOGGER_LEVEL_SERVER, "WARNING");
        }

        @Override
        protected void configureClient(ClientConfig config) {
            super.configureClient(config);
            config.connectorProvider(new HelidonConnectorProvider());
            config.property("jersey.config.helidon.client.entity.type", entityType);
        }

        @Test
        public void testBasicGet() {
            try (Response response = target("basic").path("get").request().get()) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("ok", response.readEntity(String.class));
            }
        }

        @Test
        public void testBasicPost() {
            try (Response response = target("basic").path("post").request()
                    .buildPost(Entity.entity("ok", MediaType.TEXT_PLAIN_TYPE)).invoke()) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("okok", response.readEntity(String.class));
            }
        }

        @Test
        public void queryGetTest() {
            try (Response response = target("basic").path("getquery")
                    .queryParam("first", "hello")
                    .queryParam("second", "world")
                    .request().get()) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("helloworld", response.readEntity(String.class));
            }
        }

        @Test
        public void testHeaders() {
            String[][] headers = new String[][]{{"X-TEST-ONE", "ONE"}, {"X-TEST-TWO", "TWO"}, {"X-TEST-THREE", "THREE"}};
            MultivaluedHashMap<String, Object> map = new MultivaluedHashMap<>();
            Arrays.stream(headers).forEach(a -> map.add(a[0], a[1]));
            try (Response response = target("basic").path("headers").request().headers(map).get()) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("ok", response.readEntity(String.class));
                for (int i = 0; i != headers.length; i++) {
                    Assertions.assertTrue(response.getHeaders().containsKey(headers[i][0]));
                    Assertions.assertEquals(headers[i][1], response.getStringHeaders().getFirst(headers[i][0]));
                }
            }
        }

        @Test
        public void testProduces() {
            try (Response response = target("basic").path("produces/consumes").request("test/z-test")
                    .put(Entity.entity("ok", new MediaType("test", "x-test")))) {
                Assertions.assertEquals(406, response.getStatus());
            }

            try (Response response = target("basic").path("produces/consumes").request()
                    .put(Entity.entity("ok", new MediaType("test", "x-test")))) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("okok", response.readEntity(String.class));
                Assertions.assertEquals("test/y-test", response.getStringHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
            }
        }

        @Test
        public void testAsyncGet() throws ExecutionException, InterruptedException {
            Future<Response> futureResponse = target("basic").path("get").request().async().get();
            try (Response response = futureResponse.get()) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("ok", response.readEntity(String.class));
            }
        }

        @Test
        public void testConsumes() {
            try (Response response = target("basic").path("produces/consumes").request("test/y-test")
                    .put(Entity.entity("ok", new MediaType("test", "z-test")))) {
                Assertions.assertEquals(415, response.getStatus());
            }

            try (Response response = target("basic").path("produces/consumes").request("test/y-test")
                    .put(Entity.entity("ok", MediaType.WILDCARD_TYPE))) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("okok", response.readEntity(String.class));
                Assertions.assertEquals("test/y-test", response.getStringHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
            }
        }

        @Test
        public void testRxGet() throws ExecutionException, InterruptedException {
            CompletableFuture<Response> futureResponse =
                    target("basic").path("get").request().rx(JerseyCompletionStageRxInvoker.class).get();

            try (Response response = futureResponse.get()) {
                Assertions.assertEquals(200, response.getStatus());
                Assertions.assertEquals("ok", response.readEntity(String.class));
            }
        }

        @Test
        public void testInputStreamEntity() throws IOException {
            try (Response response = target("basic").path("get").request().get()) {
                Assertions.assertEquals(200, response.getStatus());
                InputStream is = response.readEntity(InputStream.class);
                Assertions.assertEquals('o', is.read());
                Assertions.assertEquals('k', is.read());
                is.close();
            }
        }

        // -----------Async

        @Test
        public void testTwoClientsAsync() throws ExecutionException, InterruptedException {
            try (Response resetResponse = target("async").path("reset").request().get()) {
                Assertions.assertEquals(204, resetResponse.getStatus());
            }

            ClientConfig config = new ClientConfig();
            config.connectorProvider(new HelidonConnectorProvider());

            Client longClient = ClientBuilder.newClient(config);
            Invocation.Builder longRequest = longClient.target(getBaseUri()).path("async/long").request();

            Client shortClient = ClientBuilder.newClient(config);
            Invocation.Builder shortRequest = shortClient.target(getBaseUri()).path("async/short").request();

            Future<Response> futureLongResponse = longRequest.async().get();
            Future<Response> futureShortResponse = shortRequest.async().get();

            try (Response shortResponse = futureShortResponse.get()) {
                Assertions.assertEquals(200, shortResponse.getStatus());
                Assertions.assertEquals("short", shortResponse.readEntity(String.class));
            }

            try (Response longResponse = futureLongResponse.get()) {
                Assertions.assertEquals(200, longResponse.getStatus());
                Assertions.assertEquals("long", longResponse.readEntity(String.class));
            }
        }

        @Test
        public void testOneClientsTwoReqestsAsync() throws ExecutionException, InterruptedException {
            try (Response resetResponse = target("async").path("reset").request().get()) {
                Assertions.assertEquals(204, resetResponse.getStatus());
            }

            Invocation.Builder longRequest = target().path("async/long").request();
            Invocation.Builder shortRequest = target().path("async/short").request();

            Future<Response> futureLongResponse = longRequest.async().get();
            Future<Response> futureShortResponse = shortRequest.async().get();

            try (Response shortResponse = futureShortResponse.get()) {
                Assertions.assertEquals(200, shortResponse.getStatus());
                Assertions.assertEquals("short", shortResponse.readEntity(String.class));
            }

            try (Response longResponse = futureLongResponse.get()) {
                Assertions.assertEquals(200, longResponse.getStatus());
                Assertions.assertEquals("long", longResponse.readEntity(String.class));
            }
        }

        @Test
        public void testOptionsWithEntity() {
            Response response = target("basic").path("get").request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
            assertEquals(200, response.getStatus());
            response.close();
        }
    }
}