Test View Access Control
This page covers Spring Security and Quarkus Security in separate sections. For Java EE/CDI, use the CDI setup with your application’s CDI access-control beans and login flow.
First configure view protection in your application. These examples assume a public default route, a login view, and views restricted to specific roles. Adapt the route names and roles to your application.
Spring Security
Start with the Spring Boot test setup.
Simulate Users with a Secured Context
With @SpringBootTest, view access control works without extra setup.
Define the shared browserless application context with createSecured() instead of create():
Source code
BrowserlessContextConfiguration.java
BrowserlessContextConfiguration.javaimport org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import com.vaadin.browserless.SecuredBrowserlessApplicationContext;
import com.vaadin.browserless.SpringBrowserlessApplicationContext;
@Configuration
public class BrowserlessContextConfiguration {
@Bean
SecuredBrowserlessApplicationContext<Authentication> browserlessApplicationContext(
ApplicationContext applicationContext) {
return SpringBrowserlessApplicationContext.createSecured(
applicationContext, Application.class);
}
}A secured context is also a BrowserlessApplicationContext, so tests that don’t need credentials can keep autowiring that type; their newUser() creates an anonymous user.
Give each user the credentials to simulate when you create it:
-
newUser()creates an anonymous user, like Spring Security’s@WithAnonymousUser. -
newUser("admin", "ADMIN")creates an authenticated user with the given username and roles, like@WithMockUser. A role without theROLE_prefix gets it. -
newUser(authentication)installs anyAuthenticationobject, for example one built from your own user details.
The credentials are in place before the user’s window opens and navigates to the root route, so a logged-in user isn’t redirected to the login view, and custom redirect logic for authenticated users works as expected. Each test creates its user with try-with-resources, so that the user is closed when the test ends:
Source code
Tests with Simulated Users
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.Authentication;
import com.vaadin.browserless.SecuredBrowserlessApplicationContext;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.html.Anchor;
@SpringBootTest
class ViewSecurityTest {
@Autowired
private SecuredBrowserlessApplicationContext<Authentication> app;
@Test
void anonymousUser_protectedView_redirectToLogin() {
try (var anonymous = app.newUser()) {
anonymous.newWindow().navigate("protected", LoginView.class);
}
}
@Test
void anonymousUser_publicView_signInLinkPresent() {
try (var anonymous = app.newUser()) {
var window = anonymous.newWindow();
// public view is default page
Assertions.assertInstanceOf(PublicView.class,
window.getCurrentView());
Anchor anchor = window.find(Anchor.class).withText("Sign in")
.single();
Assertions.assertTrue(
window.test(anchor).isUsable(),
"Sign in link should be available for anonymous user");
}
}
@Test
void adminUser_adminView_viewShown() {
try (var admin = app.newUser("admin", "ADMIN")) {
var window = admin.newWindow();
window.navigate(AdminRoleView.class);
Assertions.assertTrue(
window.find(Avatar.class).single().isVisible(),
"Avatar should be visible for logged users");
}
}
}Closing the user also clears its authentication from the test thread, so that it doesn’t leak into the next test.
Spring Security test annotations, such as @WithMockUser, don’t affect the users of a secured context.
Test with a Reduced Application Context
In a test that selects its beans with @SpringJUnitConfig or @ContextConfiguration instead of loading the full application context, register view access control yourself.
Such a test doesn’t component-scan the shared bean, so it also creates a secured application context of its own for each test, as described in Create an Application Context for Each Test.
Vaadin applies access control through a NavigationAccessControl registered as a BeforeEnterListener for the UI.
Provide a VaadinServiceInitListener that registers it, in a test configuration class:
Source code
Set Up NavigationAccessControl in a Test Configuration
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.flow.spring.security.SpringNavigationAccessControl;
@TestConfiguration
class TestViewSecurityConfig {
@Bean
VaadinServiceInitListener setupViewSecurityScenario() {
SpringNavigationAccessControl accessControl = new SpringNavigationAccessControl();
accessControl.setLoginView(LoginView.class);
return event -> {
event.getSource().addUIInitListener(uiEvent -> {
uiEvent.getUI().addBeforeEnterListener(accessControl);
});
};
}
}Annotate the class with @TestConfiguration rather than @Configuration.
Spring Boot’s component scan skips test configuration classes, so the class doesn’t leak into @SpringBootTest contexts in the same package, which already have access control.
Alternatively, import the out-of-the-box NavigationAccessControlInitializer, which requires only a NavigationAccessControl bean:
Source code
Set Up NavigationAccessControl with NavigationAccessControlInitializer
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import com.vaadin.flow.server.auth.NavigationAccessControl;
import com.vaadin.flow.spring.security.NavigationAccessControlInitializer;
import com.vaadin.flow.spring.security.SpringNavigationAccessControl;
@TestConfiguration
@Import({NavigationAccessControlInitializer.class})
class TestViewSecurityConfig {
@Bean
NavigationAccessControl navigationAccessControl() {
SpringNavigationAccessControl accessControl = new SpringNavigationAccessControl();
accessControl.setLoginView(LoginView.class);
return accessControl;
}
}Use this variant only in a reduced context.
With Spring Security, a @SpringBootTest context already defines a navigationAccessControl bean, so importing the class there fails with a bean definition override error.
When the test needs custom user objects or complex grant rules, load the user from a UserDetailsService and pass an Authentication for it to newUser().
The following test provides its own UserDetailsService, and also includes the TestViewSecurityConfig class from the start of this section, which applies view access control:
Source code
Tests with a Mock UserDetailsService
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.vaadin.browserless.BrowserlessUIContext;
import com.vaadin.browserless.SecuredBrowserlessApplicationContext;
import com.vaadin.browserless.SpringBrowserlessApplicationContext;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.router.RouteNotFoundError;
@SpringJUnitConfig({ SecurityTestConfig.class, TestViewSecurityConfig.class })
class SpringUnitSecurityTest {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private UserDetailsService userDetailsService;
private SecuredBrowserlessApplicationContext<Authentication> app;
@BeforeEach
void setUp() {
app = SpringBrowserlessApplicationContext.createSecured(
applicationContext, AdminRoleView.class);
}
@AfterEach
void tearDown() {
app.close();
}
@Test
void superuser_adminView_viewShown() {
var window = openWindowAs("admin");
window.navigate(AdminRoleView.class);
Assertions.assertTrue(
window.find(Avatar.class).single().isVisible(),
"Avatar should be visible for logged users");
}
@Test
void user_adminView_accessDenied() {
var window = openWindowAs("user");
RouteNotFoundError errorView = window.navigate("admin-role",
RouteNotFoundError.class);
Assertions.assertTrue(
errorView.getElement().getChild(0).getOuterHTML()
.contains("Reason: Access is denied"),
"Admin view should be accessible only by users with ADMIN role");
}
private BrowserlessUIContext openWindowAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
return app.newUser(UsernamePasswordAuthenticationToken.authenticated(
user, user.getPassword(), user.getAuthorities())).newWindow();
}
}
@TestConfiguration
class SecurityTestConfig {
@Bean
UserDetailsService mockUserDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
if ("user".equals(username)) {
return new User(username, UUID.randomUUID().toString(),
List.of(
new SimpleGrantedAuthority("ROLE_DEV"),
new SimpleGrantedAuthority("ROLE_USER")
));
}
if ("admin".equals(username)) {
return new User(username, UUID.randomUUID().toString(),
List.of(
new SimpleGrantedAuthority("ROLE_SUPERUSER"),
new SimpleGrantedAuthority("ROLE_ADMIN")
));
}
throw new UsernameNotFoundException(
"User " + username + " not exists");
}
};
}
}With @SpringBootTest, inject the application’s own UserDetailsService in the same way.
Navigate after Changing Authentication
A user’s credentials are in place before its window opens, so the window’s first navigation already observes them. When a test changes the authentication later — for example, by running the application’s own programmatic login in the window — navigate again to apply access control to the new authentication:
Source code
Java
var window = app.newUser().newWindow();
window.navigate("admin-role", LoginView.class);
// Run the application's own login action here.
// The window still shows the login view. Navigating again applies
// access control to the current authentication.
window.navigate(AdminRoleView.class);The secured context keeps the authentication that a window of the user changes, and the user’s other windows observe it as well. See Authentication Timing for why reloading the current location does not replace this step.
Use Spring Security Test Annotations
A test that extends SpringBrowserlessTest simulates its user with Spring Security test annotations instead — such as @WithMockUser, @WithAnonymousUser, or @WithUserDetails.
The annotations require the Spring Security test dependency:
Source code
XML
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>The authentication is in place before the base class creates the UI and navigates to the default route. See the Spring Security documentation for the annotations.
Source code
Base-Class Tests with Mock Users
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import com.vaadin.browserless.SpringBrowserlessTest;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.html.Anchor;
@SpringBootTest
public class ViewSecurityTest extends SpringBrowserlessTest {
@Test
@WithAnonymousUser
void anonymousUser_protectedView_redirectToLogin() {
navigate("protected", LoginView.class);
}
@Test
@WithAnonymousUser
void anonymousUser_publicView_signInLinkPresent() {
// public view is default page
Assertions.assertInstanceOf(PublicView.class, getCurrentView());
Anchor anchor = find(Anchor.class).withText("Sign in").single();
Assertions.assertTrue(
test(anchor).isUsable(),
"Sign in link should be available for anonymous user");
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void adminUser_adminView_viewShown() {
navigate(AdminRoleView.class);
Assertions.assertTrue(
find(Avatar.class).single().isVisible(),
"Avatar should be visible for logged users");
}
}An annotation with setupBefore = TestExecutionEvent.TEST_EXECUTION establishes the authentication only after the initial navigation, so navigate again as described in Navigate after Changing Authentication.
|
Caution
|
In base-class tests, overriding beans with @MockitoBean or @MockBean makes Spring cache a separate application context for that test class. In a multi-class run, this can prevent the simulated user from being applied during navigation, causing protected views to redirect unexpectedly to the login view — sometimes in other test classes. If security navigation tests fail only when the whole suite runs, suspect a bean override elsewhere. See Using a Reduced Application Context for a context-friendly way to replace services.
|
Quarkus Security
Start with the Quarkus test setup.
Set Up View Access Control
The Vaadin Quarkus extension doesn’t register view access control automatically, so browserless tests register it themselves.
Vaadin applies access control through a NavigationAccessControl registered as a BeforeEnterListener for the UI.
Register it in a QuarkusTestProfile class that provides an observer for the Vaadin ServiceInitEvent:
Source code
NavigationAccessControl for Quarkus Project Test
import jakarta.enterprise.event.Observes;
import com.vaadin.browserless.quarkus.mocks.MockQuarkusServletService;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.auth.NavigationAccessControl;
import io.quarkus.arc.profile.IfBuildProfile;
import io.quarkus.test.junit.QuarkusTestProfile;
public class ViewSecurityTestProfile implements QuarkusTestProfile {
@Override
public String getConfigProfile() {
return "test-security"; 1
}
@IfBuildProfile("test-security") 1
public static class NavigationAccessControlObserver {
public void serviceInit(@Observes ServiceInitEvent event) { 2
// @QuarkusTest starts the whole application, so we check
// the VaadinService type to enable access control only for
// browserless tests
if (event.getSource() instanceof MockQuarkusServletService) { 3
event.getSource().addUIInitListener(uiEvent -> {
// Customize the NavigationAccessControl as needed
NavigationAccessControl accessControl = new NavigationAccessControl();
accessControl.setLoginView(LoginView.class);
uiEvent.getUI().addBeforeEnterListener(accessControl);
});
}
}
}
}-
Sets the configuration profile to be used for the test. The observer class is annotated with
@IfBuildProfile, so it’s active only in tests that use this profile. -
Listens for Vaadin
ServiceInitEvent. This is the same as implementingVaadinServiceInitListenerand registering the class to be loaded by JavaServiceLoader. -
Checks that execution is started by the browserless test. This is required because
@QuarkusTestcauses the whole application to start when running the test.
Simulate Users with a Secured Context
When Quarkus Security is active in the application, create the browserless application context with createSecured(), and give each user the credentials to simulate when you create it:
-
newUser()creates an anonymous user. -
newUser("admin", "ADMIN")creates an authenticated user with the given username and roles. -
newUser(identity)installs anySecurityIdentity, such as one built withQuarkusSecurityIdentity:Source code
Java
SecurityIdentity identity = QuarkusSecurityIdentity.builder() .setPrincipal(new QuarkusPrincipal("john")) .addRoles(Set.of("USER")) .setAnonymous(false) .build(); var window = app.newUser(identity).newWindow();
As with Spring, the credentials are in place before the user’s window opens and navigates to the default route, so logged-in users aren’t redirected to the login view:
Source code
Tests with Simulated Users
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.vaadin.browserless.SecuredBrowserlessApplicationContext;
import com.vaadin.browserless.quarkus.QuarkusBrowserlessApplicationContext;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.html.Anchor;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
@QuarkusTest
@TestProfile(ViewSecurityTestProfile.class) 1
class ViewSecurityTest {
private SecuredBrowserlessApplicationContext<SecurityIdentity> app;
@BeforeEach
void setUp() {
app = QuarkusBrowserlessApplicationContext
.createSecured(PublicView.class);
}
@AfterEach
void tearDown() {
app.close();
}
@Test
void anonymousUser_protectedView_redirectToLogin() {
var window = app.newUser().newWindow(); 2
window.navigate("protected", LoginView.class);
}
@Test
void anonymousUser_publicView_signInLinkPresent() {
var window = app.newUser().newWindow();
// public view is default page
Assertions.assertInstanceOf(PublicView.class, window.getCurrentView());
Anchor anchor = window.find(Anchor.class).withText("Sign in").single();
Assertions.assertTrue(
window.test(anchor).isUsable(),
"Sign in link should be available for anonymous user");
}
@Test
void adminUser_adminView_viewShown() {
var window = app.newUser("admin", "ADMIN").newWindow(); 3
window.navigate(AdminRoleView.class);
Assertions.assertTrue(
window.find(Avatar.class).single().isVisible(),
"Avatar should be visible for logged users");
}
}-
Selects the test profile that registers view access control.
-
Creates an anonymous user.
-
Creates an authenticated user with the
ADMINrole.
Use Quarkus Test Security
A test that extends QuarkusBrowserlessTest simulates its user with the Quarkus @TestSecurity annotation instead.
When Quarkus Security is on the classpath, the base class reads the authentication details from the Quarkus SecurityIdentity, before it creates the UI and navigates to the default route.
See the Quarkus Security Testing documentation for the annotation.
Add the Quarkus Security test dependency:
Source code
XML
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>Annotate the test methods with @TestSecurity, providing the username and roles that should be granted:
Source code
Base-Class Tests with Mock Users
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.vaadin.browserless.quarkus.QuarkusBrowserlessTest;
import com.vaadin.flow.component.avatar.Avatar;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.security.TestSecurity;
@QuarkusTest
@TestProfile(ViewSecurityTestProfile.class)
class ViewSecurityTest extends QuarkusBrowserlessTest {
@Test 1
void anonymousUser_protectedView_redirectToLogin() {
navigate("protected", LoginView.class);
}
@Test
@TestSecurity(user = "admin", roles = "ADMIN") 2
void adminUser_adminView_viewShown() {
navigate(AdminRoleView.class);
Assertions.assertTrue(
find(Avatar.class).single().isVisible(),
"Avatar should be visible for logged users");
}
}-
A test without
@TestSecurityruns as an anonymous user. Don’t use@TestSecurity(authorizationEnabled = false)for this: it disables Quarkus security checks, such as@RolesAllowedon services that the view calls. -
Simulates an authenticated user with the
ADMINrole.
Run the Security Tests
Run the security tests together with the rest of the suite using mvn test.
For concurrent users and security isolation, see Test Multiple Users and Windows.
011585C3-F724-4B8D-B0B2-A52595587AAA