mvysny / karibu-testing

Vaadin Server-Side Browserless Containerless Unit Testing
Apache License 2.0
105 stars 14 forks source link

Problem with OAuth authentication #143

Open simasch opened 1 year ago

simasch commented 1 year ago

I use Keycloak for authentication but I get this exception when trying to test with Karibu testing.

All my routes are annotated with @PermitAll. So there is no route "" available without authentication and also no LoginView.

If replace @PermitAll with @AnonymousAllowed on the "" route then it works.

com.vaadin.flow.router.NotFoundException: No route found for '': No route found for ''
Available routes: [ClubContestCreateView at '/clubcontest', EventView at '/events', StatusView at '/status']
If you'd like to revert back to the original Vaadin RouteNotFoundError, please remove the class com.github.mvysny.kaributesting.v10.MockRouteNotFoundError from Routes.errorRoutes
Available routes: [ClubContestCreateView at '/clubcontest', EventView at '/events', StatusView at '/status']
If you'd like to revert back to the original Vaadin RouteNotFoundError, please remove the class com.github.mvysny.kaributesting.v10.MockRouteNotFoundError from Routes.errorRoutes

    at com.github.mvysny.kaributesting.v10.MockRouteNotFoundError.setErrorParameter(Routes.kt:126)
    at com.vaadin.flow.router.internal.ErrorStateRenderer.notifyNavigationTarget(ErrorStateRenderer.java:121)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.notifyNavigationTarget(AbstractNavigationStateRenderer.java:623)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEvent(AbstractNavigationStateRenderer.java:600)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEventAndPopulateChain(AbstractNavigationStateRenderer.java:501)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.createChainIfEmptyAndExecuteBeforeEnterNavigation(AbstractNavigationStateRenderer.java:473)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:211)
    at com.vaadin.flow.router.internal.ErrorStateRenderer.handle(ErrorStateRenderer.java:106)
    at com.vaadin.flow.component.UI.handleExceptionNavigation(UI.java:1818)
    at com.vaadin.flow.component.UI.handleNavigation(UI.java:1793)
    at com.vaadin.flow.component.UI.renderViewForRoute(UI.java:1748)
    at com.vaadin.flow.component.UI.navigate(UI.java:1148)
    at com.vaadin.flow.component.UI.navigate(UI.java:1118)
    at com.github.mvysny.kaributesting.v10.MockVaadin.createUI$karibu_testing_v10(MockVaadin.kt:245)
    at com.github.mvysny.kaributesting.v10.MockVaadin.createSession(MockVaadin.kt:213)
    at com.github.mvysny.kaributesting.v10.MockVaadin.setup(MockVaadin.kt:95)
    at ch.pinnatec.stv.contest20.ui.KaribuTest.setup(KaribuTest.java:47)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptLifecycleMethod(TimeoutExtension.java:128)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptBeforeEachMethod(TimeoutExtension.java:78)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeMethodInExtensionContext(ClassBasedTestDescriptor.java:520)
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$synthesizeBeforeEachMethodAdapter$23(ClassBasedTestDescriptor.java:505)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachMethods$3(TestMethodTestDescriptor.java:174)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:202)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:202)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachMethods(TestMethodTestDescriptor.java:171)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:134)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: com.vaadin.flow.router.NotFoundException: No route found for ''
Available routes: [ClubContestCreateView at '/clubcontest', EventView at '/events', StatusView at '/status']
If you'd like to revert back to the original Vaadin RouteNotFoundError, please remove the class com.github.mvysny.kaributesting.v10.MockRouteNotFoundError from Routes.errorRoutes
    at com.github.mvysny.kaributesting.v10.MockRouteNotFoundError.setErrorParameter(Routes.kt:126)
    at com.vaadin.flow.router.internal.ErrorStateRenderer.notifyNavigationTarget(ErrorStateRenderer.java:121)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.notifyNavigationTarget(AbstractNavigationStateRenderer.java:623)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEvent(AbstractNavigationStateRenderer.java:600)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEventAndPopulateChain(AbstractNavigationStateRenderer.java:501)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.createChainIfEmptyAndExecuteBeforeEnterNavigation(AbstractNavigationStateRenderer.java:473)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:211)
    at com.vaadin.flow.router.internal.ErrorStateRenderer.handle(ErrorStateRenderer.java:106)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.reroute(AbstractNavigationStateRenderer.java:724)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handleTriggeredBeforeEvent(AbstractNavigationStateRenderer.java:682)
    at com.vaadin.flow.component.internal.JavaScriptNavigationStateRenderer.handleTriggeredBeforeEvent(JavaScriptNavigationStateRenderer.java:99)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEvent(AbstractNavigationStateRenderer.java:615)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEvent(AbstractNavigationStateRenderer.java:590)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.createChainIfEmptyAndExecuteBeforeEnterNavigation(AbstractNavigationStateRenderer.java:466)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.handle(AbstractNavigationStateRenderer.java:211)
    at com.vaadin.flow.component.internal.JavaScriptNavigationStateRenderer.handle(JavaScriptNavigationStateRenderer.java:78)
    at com.vaadin.flow.component.UI.handleNavigation(UI.java:1785)
    ... 81 more
Caused by: com.vaadin.flow.router.NotFoundException
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at com.vaadin.flow.internal.ReflectTools.createProxyInstance(ReflectTools.java:484)
    at com.vaadin.flow.internal.ReflectTools.createInstance(ReflectTools.java:452)
    at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:756)
    at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:740)
    at com.vaadin.flow.server.auth.ViewAccessChecker.beforeEnter(ViewAccessChecker.java:203)
    at com.vaadin.flow.router.internal.AbstractNavigationStateRenderer.sendBeforeEnterEvent(AbstractNavigationStateRenderer.java:613)
    ... 86 more

Any idea?

mvysny commented 1 year ago

Is the route detected properly in Routes.routes? I guess it is, but doesn't hurt to ask :-)

Could it be that the tests are running in production mode? If you try to access a route to which you don't have access to, Vaadin in production mode responds with 404 instead of 401.

The following lines are puzzling:

Available routes: [ClubContestCreateView at '/clubcontest', EventView at '/events', StatusView at '/status']

It comes from MockRouteNotFoundError but the list of routes comes from Vaadin registry rather than from Routes.routes. Perhaps Vaadin registry in production mode only returns routes that you have access to.

simasch commented 1 year ago

It runs in development mode Vaadin is running in DEVELOPMENT mode - do not use for production deployments.

Routes are found: Routes(routes=ClubContestCreateView, EventView, StatusView, errorRoutes=MockRouteNotFoundError)

EventView is the view with the RouteAlias "".

I'm using Vaadin 24.

mvysny commented 1 year ago
at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:756)
at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:740)
at com.vaadin.flow.server.auth.ViewAccessChecker.beforeEnter(ViewAccessChecker.java:203)

This is probably where the answer lies. The ViewAccessChecker decides to call BeforeEvent.rerouteToError() for some reason. @AnonymousAllowed disables the security checks in ViewAccessChecker completely (since it allows anyone including when no-one is logged in), that's why replacing @PermitAll with @AnonymousAllowed works.

Probably Keycloak is not set up in your mock env properly; the servlet principal is therefore null and ViewAccessChecker thinks no-one is logged in. @PermitAll requires any user to be logged in, which thus fails with a null principal.

simasch commented 1 year ago

The problem is, that this happens on startup of the test.

My test looks like this:

class EventViewIT extends KaribuTest {

  @BeforeEach
  void login() {
    login("_dz", "", List.of("ADMIN"));

    UI.getCurrent().navigate(EventView.class);
  }

  @Test
  void events_displayed() {
    _assert(Div.class, 2, s -> s.withClasses("contest-card"));
  }
}

And the failure happens here in my base test class

  @BeforeEach
  public void setup() {
    final Function0<UI> uiFactory = UI::new;
    final SpringServlet servlet = new MockSpringServlet(routes, ctx, uiFactory);
    MockVaadin.setup(uiFactory, servlet);
  }

And there is no login view. How should this work?

mvysny commented 1 year ago

At the moment, when Karibu is mocking the UI (during call to MockVaadin.setup()), it also navigates to the main view (""), which fails in your case. The reason for this navigation is to simulate the initial Vaadin state properly, which includes showing the root page. You can see the code in MockVaadin.createUI().

The navigation is only performed if the route exists:

            if (UI.getCurrent().internals.router.registry.getNavigationTarget("").isPresent) {
                UI.getCurrent().navigate("")
            }

So, apparently the route exists in your case, and thus the navigation is attempted, which then fails.

ViewAccessChecker contains the following lines:

            if (loginView != null) {
                beforeEnterEvent.forwardTo(loginView);
            } else {
                if (loginUrl != null) {
                    beforeEnterEvent.forwardToUrl(loginUrl);
                } else {
                    beforeEnterEvent.rerouteToError(NotFoundException.class);
                }
            }

Apparently in your case loginView and loginUrl is null (since Keycloak is expected to do the security handling), and thus rerouteToError(NotFoundException.class) is called. So, the route in fact exists.

The solution would be to initialize Keycloak before calling MockVaadin.setup(), so that Keycloak returns a non-null Principal in ViewAccessChecker.beforeEnter(). Could you please try out this solution and let me know whether it works?

One way could be to bypass Keycloak completely and use a fake/mock implementation of ViewAccessChecker which simply always provides a dummy non-null Principal. Maybe this could help: https://github.com/mvysny/vaadin-simple-security/blob/master/src/main/java/com/github/mvysny/vaadinsimplesecurity/SimpleViewAccessChecker.java .