diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 7796a2493cd..0921656f7f3 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -1158,6 +1158,11 @@ logback-classic ${version.lib.logback} + + ch.qos.logback + logback-core + ${version.lib.logback} + org.apache.activemq activemq-client diff --git a/examples/todo-app/backend/pom.xml b/examples/todo-app/backend/pom.xml index 940467e3746..ca05f12a5e2 100644 --- a/examples/todo-app/backend/pom.xml +++ b/examples/todo-app/backend/pom.xml @@ -38,6 +38,10 @@ io.helidon.demo.todos.backend.Main 3.10.2 + 4.3.1.0 + 4.9.0 + 4.9.0 + 3.0.2 @@ -85,6 +89,45 @@ com.datastax.cassandra cassandra-driver-core + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + + + io.helidon.config + helidon-config-mp + test + + + org.cassandraunit + cassandra-unit + ${version.cassandra.unit} + test + + + com.datastax.oss + java-driver-core + ${version.datastax.driver.core} + test + + + com.datastax.oss + java-driver-query-builder + ${version.datastax.driver.query.builder} + test + + + com.codahale.metrics + metrics-core + ${version.codahale.metrics.core} + test + diff --git a/examples/todo-app/backend/src/test/java/io/helidon/demo/todos/backend/BackendTests.java b/examples/todo-app/backend/src/test/java/io/helidon/demo/todos/backend/BackendTests.java new file mode 100644 index 00000000000..ca6aa8ac21b --- /dev/null +++ b/examples/todo-app/backend/src/test/java/io/helidon/demo/todos/backend/BackendTests.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.demo.todos.backend; + +import java.io.IOException; +import java.util.Base64; +import java.util.Properties; + +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; + +import io.helidon.common.http.Http; +import io.helidon.config.mp.MpConfigSources; +import io.helidon.config.yaml.mp.YamlMpConfigSource; +import io.helidon.microprofile.tests.junit5.Configuration; +import io.helidon.microprofile.tests.junit5.HelidonTest; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import org.cassandraunit.utils.EmbeddedCassandraServerHelper; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@HelidonTest +@Configuration(useExisting = true) +class BackendTests { + + private final static String CASSANDRA_HOST = "127.0.0.1"; + + @Inject + private WebTarget webTarget; + + @BeforeAll + static void init() throws IOException { + Properties cassandraProperties = initCassandra(); + + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + ConfigProviderResolver configResolver = ConfigProviderResolver.instance(); + + org.eclipse.microprofile.config.Config mpConfig = configResolver.getBuilder() + .withSources(YamlMpConfigSource.create(cl.getResource("test-application.yaml")), + MpConfigSources.create(cassandraProperties)) + .build(); + + configResolver.registerConfig(mpConfig, null); + } + + @AfterAll + static void stopServer() { + EmbeddedCassandraServerHelper.cleanEmbeddedCassandra(); + } + + private static Properties initCassandra() throws IOException { + EmbeddedCassandraServerHelper.startEmbeddedCassandra(EmbeddedCassandraServerHelper.CASSANDRA_RNDPORT_YML_FILE, + 20000L); + Properties prop = new Properties(); + prop.put("cassandra.port", String.valueOf(EmbeddedCassandraServerHelper.getNativeTransportPort())); + prop.put("cassandra.servers.host.host", CASSANDRA_HOST); + + Cluster cluster = Cluster.builder() + .withoutMetrics() + .addContactPoint(CASSANDRA_HOST) + .withPort(EmbeddedCassandraServerHelper.getNativeTransportPort()) + .build(); + + Session session = cluster.connect(); + session.execute("CREATE KEYSPACE backend WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"); + session.execute( + "CREATE TABLE backend.backend (id ascii, user ascii, message ascii, completed Boolean, created timestamp, " + + "PRIMARY KEY (id));"); + session.execute("select * from backend.backend;"); + + session.close(); + cluster.close(); + + return prop; + } + + @Test + void testTodoScenario() { + String basicAuth = "Basic " + Base64.getEncoder().encodeToString("john:password".getBytes()); + JsonObject todo = Json.createObjectBuilder() + .add("title", "todo title") + .build(); + + // Add a new todo + JsonObject returnedTodo = webTarget + .path("/api/backend") + .request(MediaType.APPLICATION_JSON_TYPE) + .header(Http.Header.AUTHORIZATION, basicAuth) + .post(Entity.json(todo), JsonObject.class); + + assertEquals("john", returnedTodo.getString("user")); + assertEquals(todo.getString("title"), returnedTodo.getString("title")); + + // Get the todo created earlier + JsonObject fromServer = webTarget.path("/api/backend/" + returnedTodo.getString("id")) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(Http.Header.AUTHORIZATION, basicAuth) + .get(JsonObject.class); + + assertEquals(returnedTodo, fromServer); + + // Update the todo created earlier + JsonObject updatedTodo = Json.createObjectBuilder() + .add("title", "updated title") + .add("completed", false) + .build(); + + fromServer = webTarget.path("/api/backend/" + returnedTodo.getString("id")) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(Http.Header.AUTHORIZATION, basicAuth) + .put(Entity.json(updatedTodo), JsonObject.class); + + assertEquals(updatedTodo.getString("title"), fromServer.getString("title")); + + // Delete the todo created earlier + fromServer = webTarget.path("/api/backend/" + returnedTodo.getString("id")) + .request(MediaType.APPLICATION_JSON_TYPE) + .header(Http.Header.AUTHORIZATION, basicAuth) + .delete(JsonObject.class); + + assertEquals(returnedTodo.getString("id"), fromServer.getString("id")); + + // Get list of todos + JsonArray jsonValues = webTarget.path("/api/backend") + .request(MediaType.APPLICATION_JSON_TYPE) + .header(Http.Header.AUTHORIZATION, basicAuth) + .get(JsonArray.class); + + assertEquals(0, jsonValues.size(), "There should be no todos on server"); + } + +} diff --git a/examples/todo-app/backend/src/test/resources/test-application.yaml b/examples/todo-app/backend/src/test/resources/test-application.yaml new file mode 100644 index 00000000000..aa071626dd7 --- /dev/null +++ b/examples/todo-app/backend/src/test/resources/test-application.yaml @@ -0,0 +1,40 @@ +# +# Copyright (c) 2021 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# increase importance +config_ordinal: 500 + +# we use custom config and Helidon JUnit integration, must allow initializer +mp: + initializer: + allow: true + no-warn: true + +server: + port: 0 + host: localhost + +tracing: + service: "todo:back" + enabled: false + +security: + providers: + - http-basic-auth: + realm: "helidon" + users: + - login: "john" + password: "password" diff --git a/examples/todo-app/frontend/pom.xml b/examples/todo-app/frontend/pom.xml index df26657ec6f..d0b743227bc 100644 --- a/examples/todo-app/frontend/pom.xml +++ b/examples/todo-app/frontend/pom.xml @@ -121,6 +121,21 @@ org.glassfish.jersey.core jersey-common + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.webserver + helidon-webserver-jersey + test + + + io.helidon.webclient + helidon-webclient + test + diff --git a/examples/todo-app/frontend/src/test/java/io/helidon/demo/todos/frontend/FrontendTest.java b/examples/todo-app/frontend/src/test/java/io/helidon/demo/todos/frontend/FrontendTest.java new file mode 100644 index 00000000000..23d69ad0684 --- /dev/null +++ b/examples/todo-app/frontend/src/test/java/io/helidon/demo/todos/frontend/FrontendTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.demo.todos.frontend; + +import io.helidon.common.http.Http; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.media.jsonp.JsonpSupport; +import io.helidon.security.Security; +import io.helidon.security.integration.webserver.WebSecurity; +import io.helidon.webclient.WebClient; +import io.helidon.webserver.Routing; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.jersey.JerseySupport; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +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.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Base64; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static io.helidon.config.ConfigSources.classpath; + +public class FrontendTest { + + private static WebServer serverBackend; + private static WebServer serverFrontend; + private static WebClient client; + private static final JsonObject TODO = Json.createObjectBuilder().add("msg", "todo").build(); + private static final String ENCODED_ID = Base64.getEncoder().encodeToString("john:password".getBytes()); + + @Path("/api/backend") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public static class FakeBackendService { + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getAllTodo() { + JsonArray jsonArray = Json.createArrayBuilder().add(TODO).build(); + return Response.ok(jsonArray, MediaType.APPLICATION_JSON).build(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createTodo(JsonObject object) { + return Response.ok(object, MediaType.APPLICATION_JSON).build(); + } + + @GET + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Response getTodo() { + return Response.ok(TODO, MediaType.APPLICATION_JSON).build(); + } + + @DELETE + @Path("/{id}") + @Produces(MediaType.APPLICATION_JSON) + public Response deleteTodo() { + return Response.ok(TODO, MediaType.APPLICATION_JSON).build(); + } + + @PUT + @Path("/{id}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response updateTodo(JsonObject object) { + return Response.ok(object, MediaType.APPLICATION_JSON).build(); + } + } + + @BeforeAll + public static void init() { + startBackendServer(); + startFrontendServer(); + client = WebClient.builder() + .baseUri("http://localhost:" + serverFrontend.port()) + .addMediaSupport(JsonpSupport.create()) + .build(); + } + + @AfterAll + public static void stopServers() { + serverBackend.shutdown(); + serverFrontend.shutdown(); + } + + private static void startBackendServer() { + serverBackend = WebServer.builder(createRouting()) + .port(0) + .addMediaSupport(JsonpSupport.create()) + .build(); + + serverBackend.start(); + + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + + private static Routing createRouting() { + return Routing.builder() + .register("/", JerseySupport.builder() + .register(FakeBackendService.class) + .build()) + .build(); + } + + private static void startFrontendServer() { + Properties prop = new Properties(); + prop.put("services.backend.endpoint", "http://127.0.0.1:" + serverBackend.port()); + Config config = Config.builder() + .sources(List.of( + classpath("frontend-application.yaml"), + ConfigSources.create(prop) + )) + .build(); + Client client = ClientBuilder.newClient(); + BackendServiceClient bsc = new BackendServiceClient(client, config); + + serverFrontend = WebServer.builder(createRouting( + Security.create(config.get("security")), + config, + bsc)) + .config(config.get("webserver")) + .addMediaSupport(JsonpSupport.create()) + .build(); + + serverFrontend.start(); + } + + private static Routing createRouting(Security security, Config config, BackendServiceClient bsc) { + return Routing.builder() + .register(WebSecurity.create(security, config.get("security"))) + .register("/env", new EnvHandler(config)) + .register("/api", new TodosHandler(bsc)) + .build(); + } + + @Test + public void testGetList() throws ExecutionException, InterruptedException { + client.get() + .path("/api/todo") + .headers(headers -> { + headers.add(Http.Header.AUTHORIZATION, "Basic " + ENCODED_ID); + return headers; + }) + .request(JsonArray.class) + .thenAccept(jsonValues -> { + Assertions.assertEquals(TODO, jsonValues.getJsonObject(0)); + }) + .toCompletableFuture() + .get(); + } + + @Test + public void testPostTodo() throws ExecutionException, InterruptedException { + client.post() + .path("/api/todo") + .headers(headers -> { + headers.add(Http.Header.AUTHORIZATION, "Basic " + ENCODED_ID); + return headers; + }) + .submit(TODO, JsonObject.class) + .thenAccept(jsonObject -> { + Assertions.assertEquals(TODO, jsonObject); + }) + .toCompletableFuture() + .get(); + } + + @Test + public void testGetTodo() throws ExecutionException, InterruptedException { + client.get() + .path("/api/todo/1") + .headers(headers -> { + headers.add(Http.Header.AUTHORIZATION, "Basic " + ENCODED_ID); + return headers; + }) + .request(JsonObject.class) + .thenAccept(jsonObject -> { + Assertions.assertEquals(TODO, jsonObject); + }) + .toCompletableFuture() + .get(); + } + + @Test + public void testDeleteTodo() throws ExecutionException, InterruptedException { + client.delete() + .path("/api/todo/1") + .headers(headers -> { + headers.add(Http.Header.AUTHORIZATION, "Basic " + ENCODED_ID); + return headers; + }) + .request(JsonObject.class) + .thenAccept(jsonObject -> { + Assertions.assertEquals(TODO, jsonObject); + }) + .toCompletableFuture() + .get(); + } + + @Test + public void testUpdateTodo() throws ExecutionException, InterruptedException { + client.put() + .path("/api/todo/1") + .headers(headers -> { + headers.add(Http.Header.AUTHORIZATION, "Basic " + ENCODED_ID); + return headers; + }) + .submit(TODO, JsonObject.class) + .thenAccept(jsonObject -> { + Assertions.assertEquals(TODO, jsonObject); + }) + .toCompletableFuture() + .get(); + } + + @Test + public void testEnvHandler() throws ExecutionException, InterruptedException { + client.get() + .path("/env") + .request(String.class) + .thenAccept(s -> { + Assertions.assertEquals("docker", s); + }) + .toCompletableFuture() + .get(); + } + +} diff --git a/examples/todo-app/frontend/src/test/resources/frontend-application.yaml b/examples/todo-app/frontend/src/test/resources/frontend-application.yaml new file mode 100644 index 00000000000..7023ef634b4 --- /dev/null +++ b/examples/todo-app/frontend/src/test/resources/frontend-application.yaml @@ -0,0 +1,32 @@ +# +# Copyright (c) 2021 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +env: docker + +webserver: + port: 0 + +security: + providers: + - http-basic-auth: + realm: "helidon" + users: + - login: "john" + password: "password" + web-server: + paths: + - path: "/api/{+}" + authenticate: true \ No newline at end of file