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