How is unit test a secure controller that uses thymeleaf (without getting a TemplateProcessingException)?

I am trying to run unit test in spring-boot using spring security and a simple home (root) controller that uses thymeleaf to process the template. I am trying to write some unit tests to make sure that my permissions are working correctly and that the required data is hidden or shown from my template (which uses thymeleaf spring security integration). The application itself works correctly when I launch it. I just want to check that it works with a set of integration tests. You can find all the code here, but I will also include the relevant snippets:

https://github.com/azeckoski/lti_starter 

The controller is really simple and does nothing but draw a template (in the root directory - ie //)) / p>

 @Controller public class HomeController extends BaseController { @RequestMapping(method = RequestMethod.GET) public String index(HttpServletRequest req, Principal principal, Model model) { log.info("HOME: " + req); model.addAttribute("name", "HOME"); return "home"; // name of the template } } 

There are many in the template, but the corresponding bits for the test are:

 <p>Hello Spring Boot User <span th:text="${username}"/>! (<span th:text="${name}"/>)</p> <div sec:authorize="hasRole('ROLE_USER')"> This content is only shown to users (ROLE_USER). </div> <div sec:authorize="isAnonymous()"><!-- only show this when user is NOT logged in --> <h2>Form Login endpoint</h2> ... </div> 

And finally, the test:

 @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class AppControllersTest extends BaseApplicationTest { @Autowired WebApplicationContext wac; @Autowired private FilterChainProxy springSecurityFilter; private MockMvc mockMvc; @Before public void setup() { // Process mock annotations MockitoAnnotations.initMocks(this); // Setup Spring test in webapp-mode (same config as spring-boot) this.mockMvc = MockMvcBuilders.webAppContextSetup(wac) .addFilter(springSecurityFilter, "/*") .build(); } @Test public void testLoadRoot() throws Exception { // Test basic home controller request MvcResult result = this.mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andReturn(); String content = result.getResponse().getContentAsString(); assertNotNull(content); assertTrue(content.contains("Hello Spring Boot")); assertTrue(content.contains("Form Login endpoint")); } @Test public void testLoadRootWithAuth() throws Exception { Collection<GrantedAuthority> authorities = new HashSet<>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); Authentication authToken = new UsernamePasswordAuthenticationToken("azeckoski", "password", authorities); SecurityContextHolder.getContext().setAuthentication(authToken); // Test basic home controller request MvcResult result = this.mockMvc.perform(get("/")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andReturn(); String content = result.getResponse().getContentAsString(); assertNotNull(content); assertTrue(content.contains("Hello Spring Boot")); assertTrue(content.contains("only shown to users (ROLE_USER)")); } } 

Eror, which I get in BOTH of the above tests:

testLoadRoot (ltistarter.controllers.AppControllersTest) Elapsed time: 0.648 s <ERROR! org.springframework.web.util.NestedServletException: Request processing failed; nested exception org.thymeleaf.exceptions.TemplateProcessingException: an error occurred while executing the processor 'Org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor' (home: 33) in org.springframework.web.context.support.WebApplicationContextContextContext WebApplicationContextUtils.java:84) in org.thymeleaf.extras.springsecurity3.auth.AuthUtils.getExpressionHandler (AuthUtils.java:260) in org.thymeleaf.extras.springsecurity3.auth.AuthUtils.authorizeUsingAccessxpression ( .thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor.isVisible (AuthorizeAttrProcessor.java:100) in org.thymeleaf.processor.attr.AbstractConditionalVisibilityAttrProcessor. .doProcess (AbstractAttrProcessor.java:87) in org.thymeleaf.proce ssor.AbstractProcessor.process (AbstractProcessor.java:212) at org.thymeleaf.dom.Node.applyNextProcessor (Node.java: 1016) at org.thymeleaf.dom.Node.processNode (Node.java: 971) at org.thymeleaf .dom.NestableNode.computeNextChild (NestableNode.java:672) in org.thymeleaf.dom.NestableNode.doAdditionalProcess (NestableNode.java:655) in org.thymeleaf.dom.Node.processNode (Node.java: 990) at org. thymeleaf.dom.NestableNode.computeNextChild (NestableNode.java:672) in org.thymeleaf.dom.NestableNode.doAdditionalProcess (NestableNode.java:655) in org.thymeleaf.dom.Node.processNode (Node.java: 990) at org .thymeleaf.dom.NestableNode.computeNextChild (NestableNode.java:672) in org.thymeleaf.dom.NestableNode.doAdditionalProcess (NestableNode.java:655) in org.thymeleaf.dom.Node.processNode (Node.java: 990) org.thymeleaf.dom.Document.process (Document.java:93) in org.thymeleaf.TemplateEngine.process (TemplateEngine.java:1155) in org.thymeleaf.TemplateEngine.process (TemplateEngine.java:1060) at org .thymeleaf.TemplateEngine.process (TemplateEngine.java:1011) in org.thymeleaf.spring4.view.ThymeleafView.renderFragment (ThymeleafView.javahaps35) in org.thymeleaf.spring4.view.ThymeleafView.render (ThymeleafView ) in org.springframework.web.servlet.DispatcherServlet.render (DispatcherServlet.java:1221) in org.springframework.test.web.servlet.TestDispatcherServlet.render (TestDispatcherServlet.java:102) in org.springframework.web.sweb DispatcherServlet.processDispatchResult (DispatcherServlet.java:1005) in org.springframework.web.servlet.DispatcherServlet.doDispatch (DispatcherServlet.java:952) in org.springframework.web.servlet.DispatcherServlet.doService org (Dispatcher) .springframework.web.servlet.FrameworkServlet.processRequest (FrameworkServlet.java:961) in org.springframework.web.servlet.FrameworkServlet.doGet (FrameworkServlet.java:852) in javax.servlet.http.HttpServlet.service ( : 735) in org.springframework.web.servlet.Framework Servlet.service (FrameworkServlet.java:837) in org.springframework.test.web.servlet.TestDispatcherServlet.service (TestDispatcherServlet.java:62) in javax.servlet.http.HttpServlet.service (HttpServlet.java:848) .springframework.mock.web.MockFilterChain $ ServletFilterProxy.doFilter (MockFilterChain.java:170) in org.springframework.mock.web.MockFilterChain.doFilter (MockFilterChain.java:137) in org.sseilfChainFerchain.filterChainferfainChainferferferChainFerchain.FilterChainFerfainChain.FilterBain .doFilter (FilterChainProxy.javahaps30) in org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke (FilterSecurityInterceptor.java:118) in org.springframework.security.web.access.intercept.FilterSecurityIntererer .java: 84) in org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter (FilterChainProxy.javahaps42) in org.springframework.security.web.access.ExceptionTranslationFilter.doFilter (ExceptionTranslationFilter.java113. sp ringframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter (FilterChainProxy.javahaps42) in org.springframework.security.web.session.SessionManagementFilter.doFilter (SessionManagementFilter.java:103) in org.springfweb.security.seil VirtualFilterChain.doFilter (FilterChainProxy.javahaps42) in org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter (AnonymousAuthenticationFilter.java:113) in org.springframework.security.web.FilterChainProjdo Filter $) 342) in org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter (SecurityContextHolderAwareRequestFilter.java:154) in org.springframework.security.web.FilterChainProxy $ VirtualFilterChainDo Filter .web.savedrequest.RequestCacheAwareFilter.doFilter (RequestCacheAwareFilter.java:45) at org.springframework.security.web.FilterChainProxy $ V irtualFilterChain.doFilter (FilterChainProxy.javahaps42) in org.springframework.security.web.authentication.logout.LogoutFilter.doFilter (LogoutFilter.java:110) in org.springframework.security.web.FilterChainProxy. FilterOilFilterainfilter java: 342) in org.springframework.security.web.csrf.CsrfFilter.doFilterInternal (CsrfFilter.java:85) in org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilterfavajava.source .web.FilterChainProxy $ VirtualFilterChain.doFilter (FilterChainProxy.javaI4242) in org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal (HeaderWriterFilter.java:57) .java: 107) in org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter (FilterChainProxy.javahaps42) in org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter (SecurityContextPersistence Filter.java:87) in org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter (FilterChainProxy.javaI42) in org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterernererterinterinter 50) in org.springframework.web.filter.OncePerRequestFilter.doFilter (OncePerRequestFilter.java:107) in org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter (FilterChainProxy.javapringf.web.source .FilterChainProxy.doFilterInternal (FilterChainProxy.java:192) in org.springframework.security.web.FilterChainProxy.doFilter (FilterChainProxy.java:160) in org.springframework.mock.web.MockFilterChain.doFilter13 (M org.springframework.test.web.servlet.MockMvc.perform (MockMvc.java:141) in ltistarter.controllers.AppControllersTest.testLoadRoot (AppControllersTest.java:70)

HOWEVER, this only happens if both tests are enabled and springSecurityFilter is enabled. If I disable one of the tests and remove the springSecurityFilter code ( .addFilter(springSecurityFilter, "/*") ), I will not get this error anymore. I suspect something might ruin the WebApplicationContext or leave the security stuff in a failed state, but I'm not sure what I need to reset or change.

So, if I select the second test and remove SpringSecurityFilter, then my first test will still fail (in particular, it is assertTrue(content.contains("Form Login endpoint")) ), but I no longer get any errors. When I look at the generated HTML, I don’t see ANY of the tag content that uses the sec:authorize attribute.

So, I searched and found a suggestion that I need to add to springSecurityFilter (which I made in the code example above), however, as soon as I do this, I immediately get a refusal (it’s not) t even reach the point where it fails without her). Any suggestions on what causes this exception and how to fix it?

+6
source share
1 answer

I have a workaround that seems to completely solve this problem for spring-boot: 1.1.4, spring-security: 3.2.4 and thymeleaf: 2.1.3 (although this is a bit of a hack).

This is a modified unit test class:

 @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class AppControllersTest { @Autowired public WebApplicationContext context; @Autowired private FilterChainProxy springSecurityFilter; private MockMvc mockMvc; @Before public void setup() { assertNotNull(context); assertNotNull(springSecurityFilter); // Process mock annotations MockitoAnnotations.initMocks(this); // Setup Spring test in webapp-mode (same config as spring-boot) this.mockMvc = MockMvcBuilders.webAppContextSetup(context) .addFilters(springSecurityFilter) .build(); context.getServletContext().setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); } ... 

The magic here makes WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE be the actual context of the web application (which I entered). This allows me to work with the actual sec attributes: BUT my second test, where I try to set permissions so that the user is logged in, fails (it looks like the user is still ANONYMOUS).

UPDATE

There was something missing (which, in my opinion, is a gap in the way spring protection works), but this is a pretty good task (although it's a bit of a hack). See This for more details on the issue: Spring Test and Security: How to simulate authentication?

I needed to add a method that creates a session layout for the test. This method will set Principal / Authentication security and force the SecurityContext to be added to the HttpSession , which can then be added to the test request (see below for a test example and an example for the NamedOAuthPrincipal class).

 public MockHttpSession makeAuthSession(String username, String... roles) { if (StringUtils.isEmpty(username)) { username = "azeckoski"; } MockHttpSession session = new MockHttpSession(); session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); Collection<GrantedAuthority> authorities = new HashSet<>(); if (roles != null && roles.length > 0) { for (String role : roles) { authorities.add(new SimpleGrantedAuthority(role)); } } //Authentication authToken = new UsernamePasswordAuthenticationToken("azeckoski", "password", authorities); // causes a NPE when it tries to access the Principal Principal principal = new NamedOAuthPrincipal(username, authorities, "key", "signature", "HMAC-SHA-1", "signaturebase", "token"); Authentication authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); SecurityContextHolder.getContext().setAuthentication(authToken); return session; } 

Class for creating Principal (with OAuth support through ConsumerCredentials). If you are not using OAuth, you can skip the ConsumerCredentials part only to implement the principal (but you must return the GrantedAuthority collection).

 public static class NamedOAuthPrincipal extends ConsumerCredentials implements Principal { public String name; public Collection<GrantedAuthority> authorities; public NamedOAuthPrincipal(String name, Collection<GrantedAuthority> authorities, String consumerKey, String signature, String signatureMethod, String signatureBaseString, String token) { super(consumerKey, signature, signatureMethod, signatureBaseString, token); this.name = name; this.authorities = authorities; } @Override public String getName() { return name; } public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } } 

And then modify the test as follows (to create a session, and then set it to the mock request):

 @Test public void testLoadRootWithAuth() throws Exception { // Test basic home controller request with a session and logged in user MockHttpSession session = makeAuthSession("azeckoski", "ROLE_USER"); MvcResult result = this.mockMvc.perform(get("/").session(session)) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andReturn(); String content = result.getResponse().getContentAsString(); assertNotNull(content); assertTrue(content.contains("Hello Spring Boot")); } 
+9
source

Source: https://habr.com/ru/post/972969/


All Articles