diff --git a/.vscode/settings.json b/.vscode/settings.json index d38c3bd0..5682f9dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "java.configuration.updateBuildConfiguration": "automatic", "java.dependency.packagePresentation": "hierarchical", - "java.format.settings.url": "file:./gradle/spotless/eclipse-formatter.xml", + "java.format.settings.url": "./gradle/spotless/eclipse-formatter.xml", "java.saveActions.organizeImports": true, "editor.formatOnSave": true, "html.format.enable": false, diff --git a/src/main/java/io/neonbee/endpoint/MountableEndpoint.java b/src/main/java/io/neonbee/endpoint/MountableEndpoint.java index 785ce300..1450a27a 100644 --- a/src/main/java/io/neonbee/endpoint/MountableEndpoint.java +++ b/src/main/java/io/neonbee/endpoint/MountableEndpoint.java @@ -11,7 +11,7 @@ import io.neonbee.config.AuthHandlerConfig; import io.neonbee.config.EndpointConfig; import io.neonbee.config.ServerConfig; -import io.neonbee.internal.handler.AuthChainHandler; +import io.neonbee.internal.handler.ChainAuthHandler; import io.neonbee.internal.handler.HooksHandler; import io.neonbee.internal.helper.AsyncHelper; import io.neonbee.logging.LoggingFacade; @@ -88,7 +88,7 @@ private MountableEndpoint(EndpointConfig endpointConfig, Endpoint endpoint, Rout * @param rootRouter the router on which the endpoint will be mounted * @param defaultAuthHandler the default auth handler if there is no endpoint-specific one */ - public void mount(Vertx vertx, Router rootRouter, Optional defaultAuthHandler) { + public void mount(Vertx vertx, Router rootRouter, Optional defaultAuthHandler) { String endpointBasePath = getEndpointBasePath(endpointConfig, endpoint); Route endpointRoute = rootRouter.route(endpointBasePath + "*"); endpointRoute.handler(new HooksHandler()); @@ -96,7 +96,7 @@ public void mount(Vertx vertx, Router rootRouter, Optional def Optional> effectiveAuthChainConfig = Optional.ofNullable(endpointConfig.getAuthChainConfig()) .or(() -> Optional.ofNullable(endpoint.getDefaultConfig().getAuthChainConfig())); - effectiveAuthChainConfig.map(authChainConfig -> AuthChainHandler.create(vertx, authChainConfig)) + effectiveAuthChainConfig.map(authChainConfig -> ChainAuthHandler.create(vertx, authChainConfig)) .or(() -> defaultAuthHandler).ifPresent(endpointRoute::handler); if (LOGGER.isInfoEnabled()) { diff --git a/src/main/java/io/neonbee/internal/handler/AuthChainHandler.java b/src/main/java/io/neonbee/internal/handler/ChainAuthHandler.java similarity index 63% rename from src/main/java/io/neonbee/internal/handler/AuthChainHandler.java rename to src/main/java/io/neonbee/internal/handler/ChainAuthHandler.java index 31c3b734..9f617d36 100644 --- a/src/main/java/io/neonbee/internal/handler/AuthChainHandler.java +++ b/src/main/java/io/neonbee/internal/handler/ChainAuthHandler.java @@ -3,17 +3,19 @@ import java.util.List; import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; + import io.neonbee.config.AuthHandlerConfig; import io.vertx.core.Vertx; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.AuthenticationHandler; -import io.vertx.ext.web.handler.ChainAuthHandler; -public interface AuthChainHandler extends AuthenticationHandler { +public interface ChainAuthHandler extends AuthenticationHandler { /** * A no operation authentication handler. */ - AuthChainHandler NOOP_AUTHENTICATION_HANDLER = RoutingContext::next; + @VisibleForTesting + ChainAuthHandler NOOP_AUTHENTICATION_HANDLER = RoutingContext::next; /** * Creates an AuthChainHandler instance. @@ -23,16 +25,21 @@ public interface AuthChainHandler extends AuthenticationHandler { * @return an AuthChainHandler based on the passed configuration. If the passed authChainConfig is null or * empty a {@link #NOOP_AUTHENTICATION_HANDLER} will be returned. */ - static AuthChainHandler create(Vertx vertx, List authChainConfig) { + static ChainAuthHandler create(Vertx vertx, List authChainConfig) { if (authChainConfig == null || authChainConfig.isEmpty()) { return NOOP_AUTHENTICATION_HANDLER; } - ChainAuthHandler authChainHandler = ChainAuthHandler.any(); + io.vertx.ext.web.handler.ChainAuthHandler chainAuthHandler = io.vertx.ext.web.handler.ChainAuthHandler.any(); List authHandlers = authChainConfig.stream().map(config -> config.createAuthHandler(vertx)).collect(Collectors.toList()); - authHandlers.forEach(authChainHandler::add); + authHandlers.forEach(chainAuthHandler::add); - return (AuthChainHandler) authChainHandler; + return new ChainAuthHandler() { + @Override + public void handle(RoutingContext event) { + chainAuthHandler.handle(event); + } + }; } } diff --git a/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java b/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java index 9c059b14..9a93b8ee 100644 --- a/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java +++ b/src/main/java/io/neonbee/internal/verticle/ServerVerticle.java @@ -22,8 +22,8 @@ import io.neonbee.endpoint.Endpoint; import io.neonbee.endpoint.MountableEndpoint; import io.neonbee.handler.ErrorHandler; -import io.neonbee.internal.handler.AuthChainHandler; 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; @@ -69,8 +69,8 @@ public void start(Promise startPromise) { ServerConfig config = new ServerConfig(config()); createRouter(config).compose(router -> { - Optional defaultAuthHandler = - Optional.ofNullable(config.getAuthChainConfig()).map(c -> AuthChainHandler.create(vertx, c)); + Optional defaultAuthHandler = + Optional.ofNullable(config.getAuthChainConfig()).map(c -> ChainAuthHandler.create(vertx, c)); return mountEndpoints(router, config.getEndpointConfigs(), defaultAuthHandler).onSuccess(v -> { // the NotFoundHandler fails the routing context finally. // To ensure that no handler will be added after it, it is added here. @@ -165,7 +165,7 @@ private Future createHttpServer(Router router, ServerConfig config) */ @VisibleForTesting protected Future mountEndpoints(Router router, List endpointConfigs, - Optional defaultAuthHandler) { + Optional defaultAuthHandler) { if (endpointConfigs.isEmpty()) { LOGGER.warn("No endpoints configured"); return succeededFuture(); diff --git a/src/test/java/io/neonbee/internal/handler/ChainAuthHandlerTest.java b/src/test/java/io/neonbee/internal/handler/ChainAuthHandlerTest.java new file mode 100644 index 00000000..c421f33f --- /dev/null +++ b/src/test/java/io/neonbee/internal/handler/ChainAuthHandlerTest.java @@ -0,0 +1,82 @@ +package io.neonbee.internal.handler; + +import static com.google.common.truth.Truth.assertThat; +import static io.neonbee.internal.handler.ChainAuthHandler.NOOP_AUTHENTICATION_HANDLER; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.neonbee.config.AuthHandlerConfig; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import io.vertx.ext.web.handler.impl.AuthenticationHandlerInternal; +import io.vertx.junit5.VertxExtension; + +@ExtendWith(VertxExtension.class) +class ChainAuthHandlerTest { + @Test + @DisplayName("create empty ChainAuthHandler") + void emptyChainAuthHandlerTest() { + assertThat(ChainAuthHandler.create(null, null)).isSameInstanceAs(NOOP_AUTHENTICATION_HANDLER); + assertThat(ChainAuthHandler.create(null, List.of())).isSameInstanceAs(NOOP_AUTHENTICATION_HANDLER); + } + + @Test + @DisplayName("create a non-empty ChainAuthHandler") + void createChainAuthHandlerTest(Vertx vertx) throws IOException { + AuthHandlerConfig config1 = mock(AuthHandlerConfig.class); + when(config1.createAuthHandler(any())).thenReturn(mock(AuthenticationHandlerInternal.class)); + + AuthHandlerConfig config2 = mock(AuthHandlerConfig.class); + when(config2.createAuthHandler(any())).thenReturn(mock(AuthenticationHandlerInternal.class)); + + ChainAuthHandler handler = ChainAuthHandler.create(vertx, List.of(config1, config2)); + assertThat(handler).isNotSameInstanceAs(NOOP_AUTHENTICATION_HANDLER); + verify(config1).createAuthHandler(vertx); + verify(config2).createAuthHandler(vertx); + } + + @Test + @DisplayName("test ChainAuthHandler with multiple checks") + @SuppressWarnings("unchecked") + void testChainAuthHandler(Vertx vertx) throws IOException { + AtomicBoolean firstHandlerCalled = new AtomicBoolean(); + AuthenticationHandlerInternal handler1 = mock(AuthenticationHandlerInternal.class); + AuthHandlerConfig config1 = mock(AuthHandlerConfig.class); + when(config1.createAuthHandler(any())).thenReturn(handler1); + doAnswer(invocation -> { + firstHandlerCalled.set(true); + ((Handler>) invocation.getArgument(1)).handle(Future.failedFuture(new HttpException(401))); + return null; + }).when(handler1).authenticate(any(), any()); + + AuthenticationHandlerInternal handler2 = mock(AuthenticationHandlerInternal.class); + AuthHandlerConfig config2 = mock(AuthHandlerConfig.class); + when(config2.createAuthHandler(any())).thenReturn(handler2); + + RoutingContext routingContextMock = mock(RoutingContext.class); + HttpServerRequest requestMock = mock(HttpServerRequest.class); + when(requestMock.isEnded()).thenReturn(true); + when(routingContextMock.request()).thenReturn(requestMock); + + ChainAuthHandler handler = ChainAuthHandler.create(vertx, List.of(config1, config2)); + handler.handle(routingContextMock); + assertThat(firstHandlerCalled.get()).isTrue(); + verify(handler2).authenticate(eq(routingContextMock), any()); + } +} diff --git a/src/test/java/io/neonbee/test/base/NeonBeeTestBase.java b/src/test/java/io/neonbee/test/base/NeonBeeTestBase.java index 64495a6c..df027003 100644 --- a/src/test/java/io/neonbee/test/base/NeonBeeTestBase.java +++ b/src/test/java/io/neonbee/test/base/NeonBeeTestBase.java @@ -45,7 +45,7 @@ import io.neonbee.entity.EntityVerticle; import io.neonbee.internal.deploy.DeployableVerticle; import io.neonbee.internal.deploy.Deployment; -import io.neonbee.internal.handler.AuthChainHandler; +import io.neonbee.internal.handler.ChainAuthHandler; import io.neonbee.internal.verticle.ServerVerticle; import io.neonbee.job.JobVerticle; import io.neonbee.test.helper.ConcurrentHelper; @@ -412,7 +412,7 @@ public DummyEntityVerticleFactory createDummyEntityVerticle(FullQualifiedName fq } private ServerVerticle createDummyServerVerticle(TestInfo testInfo) { - AuthChainHandler dummyAuthHandler = ctx -> { + ChainAuthHandler dummyAuthHandler = ctx -> { ctx.setUser(User.create(provideUserPrincipal(testInfo))); Session session = ctx.session(); if (session != null) { @@ -428,7 +428,7 @@ private ServerVerticle createDummyServerVerticle(TestInfo testInfo) { @Override protected Future mountEndpoints(Router router, List endpointConfigs, - Optional defaultAuthHandler) { + Optional defaultAuthHandler) { return super.mountEndpoints(router, endpointConfigs, Optional.of(dummyAuthHandler)); } };