From deff579ccf34f5e55512a2fc4c4141ab813e9bfe Mon Sep 17 00:00:00 2001 From: Michael Halberstadt Date: Tue, 14 Jun 2022 10:48:35 +0200 Subject: [PATCH] feat: make ServerVerticle handler configurable The `ServerVerticle` attaches request handlers to the route. The request handlers attached to the route cannot be changed at the moment. This change makes the request handlers that the `ServerVerticle` attaches to the route configurable. A `RoutingHandlerFactory` is used to create an instance of the `Handler` class. In the `ServerConfig`, a list of `RoutingHandlerFactory` class names is used to specify the `RoutingHandlerFactory`. The handler created by the `RoutingHandlerFactory` is then added to the root route. The request handlers are added in the order specified in the `ServerConfig` list. --- docs/ServerVerticle.md | 25 ++++ ...ernal.verticle.ServerVerticle.example.yaml | 12 +- .../neonbee/config/ServerConfigConverter.java | 15 +++ .../java/io/neonbee/config/ServerConfig.java | 51 +++++++- .../factories/CacheControlHandlerFactory.java | 19 +++ .../CorrelationIdHandlerFactory.java | 22 ++++ ...sallowingFileUploadBodyHandlerFactory.java | 19 +++ .../factories/InstanceInfoHandlerFactory.java | 19 +++ .../factories/LoggerHandlerFactory.java | 19 +++ .../factories/RoutingHandlerFactory.java | 22 ++++ .../factories/SessionHandlerFactory.java | 69 +++++++++++ .../factories/TimeoutHandlerFactory.java | 25 ++++ .../internal/verticle/ServerVerticle.java | 95 +++++++-------- .../io/neonbee/config/ServerConfigTest.java | 22 ++++ .../CacheControlHandlerFactoryTest.java | 23 ++++ .../CorrelationIdHandlerFactoryTest.java | 37 ++++++ ...owingFileUploadBodyHandlerFactoryTest.java | 22 ++++ .../InstanceInfoHandlerFactoryTest.java | 22 ++++ .../factories/LoggerHandlerFactoryTest.java | 22 ++++ .../factories/SessionHandlerFactoryTest.java | 111 ++++++++++++++++++ .../factories/TimeoutHandlerFactoryTest.java | 36 ++++++ .../internal/verticle/ServerVerticleTest.java | 99 ++++++++++------ 22 files changed, 717 insertions(+), 89 deletions(-) create mode 100644 docs/ServerVerticle.md create mode 100644 src/main/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/LoggerHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/RoutingHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java create mode 100644 src/main/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactory.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactoryTest.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactoryTest.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactoryTest.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactoryTest.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/LoggerHandlerFactoryTest.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/SessionHandlerFactoryTest.java create mode 100644 src/test/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactoryTest.java diff --git a/docs/ServerVerticle.md b/docs/ServerVerticle.md new file mode 100644 index 00000000..c92e29a4 --- /dev/null +++ b/docs/ServerVerticle.md @@ -0,0 +1,25 @@ +# ServerVerticle configuration + +## Configuration + +### Handler factories + +Handler factories are used to register routing handlers on the root router in the ServerVerticle. A handler factory must +implement the `io.neonbee.internal.handler.factories.RoutingHandlerFactory` interface and must be configured in the +handlerFactories section of the ServerVerticle configuration. The configured handlerFactories are loaded and added in +the configured order. +The order of handler factories has to take the priority of the returned handlers into account. +See `io.vertx.ext.web.impl.RouteState.weight` If the order is violated, an Exception is thrown. + +#### Default configuration + +```yaml +handlerFactories: +- io.neonbee.internal.handler.factories.LoggerHandlerFactory +- io.neonbee.internal.handler.factories.InstanceInfoHandlerFactory +- io.neonbee.internal.handler.factories.CorrelationIdHandlerFactory +- io.neonbee.internal.handler.factories.TimeoutHandlerFactory +- io.neonbee.internal.handler.factories.SessionHandlerFactory +- io.neonbee.internal.handler.factories.CacheControlHandlerFactory +- io.neonbee.internal.handler.factories.DisallowingFileUploadBodyHandlerFactory +``` diff --git a/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml b/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml index 5bcbe173..2db6957b 100644 --- a/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml +++ b/resources/config/io.neonbee.internal.verticle.ServerVerticle.example.yaml @@ -90,4 +90,14 @@ config: provider: # the authentication provider to be set for this handler# any of: HTDIGEST, HTPASSWD, JDBC, JWT, MONGO, OAUTH2, mandatory attribute type: string - # ... more authentication provider options (see the specific provider implementations) \ No newline at end of file + # ... more authentication provider options (see the specific provider implementations) + + # default handler factories. The order of handler factories has to take the priority of the returned handlers into account. + handlerFactories: + - io.neonbee.internal.handler.factories.LoggerHandlerFactory + - io.neonbee.internal.handler.factories.InstanceInfoHandlerFactory + - io.neonbee.internal.handler.factories.CorrelationIdHandlerFactory + - io.neonbee.internal.handler.factories.TimeoutHandlerFactory + - io.neonbee.internal.handler.factories.SessionHandlerFactory + - io.neonbee.internal.handler.factories.CacheControlHandlerFactory + - io.neonbee.internal.handler.factories.DisallowingFileUploadBodyHandlerFactory \ No newline at end of file diff --git a/src/generated/java/io/neonbee/config/ServerConfigConverter.java b/src/generated/java/io/neonbee/config/ServerConfigConverter.java index dcaab50c..a0aa4b5f 100644 --- a/src/generated/java/io/neonbee/config/ServerConfigConverter.java +++ b/src/generated/java/io/neonbee/config/ServerConfigConverter.java @@ -55,6 +55,16 @@ static void fromJson(Iterable> json, ServerC obj.setErrorHandlerTemplate((String) member.getValue()); } break; + case "handlerFactoriesClassNames": + if (member.getValue() instanceof JsonArray) { + java.util.ArrayList list = new java.util.ArrayList<>(); + ((Iterable) member.getValue()).forEach(item -> { + if (item instanceof String) + list.add((String) item); + }); + obj.setHandlerFactoriesClassNames(list); + } + break; case "sessionCookieName": if (member.getValue() instanceof String) { obj.setSessionCookieName((String) member.getValue()); @@ -104,6 +114,11 @@ static void toJson(ServerConfig obj, java.util.Map json) { if (obj.getErrorHandlerTemplate() != null) { json.put("errorHandlerTemplate", obj.getErrorHandlerTemplate()); } + if (obj.getHandlerFactoriesClassNames() != null) { + JsonArray array = new JsonArray(); + obj.getHandlerFactoriesClassNames().forEach(item -> array.add(item)); + json.put("handlerFactoriesClassNames", array); + } if (obj.getSessionCookieName() != null) { json.put("sessionCookieName", obj.getSessionCookieName()); } diff --git a/src/main/java/io/neonbee/config/ServerConfig.java b/src/main/java/io/neonbee/config/ServerConfig.java index 381d35be..23400bef 100644 --- a/src/main/java/io/neonbee/config/ServerConfig.java +++ b/src/main/java/io/neonbee/config/ServerConfig.java @@ -21,6 +21,14 @@ import io.neonbee.endpoint.odatav4.ODataV4Endpoint; import io.neonbee.endpoint.raw.RawEndpoint; import io.neonbee.internal.handler.DefaultErrorHandler; +import io.neonbee.internal.handler.factories.CacheControlHandlerFactory; +import io.neonbee.internal.handler.factories.CorrelationIdHandlerFactory; +import io.neonbee.internal.handler.factories.DisallowingFileUploadBodyHandlerFactory; +import io.neonbee.internal.handler.factories.InstanceInfoHandlerFactory; +import io.neonbee.internal.handler.factories.LoggerHandlerFactory; +import io.neonbee.internal.handler.factories.RoutingHandlerFactory; +import io.neonbee.internal.handler.factories.SessionHandlerFactory; +import io.neonbee.internal.handler.factories.TimeoutHandlerFactory; import io.neonbee.internal.json.ImmutableJsonObject; import io.neonbee.internal.verticle.ServerVerticle; import io.vertx.codegen.annotations.DataObject; @@ -108,6 +116,7 @@ @DataObject(generateConverter = true, publicConverter = false) @SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.CyclomaticComplexity", "PMD.TooManyMethods", "PMD.GodClass" }) public class ServerConfig extends HttpServerOptions { + public enum SessionHandling { /** * No session handling. @@ -115,6 +124,8 @@ public enum SessionHandling { NONE, /** * Local session handling or in clustered operation on each cluster node. + * + * Sessions are stored locally in a shared local map and only available on this instance. */ LOCAL, /** @@ -196,15 +207,24 @@ public String getCorrelationId(RoutingContext routingContext) { */ public static final String DEFAULT_SESSION_COOKIE_NAME = "neonbee-web.session"; + /** + * List of instances of {@link RoutingHandlerFactory} that are loaded by default. + */ + public static final List DEFAULT_HANDLER_FACTORIES_CLASS_NAMES = + List.of(LoggerHandlerFactory.class.getName(), InstanceInfoHandlerFactory.class.getName(), + CorrelationIdHandlerFactory.class.getName(), TimeoutHandlerFactory.class.getName(), + SessionHandlerFactory.class.getName(), CacheControlHandlerFactory.class.getName(), + DisallowingFileUploadBodyHandlerFactory.class.getName()); + private static final String PROPERTY_PORT = "port"; private static final List DEFAULT_ENDPOINT_CONFIGS = Collections.unmodifiableList(Stream .of(RawEndpoint.class, ODataV4Endpoint.class, MetricsEndpoint.class, HealthEndpoint.class) .map(endpointClass -> new EndpointConfig().setType(endpointClass.getName())).collect(Collectors.toList())); - private static final ImmutableBiMap REPHRASE_MAP = - ImmutableBiMap.of("endpointConfigs", "endpoints", "authChainConfig", "authenticationChain", - "errorHandlerClassName", "errorHandler", "errorHandlerTemplate", "errorTemplate"); + private static final ImmutableBiMap REPHRASE_MAP = ImmutableBiMap.of("endpointConfigs", "endpoints", + "authChainConfig", "authenticationChain", "errorHandlerClassName", "errorHandler", "errorHandlerTemplate", + "errorTemplate", "handlerFactoriesClassNames", "handlerFactories"); private int timeout = DEFAULT_TIMEOUT; @@ -220,6 +240,8 @@ public String getCorrelationId(RoutingContext routingContext) { private List authChainConfig; + private List handlerFactoriesClassNames = DEFAULT_HANDLER_FACTORIES_CLASS_NAMES; + private String errorHandlerClassName; private String errorHandlerTemplate; @@ -412,6 +434,29 @@ public ServerConfig setAuthChainConfig(List authChainConfig) return this; } + /** + * Returns a list of {@link RoutingHandlerFactory} names used to instantiate the handler objects that will be added + * to the root route. The handlers are added to the route in the order in which they are specified. The class must + * implement the {@link RoutingHandlerFactory} interface and must provide a default constructor. + * + * @return list of {@link RoutingHandlerFactory} names. + */ + public List getHandlerFactoriesClassNames() { + return handlerFactoriesClassNames; + } + + /** + * Set a custom list of {@link RoutingHandlerFactory} names. + * + * @param handlerFactoriesClassNames the list of {@link RoutingHandlerFactory} names. + * @return the {@link ServerConfig} for chaining + */ + @Fluent + public ServerConfig setHandlerFactoriesClassNames(List handlerFactoriesClassNames) { + this.handlerFactoriesClassNames = handlerFactoriesClassNames; + return this; + } + /** * Returns a custom error handler class name, which is instantiated as failure handler of the * {@link ServerVerticle}. The {@link DefaultErrorHandler} is used in case no value is supplied. The class must diff --git a/src/main/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactory.java new file mode 100644 index 00000000..d97d895e --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactory.java @@ -0,0 +1,19 @@ +package io.neonbee.internal.handler.factories; + +import static io.vertx.core.Future.succeededFuture; + +import io.neonbee.internal.handler.CacheControlHandler; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Create the {@link CacheControlHandler}. + */ +public class CacheControlHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + return succeededFuture(new CacheControlHandler()); + } +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactory.java new file mode 100644 index 00000000..0c83f865 --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactory.java @@ -0,0 +1,22 @@ +package io.neonbee.internal.handler.factories; + +import static io.vertx.core.Future.succeededFuture; + +import io.neonbee.NeonBee; +import io.neonbee.config.ServerConfig; +import io.neonbee.internal.handler.CorrelationIdHandler; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Create the {@link CorrelationIdHandler}. + */ +public class CorrelationIdHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + ServerConfig serverConfig = NeonBee.get().getServerConfig(); + return succeededFuture(new CorrelationIdHandler(serverConfig.getCorrelationStrategy())); + } +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactory.java new file mode 100644 index 00000000..6420edae --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactory.java @@ -0,0 +1,19 @@ +package io.neonbee.internal.handler.factories; + +import static io.vertx.core.Future.succeededFuture; + +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; + +/** + * Create A {@link BodyHandler} that disallows file uploads. + */ +public class DisallowingFileUploadBodyHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + return succeededFuture(BodyHandler.create(false /* do not handle file uploads */)); + } +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactory.java new file mode 100644 index 00000000..098f1496 --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactory.java @@ -0,0 +1,19 @@ +package io.neonbee.internal.handler.factories; + +import static io.vertx.core.Future.succeededFuture; + +import io.neonbee.internal.handler.InstanceInfoHandler; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Create the {@link InstanceInfoHandler}. + */ +public class InstanceInfoHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + return succeededFuture(new InstanceInfoHandler()); + } +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/LoggerHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/LoggerHandlerFactory.java new file mode 100644 index 00000000..f93919cd --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/LoggerHandlerFactory.java @@ -0,0 +1,19 @@ +package io.neonbee.internal.handler.factories; + +import static io.vertx.core.Future.succeededFuture; + +import io.neonbee.internal.handler.LoggerHandler; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Create the {@link LoggerHandler}. + */ +public class LoggerHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + return succeededFuture(new LoggerHandler()); + } +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/RoutingHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/RoutingHandlerFactory.java new file mode 100644 index 00000000..f4e12da7 --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/RoutingHandlerFactory.java @@ -0,0 +1,22 @@ +package io.neonbee.internal.handler.factories; + +import io.neonbee.internal.verticle.ServerVerticle; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Factory interface for creating routing handlers. + * + * This interface is evaluated when {@link ServerVerticle} is started. The returned handlers will be added to the root + * route. + */ +public interface RoutingHandlerFactory { + + /** + * Returns an instance of the {@link Handler} object to add to the main route. + * + * @return the {@link Handler} object + */ + Future> createHandler(); +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java new file mode 100644 index 00000000..0133667a --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/SessionHandlerFactory.java @@ -0,0 +1,69 @@ +package io.neonbee.internal.handler.factories; + +import java.util.Optional; + +import com.google.common.annotations.VisibleForTesting; + +import io.neonbee.NeonBee; +import io.neonbee.config.ServerConfig; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.PlatformHandler; +import io.vertx.ext.web.handler.SessionHandler; +import io.vertx.ext.web.sstore.ClusteredSessionStore; +import io.vertx.ext.web.sstore.LocalSessionStore; +import io.vertx.ext.web.sstore.SessionStore; + +/** + * Creates a SessionHandler. + * + * If ServerConfig.SessionHandling is set to NONE a no-operation handler is returned, which does not perform any session + * handling. + */ +public class SessionHandlerFactory implements RoutingHandlerFactory { + + @VisibleForTesting + static final class NoOpHandler implements PlatformHandler { + @Override + public void handle(RoutingContext routingContext) { + routingContext.next(); + } + } + + @Override + public Future> createHandler() { + ServerConfig config = NeonBee.get().getServerConfig(); + Handler sh = + createSessionStore(NeonBee.get().getVertx(), config.getSessionHandling()).map(SessionHandler::create) + .map(sessionHandler -> sessionHandler.setSessionCookieName(config.getSessionCookieName())) + .map(sessionHandler -> (Handler) sessionHandler).orElseGet(NoOpHandler::new); + return Future.succeededFuture(sh); + } + + /** + * Creates a {@linkplain SessionStore} based on the given {@linkplain ServerConfig} to use either local or clustered + * session handling. If no session handling should be used, an empty optional is returned. + * + * @param vertx the Vert.x instance to create the {@linkplain SessionStore} for + * @param sessionHandling the session handling type + * @return an optional session store, suitable for the given Vert.x instance and based on the provided config value + * (none/local/clustered). In case the session handling is set to clustered, but Vert.x does not run in + * clustered mode, fallback to the local session handling. + */ + @VisibleForTesting + static Optional createSessionStore(Vertx vertx, ServerConfig.SessionHandling sessionHandling) { + switch (sessionHandling) { + case LOCAL: + return Optional.of(LocalSessionStore.create(vertx)); + case CLUSTERED: + if (!vertx.isClustered()) { // Behaves like clustered in case that instance isn't clustered + return Optional.of(LocalSessionStore.create(vertx)); + } + return Optional.of(ClusteredSessionStore.create(vertx)); + default: /* nothing to do here, no session handling, so neither add a cookie, nor a session handler */ + return Optional.empty(); + } + } +} diff --git a/src/main/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactory.java b/src/main/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactory.java new file mode 100644 index 00000000..20ec3b54 --- /dev/null +++ b/src/main/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactory.java @@ -0,0 +1,25 @@ +package io.neonbee.internal.handler.factories; + +import static io.vertx.core.Future.succeededFuture; + +import java.util.concurrent.TimeUnit; + +import io.neonbee.NeonBee; +import io.neonbee.config.ServerConfig; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.TimeoutHandler; + +/** + * Create the {@link TimeoutHandler}. + */ +public class TimeoutHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + ServerConfig serverConfig = NeonBee.get().getServerConfig(); + return succeededFuture(TimeoutHandler.create(TimeUnit.SECONDS.toMillis(serverConfig.getTimeout()), + serverConfig.getTimeoutStatusCode())); + } +} diff --git a/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java b/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java index b5ce6647..64974dad 100644 --- a/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java +++ b/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java @@ -2,7 +2,6 @@ import static io.vertx.core.Future.failedFuture; import static io.vertx.core.Future.succeededFuture; -import static java.util.concurrent.TimeUnit.SECONDS; import java.lang.invoke.MethodHandles; import java.util.List; @@ -18,31 +17,24 @@ import io.neonbee.NeonBee; import io.neonbee.config.EndpointConfig; import io.neonbee.config.ServerConfig; -import io.neonbee.config.ServerConfig.SessionHandling; import io.neonbee.endpoint.Endpoint; import io.neonbee.endpoint.MountableEndpoint; import io.neonbee.handler.ErrorHandler; -import io.neonbee.internal.handler.CacheControlHandler; import io.neonbee.internal.handler.ChainAuthHandler; -import io.neonbee.internal.handler.CorrelationIdHandler; import io.neonbee.internal.handler.DefaultErrorHandler; -import io.neonbee.internal.handler.InstanceInfoHandler; -import io.neonbee.internal.handler.LoggerHandler; import io.neonbee.internal.handler.NotFoundHandler; +import io.neonbee.internal.handler.factories.RoutingHandlerFactory; import io.neonbee.internal.helper.AsyncHelper; import io.vertx.core.AbstractVerticle; +import io.vertx.core.CompositeFuture; import io.vertx.core.Future; +import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; -import io.vertx.ext.web.handler.BodyHandler; -import io.vertx.ext.web.handler.SessionHandler; -import io.vertx.ext.web.handler.TimeoutHandler; -import io.vertx.ext.web.sstore.ClusteredSessionStore; -import io.vertx.ext.web.sstore.LocalSessionStore; -import io.vertx.ext.web.sstore.SessionStore; +import io.vertx.ext.web.RoutingContext; /** * The {@link ServerVerticle} handles exposing all {@linkplain Endpoint endpoints} currently using the HTTP(S) protocol. @@ -95,21 +87,22 @@ private Future createRouter(ServerConfig config) { // sequence issues, block scope the variable to prevent using it after the endpoints have been mounted Route rootRoute = router.route(); - return createErrorHandler(config.getErrorHandlerClassName(), vertx).compose(errorHandler -> { - rootRoute.failureHandler(errorHandler); - rootRoute.handler(new LoggerHandler()); - rootRoute.handler(new InstanceInfoHandler()); - rootRoute.handler(new CorrelationIdHandler(config.getCorrelationStrategy())); - rootRoute.handler( - TimeoutHandler.create(SECONDS.toMillis(config.getTimeout()), config.getTimeoutStatusCode())); - createSessionStore(vertx, config.getSessionHandling()).map(SessionHandler::create) - .ifPresent(sessionHandler -> rootRoute - .handler(sessionHandler.setSessionCookieName(config.getSessionCookieName()))); - rootRoute.handler(new CacheControlHandler()); - rootRoute.handler(BodyHandler.create(false /* do not handle file uploads */)); - - return succeededFuture(router); - }).onFailure(e -> LOGGER.error("Router could not be created", e)); + return createErrorHandler(config.getErrorHandlerClassName(), vertx).onSuccess(rootRoute::failureHandler) + .compose(unused -> { + List handlerFutures = config.getHandlerFactoriesClassNames().stream() + .map(ServerVerticle::instantiateHandler).collect(Collectors.toList()); + return CompositeFuture.all(handlerFutures); + }).compose(compositeFuture -> { + List> handlers = compositeFuture.list(); + handlers.forEach(routingContextHandler -> { + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Appending \"{}\" request handler to root router.", + routingContextHandler.getClass().getName()); + } + rootRoute.handler(routingContextHandler); + }); + return succeededFuture(router); + }).onFailure(e -> LOGGER.error("Router could not be created", e)); } @VisibleForTesting @@ -134,6 +127,29 @@ static Future createErrorHandler(String className, Vertx vertx) { } } + /** + * Instantiate the {@link Handler} by executing the {@link RoutingHandlerFactory}. + * + * @param handlerFactoryName {@link RoutingHandlerFactory} name + * @return Future with the Handler instance + */ + @VisibleForTesting + static Future> instantiateHandler(String handlerFactoryName) { + try { + Class factoryClass = Class.forName(handlerFactoryName); + if (!RoutingHandlerFactory.class.isAssignableFrom(factoryClass)) { + return failedFuture("Class \"" + handlerFactoryName + "\" is not an instance of " + + RoutingHandlerFactory.class.getName()); + } + + RoutingHandlerFactory factoryInstance = (RoutingHandlerFactory) factoryClass.getConstructor().newInstance(); + return factoryInstance.createHandler(); + } catch (Exception e) { + LOGGER.error("Failed to instantiate Handler: {}", handlerFactoryName, e); + return failedFuture(e); + } + } + private Future createHttpServer(Router router, ServerConfig config) { // Use the port passed via command line options, instead the configured one. Optional.ofNullable(NeonBee.get(vertx).getOptions().getServerPort()).ifPresent(config::setPort); @@ -180,29 +196,4 @@ protected Future mountEndpoints(Router router, List endpoi // all endpoints have been mounted in correct order successfully }).mapEmpty(); } - - /** - * Creates a {@linkplain SessionStore} based on the given {@linkplain ServerConfig} to use either local or clustered - * session handling. If no session handling should be used, an empty optional is returned. - * - * @param vertx the Vert.x instance to create the {@linkplain SessionStore} for - * @param sessionHandling the session handling type - * @return a optional session store, suitable for the given Vert.x instance and based on the provided config value - * (none/local/clustered). In case the session handling is set to clustered, but Vert.x does not run in - * clustered mode, fallback to the local session handling. - */ - @VisibleForTesting - static Optional createSessionStore(Vertx vertx, SessionHandling sessionHandling) { - switch (sessionHandling) { - case LOCAL: // sessions are stored locally in a shared local map and only available on this instance - return Optional.of(LocalSessionStore.create(vertx)); - case CLUSTERED: // sessions are stored in a distributed map which is accessible across the Vert.x cluster - if (!vertx.isClustered()) { // Behaves like clustered in case that instance isn't clustered - return Optional.of(LocalSessionStore.create(vertx)); - } - return Optional.of(ClusteredSessionStore.create(vertx)); - default: /* nothing to do here, no session handling, so neither add a cookie, nor a session handler */ - return Optional.empty(); - } - } } diff --git a/src/test/java/io/neonbee/config/ServerConfigTest.java b/src/test/java/io/neonbee/config/ServerConfigTest.java index edbd887c..060bb5ce 100644 --- a/src/test/java/io/neonbee/config/ServerConfigTest.java +++ b/src/test/java/io/neonbee/config/ServerConfigTest.java @@ -4,6 +4,7 @@ import static io.neonbee.config.ServerConfig.DEFAULT_COMPRESSION_LEVEL; import static io.neonbee.config.ServerConfig.DEFAULT_COMPRESSION_SUPPORTED; import static io.neonbee.config.ServerConfig.DEFAULT_DECOMPRESSION_SUPPORTED; +import static io.neonbee.config.ServerConfig.DEFAULT_HANDLER_FACTORIES_CLASS_NAMES; import static io.neonbee.config.ServerConfig.DEFAULT_PORT; import static io.neonbee.config.ServerConfig.DEFAULT_USE_ALPN; @@ -19,6 +20,8 @@ import io.neonbee.config.ServerConfig.CorrelationStrategy; import io.neonbee.config.ServerConfig.SessionHandling; import io.neonbee.endpoint.raw.RawEndpoint; +import io.neonbee.internal.handler.factories.CacheControlHandlerFactory; +import io.neonbee.internal.handler.factories.CorrelationIdHandlerFactory; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.ClientAuth; import io.vertx.core.http.Http2Settings; @@ -41,6 +44,9 @@ class ServerConfigTest { private static final String ERROR_TEMPLATE = "custom-template.html"; + private static final List FACTORY_CLASS_NAME_LIST = + List.of(CacheControlHandlerFactory.class.getName(), CorrelationIdHandlerFactory.class.getName()); + @Test @DisplayName("test toJson") void testToJson() { @@ -59,6 +65,7 @@ void testToJson() { sc.setCorrelationStrategy(correlationStrategy).setTimeoutStatusCode(timeoutStatusCode); sc.setEndpointConfigs(endpointConfigs).setAuthChainConfig(authHandlerConfig); sc.setErrorHandlerClassName(ERROR_HANDLER).setErrorHandlerTemplate(ERROR_TEMPLATE); + sc.setHandlerFactoriesClassNames(FACTORY_CLASS_NAME_LIST); JsonObject expected = new JsonObject().put("timeout", timeout).put("sessionHandling", sessionHandling.name()); expected.put("sessionCookieName", sessionCookieName).put("correlationStrategy", correlationStrategy.name()); @@ -66,6 +73,10 @@ void testToJson() { expected.put("authenticationChain", new JsonArray().add(ahc.toJson())); expected.put("errorHandler", ERROR_HANDLER).put("errorTemplate", ERROR_TEMPLATE); + JsonArray expectedHandlerFactories = new JsonArray(); + FACTORY_CLASS_NAME_LIST.forEach(expectedHandlerFactories::add); + expected.put("handlerFactories", expectedHandlerFactories); + assertThat(sc.toJson()).containsAtLeastElementsIn(expected); } @@ -87,6 +98,10 @@ void testJsonConstructor() { json.put("errorHandler", ERROR_HANDLER); json.put("errorTemplate", ERROR_TEMPLATE); + JsonArray handlerFactories = new JsonArray(); + FACTORY_CLASS_NAME_LIST.forEach(handlerFactories::add); + json.put("handlerFactories", handlerFactories); + ServerConfig sc = new ServerConfig(json); assertThat(sc.getTimeout()).isEqualTo(timeout); assertThat(sc.getSessionHandling()).isEqualTo(sessionHandling); @@ -109,6 +124,7 @@ void testDefaultConstructor() { assertThat(sc.isCompressionSupported()).isEqualTo(DEFAULT_COMPRESSION_SUPPORTED); assertThat(sc.getCompressionLevel()).isEqualTo(DEFAULT_COMPRESSION_LEVEL); assertThat(sc.isDecompressionSupported()).isEqualTo(DEFAULT_DECOMPRESSION_SUPPORTED); + assertThat(sc.getHandlerFactoriesClassNames()).isEqualTo(DEFAULT_HANDLER_FACTORIES_CLASS_NAMES); } @Test @@ -146,6 +162,9 @@ void testGettersAndSetters() { assertThat(sc.setErrorHandlerTemplate(ERROR_TEMPLATE)).isSameInstanceAs(sc); assertThat(sc.getErrorHandlerTemplate()).isEqualTo(ERROR_TEMPLATE); + + assertThat(sc.setHandlerFactoriesClassNames(FACTORY_CLASS_NAME_LIST)).isSameInstanceAs(sc); + assertThat(sc.getHandlerFactoriesClassNames()).isEqualTo(FACTORY_CLASS_NAME_LIST); } @Test @@ -366,5 +385,8 @@ void testOverriddenSetters() { List subProtocols = List.of(""); assertThat(sc.setWebSocketSubProtocols(subProtocols)).isSameInstanceAs(sc); assertThat(sc.getWebSocketSubProtocols()).containsExactlyElementsIn(subProtocols); + + assertThat(sc.setHandlerFactoriesClassNames(FACTORY_CLASS_NAME_LIST)).isSameInstanceAs(sc); + assertThat(sc.getHandlerFactoriesClassNames()).containsExactlyElementsIn(FACTORY_CLASS_NAME_LIST); } } diff --git a/src/test/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactoryTest.java new file mode 100644 index 00000000..ac3d00f4 --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/CacheControlHandlerFactoryTest.java @@ -0,0 +1,23 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.neonbee.internal.handler.CacheControlHandler; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class CacheControlHandlerFactoryTest { + + @Test + void testCreateHandler(VertxTestContext testContext) { + new CacheControlHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(CacheControlHandler.class); + testContext.completeNow(); + }))); + } +} diff --git a/src/test/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactoryTest.java new file mode 100644 index 00000000..f8452861 --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/CorrelationIdHandlerFactoryTest.java @@ -0,0 +1,37 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; + +import io.neonbee.NeonBee; +import io.neonbee.config.ServerConfig; +import io.neonbee.internal.handler.CorrelationIdHandler; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class CorrelationIdHandlerFactoryTest { + + @Test + void testCreateHandler(VertxTestContext testContext) { + + try (MockedStatic staticNeonBeeMock = mockStatic(NeonBee.class)) { + NeonBee neonBeeMock = mock(NeonBee.class); + when(neonBeeMock.getServerConfig()).thenReturn(new ServerConfig()); + staticNeonBeeMock.when(() -> NeonBee.get()).thenReturn(neonBeeMock); + + new CorrelationIdHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(CorrelationIdHandler.class); + testContext.completeNow(); + }))); + } + + } +} diff --git a/src/test/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactoryTest.java new file mode 100644 index 00000000..cbd013dd --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/DisallowingFileUploadBodyHandlerFactoryTest.java @@ -0,0 +1,22 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class DisallowingFileUploadBodyHandlerFactoryTest { + @Test + void testCreateHandler(VertxTestContext testContext) { + new DisallowingFileUploadBodyHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(BodyHandler.class); + testContext.completeNow(); + }))); + } +} diff --git a/src/test/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactoryTest.java new file mode 100644 index 00000000..4c53e742 --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/InstanceInfoHandlerFactoryTest.java @@ -0,0 +1,22 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.neonbee.internal.handler.InstanceInfoHandler; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class InstanceInfoHandlerFactoryTest { + @Test + void testCreateHandler(VertxTestContext testContext) { + new InstanceInfoHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(InstanceInfoHandler.class); + testContext.completeNow(); + }))); + } +} diff --git a/src/test/java/io/neonbee/internal/handler/factories/LoggerHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/LoggerHandlerFactoryTest.java new file mode 100644 index 00000000..cbb30fa5 --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/LoggerHandlerFactoryTest.java @@ -0,0 +1,22 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.neonbee.internal.handler.LoggerHandler; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class LoggerHandlerFactoryTest { + @Test + void testCreateHandler(VertxTestContext testContext) { + new LoggerHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(LoggerHandler.class); + testContext.completeNow(); + }))); + } +} diff --git a/src/test/java/io/neonbee/internal/handler/factories/SessionHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/SessionHandlerFactoryTest.java new file mode 100644 index 00000000..cde78952 --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/SessionHandlerFactoryTest.java @@ -0,0 +1,111 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; +import static io.neonbee.internal.handler.factories.SessionHandlerFactory.createSessionStore; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; + +import io.neonbee.NeonBee; +import io.neonbee.config.ServerConfig; +import io.neonbee.config.ServerConfig.SessionHandling; +import io.vertx.core.Vertx; +import io.vertx.core.impl.VertxInternal; +import io.vertx.core.shareddata.SharedData; +import io.vertx.ext.web.handler.SessionHandler; +import io.vertx.ext.web.sstore.ClusteredSessionStore; +import io.vertx.ext.web.sstore.LocalSessionStore; +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class SessionHandlerFactoryTest { + + @Test + void testCreateHandlerNone(Vertx vertx, VertxTestContext testContext) { + try (MockedStatic staticNeonBeeMock = mockStatic(NeonBee.class)) { + NeonBee neonBeeMock = mock(NeonBee.class); + ServerConfig serverConfig = new ServerConfig(); + serverConfig.setSessionHandling(SessionHandling.NONE); + when(neonBeeMock.getServerConfig()).thenReturn(serverConfig); + when(neonBeeMock.getVertx()).thenReturn(vertx); + staticNeonBeeMock.when(() -> NeonBee.get()).thenReturn(neonBeeMock); + + new SessionHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(SessionHandlerFactory.NoOpHandler.class); + testContext.completeNow(); + }))); + } + } + + @Test + void testCreateHandlerLocal(Vertx vertx, VertxTestContext testContext) { + try (MockedStatic staticNeonBeeMock = mockStatic(NeonBee.class)) { + NeonBee neonBeeMock = mock(NeonBee.class); + ServerConfig serverConfig = new ServerConfig(); + serverConfig.setSessionHandling(ServerConfig.SessionHandling.LOCAL); + when(neonBeeMock.getServerConfig()).thenReturn(serverConfig); + when(neonBeeMock.getVertx()).thenReturn(vertx); + staticNeonBeeMock.when(() -> NeonBee.get()).thenReturn(neonBeeMock); + + new SessionHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(SessionHandler.class); + testContext.completeNow(); + }))); + } + } + + @Test + void testCreateHandlerClustered(Vertx vertx, VertxTestContext testContext) { + try (MockedStatic staticNeonBeeMock = mockStatic(NeonBee.class)) { + NeonBee neonBeeMock = mock(NeonBee.class); + ServerConfig serverConfig = new ServerConfig(); + serverConfig.setSessionHandling(ServerConfig.SessionHandling.CLUSTERED); + when(neonBeeMock.getServerConfig()).thenReturn(serverConfig); + when(neonBeeMock.getVertx()).thenReturn(vertx); + staticNeonBeeMock.when(() -> NeonBee.get()).thenReturn(neonBeeMock); + + new SessionHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(SessionHandler.class); + testContext.completeNow(); + }))); + } + } + + @Test + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + void testCreateSessionStore() { + Vertx mockedVertx = mock(VertxInternal.class); + when(mockedVertx.isClustered()).thenReturn(false); + when(mockedVertx.sharedData()).thenReturn(mock(SharedData.class)); + + assertThat(createSessionStore(mockedVertx, SessionHandling.NONE).isEmpty()).isTrue(); + assertThat(createSessionStore(mockedVertx, SessionHandling.LOCAL).get()).isInstanceOf(LocalSessionStore.class); + assertThat(createSessionStore(mockedVertx, SessionHandling.CLUSTERED).get()) + .isInstanceOf(LocalSessionStore.class); + } + + @Test + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + void testCreateSessionStoreClustered() { + Vertx mockedVertx = mock(VertxInternal.class); + when(mockedVertx.isClustered()).thenReturn(true); + when(mockedVertx.sharedData()).thenReturn(mock(SharedData.class)); + + assertThat(createSessionStore(mockedVertx, SessionHandling.NONE).isEmpty()).isTrue(); + assertThat(createSessionStore(mockedVertx, SessionHandling.LOCAL).get()).isInstanceOf(LocalSessionStore.class); + assertThat(createSessionStore(mockedVertx, SessionHandling.CLUSTERED).get()) + .isInstanceOf(ClusteredSessionStore.class); + } + +} diff --git a/src/test/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactoryTest.java b/src/test/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactoryTest.java new file mode 100644 index 00000000..0ec69d7b --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/factories/TimeoutHandlerFactoryTest.java @@ -0,0 +1,36 @@ +package io.neonbee.internal.handler.factories; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; + +import io.neonbee.NeonBee; +import io.neonbee.config.ServerConfig; +import io.vertx.ext.web.handler.TimeoutHandler; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; + +@ExtendWith(VertxExtension.class) +class TimeoutHandlerFactoryTest { + @Test + void testCreateHandler(VertxTestContext testContext) { + + try (MockedStatic staticNeonBeeMock = mockStatic(NeonBee.class)) { + NeonBee neonBeeMock = mock(NeonBee.class); + when(neonBeeMock.getServerConfig()).thenReturn(new ServerConfig()); + staticNeonBeeMock.when(() -> NeonBee.get()).thenReturn(neonBeeMock); + + new TimeoutHandlerFactory().createHandler() + .onComplete(testContext.succeeding(instance -> testContext.verify(() -> { + assertThat(instance).isInstanceOf(TimeoutHandler.class); + testContext.completeNow(); + }))); + } + + } +} diff --git a/src/test/java/io/neonbee/internal/verticle/ServerVerticleTest.java b/src/test/java/io/neonbee/internal/verticle/ServerVerticleTest.java index 1a7b0241..42b26f55 100644 --- a/src/test/java/io/neonbee/internal/verticle/ServerVerticleTest.java +++ b/src/test/java/io/neonbee/internal/verticle/ServerVerticleTest.java @@ -1,9 +1,6 @@ package io.neonbee.internal.verticle; import static com.google.common.truth.Truth.assertThat; -import static io.neonbee.internal.verticle.ServerVerticle.createSessionStore; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.nio.file.Files; @@ -13,48 +10,22 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -import io.neonbee.config.ServerConfig.SessionHandling; import io.neonbee.internal.handler.DefaultErrorHandler; +import io.neonbee.internal.handler.factories.RoutingHandlerFactory; import io.neonbee.test.base.NeonBeeTestBase; import io.neonbee.test.helper.WorkingDirectoryBuilder; +import io.vertx.core.Future; +import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpMethod; -import io.vertx.core.impl.VertxInternal; import io.vertx.core.json.JsonObject; -import io.vertx.core.shareddata.SharedData; -import io.vertx.ext.web.sstore.ClusteredSessionStore; -import io.vertx.ext.web.sstore.LocalSessionStore; +import io.vertx.ext.web.RoutingContext; import io.vertx.junit5.Checkpoint; import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; class ServerVerticleTest extends NeonBeeTestBase { - @Test - @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) - void testCreateSessionStore() { - Vertx mockedVertx = mock(VertxInternal.class); - when(mockedVertx.isClustered()).thenReturn(false); - when(mockedVertx.sharedData()).thenReturn(mock(SharedData.class)); - - assertThat(createSessionStore(mockedVertx, SessionHandling.NONE).isEmpty()).isTrue(); - assertThat(createSessionStore(mockedVertx, SessionHandling.LOCAL).get()).isInstanceOf(LocalSessionStore.class); - assertThat(createSessionStore(mockedVertx, SessionHandling.CLUSTERED).get()) - .isInstanceOf(LocalSessionStore.class); - } - - @Test - @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) - void testCreateSessionStoreClustered() { - Vertx mockedVertx = mock(VertxInternal.class); - when(mockedVertx.isClustered()).thenReturn(true); - when(mockedVertx.sharedData()).thenReturn(mock(SharedData.class)); - - assertThat(createSessionStore(mockedVertx, SessionHandling.NONE).isEmpty()).isTrue(); - assertThat(createSessionStore(mockedVertx, SessionHandling.LOCAL).get()).isInstanceOf(LocalSessionStore.class); - assertThat(createSessionStore(mockedVertx, SessionHandling.CLUSTERED).get()) - .isInstanceOf(ClusteredSessionStore.class); - } @Test @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) @@ -147,4 +118,66 @@ protected WorkingDirectoryBuilder provideWorkingDirectoryBuilder(TestInfo testIn return super.provideWorkingDirectoryBuilder(testInfo, testContext); } } + + @Test + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + void testInstantiateHandler(VertxTestContext testContext) { + ServerVerticle.instantiateHandler(TestRoutingHandlerFactory.class.getName()) + .onComplete(testContext.succeeding(clazz -> testContext.verify(() -> { + assertThat(clazz).isInstanceOf(Handler.class); + testContext.completeNow(); + }))); + } + + @Test + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + void testFailingInstantiateHandler(VertxTestContext testContext) { + ServerVerticle.instantiateHandler(TestFailingRoutingHandlerFactory.class.getName()) + .onComplete(testContext.failing(throwable -> testContext.verify(() -> { + assertThat(throwable).hasMessageThat() + .isEqualTo(TestFailingRoutingHandlerFactory.EXCEPTION_MESSAGE); + testContext.completeNow(); + }))); + } + + @Test + @Timeout(value = 2, timeUnit = TimeUnit.SECONDS) + void testExptionInstantiateHandler(VertxTestContext testContext) { + ServerVerticle.instantiateHandler(TestThrowingRoutingHandlerFactory.class.getName()) + .onComplete(testContext.failing(throwable -> testContext.verify(() -> { + assertThat(throwable).isInstanceOf(IllegalStateException.class); + assertThat(throwable).hasMessageThat() + .isEqualTo(TestThrowingRoutingHandlerFactory.EXCEPTION_MESSAGE); + testContext.completeNow(); + }))); + } + + public static class TestRoutingHandlerFactory implements RoutingHandlerFactory { + + @Override + public Future> createHandler() { + return Future.succeededFuture(event -> {}); + } + } + + public static class TestFailingRoutingHandlerFactory implements RoutingHandlerFactory { + + public static final String EXCEPTION_MESSAGE = "test failing createHandler."; + + @Override + public Future> createHandler() { + return Future.failedFuture(EXCEPTION_MESSAGE); + } + } + + public static class TestThrowingRoutingHandlerFactory implements RoutingHandlerFactory { + + public static final String EXCEPTION_MESSAGE = "Test exception thrown in createHandler."; + + @Override + public Future> createHandler() { + throw new IllegalStateException(EXCEPTION_MESSAGE); + } + } + }