How to unit test a secured controller which uses thymeleaf (without getting TemplateProcessingException)?

前端 未结 2 1610
故里飘歌
故里飘歌 2021-01-05 16:46

I am trying to run a unit test in spring-boot using spring security and a simple home (root) controller which uses thymeleaf for the template processing. I am trying to writ

相关标签:
2条回答
  • 2021-01-05 17:14

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

    This is the 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 is forcing the WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE to be the actual web app context (which I injected). This allows the actual sec: attributes to work BUT my second test where I try to set the authority so the user is logged in does not pass (it looks like the user is still ANONYMOUS).

    UPDATE

    There was something missing (which I think is a gap in how spring security works) but it is lucky fairly easy to solve (though it's a bit of a hack). See this for more details on the issue: Spring Test & Security: How to mock authentication?

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

    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 to create the Principal (with OAuth support via ConsumerCredentials). If you are not using OAuth then you can skip the ConsumerCredentials part just implement the Principal (but you should return the collection of GrantedAuthority).

    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 like so (to create the session and then set it on 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"));
    }
    
    0 讨论(0)
  • 2021-01-05 17:30

    If you don't care about testing returned view and would like to only test the controller, simply disable Thymeleaf is Spring boot 2+ in your test application properties file

    spring.thymeleaf.enabled=false
    
    0 讨论(0)
提交回复
热议问题