如何对使用百日咳的安全控制器进行单元测试(没有得到TemplateProcessingException)?

时间:2014-07-28 16:10:47

标签: spring-mvc spring-security spring-boot thymeleaf spring-test-mvc

我正在尝试使用spring security和一个简单的home(root)控制器在spring-boot中运行单元测试,该控制器使用百日咳进行模板处理。我正在尝试编写一些单元测试来验证我的安全权限是否正常工作以及正确的数据是否隐藏或显示在我的模板中(使用百合花弹簧安全集成)。应用程序本身在运行时可以正常工作。我只是想验证它是否正在使用一组集成测试。 你可以在这里找到所有代码,但我也会在下面包含相关的代码:

https://github.com/azeckoski/lti_starter

控制器非常简单,只做渲染模板(在根部 - 即" /")。

@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
    }
}

模板中有很多,但测试的相关位是:

<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>

最后测试:

@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)"));
    }
}

我对上述测试的看法是:

  

testLoadRoot(ltistarter.controllers.AppControllersTest)已用时间:   0.648秒&lt;&lt;&lt;错误! org.springframework.web.util.NestedServletException:Request   处理失败;嵌套异常是   org.thymeleaf.exceptions.TemplateProcessingException:期间出错   执行处理器   &#39; org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor&#39;   (家:33)在   org.springframework.web.context.support.WebApplicationContextUtils.getRequiredWebApplicationContext(WebApplicationContextUtils.java:84)     在   org.thymeleaf.extras.springsecurity3.auth.AuthUtils.getExpressionHandler(AuthUtils.java:260)     在   org.thymeleaf.extras.springsecurity3.auth.AuthUtils.authorizeUsingAccessExpression(AuthUtils.java:182)     在   org.thymeleaf.extras.springsecurity3.dialect.processor.AuthorizeAttrProcessor.isVisible(AuthorizeAttrProcessor.java:100)     在   org.thymeleaf.processor.attr.AbstractConditionalVisibilityAttrProcessor.processAttribute(AbstractConditionalVisibilityAttrProcessor.java:58)     在   org.thymeleaf.processor.attr.AbstractAttrProcessor.doProcess(AbstractAttrProcessor.java:87)     在   org.thymeleaf.processor.AbstractProcessor.process(AbstractProcessor.java:212)     在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)     在   org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:655)     在org.thymeleaf.dom.Node.processNode(Node.java:990)at   org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:672)     在   org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:655)     在org.thymeleaf.dom.Node.processNode(Node.java:990)at   org.thymeleaf.dom.NestableNode.computeNextChild(NestableNode.java:672)     在   org.thymeleaf.dom.NestableNode.doAdditionalProcess(NestableNode.java:655)     在org.thymeleaf.dom.Node.processNode(Node.java:990)at   org.thymeleaf.dom.Document.process(Document.java:93)at   org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1155)at   org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1060)at   org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1011)at   org.thymeleaf.spring4.view.ThymeleafView.renderFragment(ThymeleafView.java:335)     在   org.thymeleaf.spring4.view.ThymeleafView.render(ThymeleafView.java:190)     在   org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1221)     在   org.springframework.test.web.servlet.TestDispatcherServlet.render(TestDispatcherServlet.java:102)     在   org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1005)     在   org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:952)     在   org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870)     在   org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)     在   org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)     在javax.servlet.http.HttpServlet.service(HttpServlet.java:735)at   org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)     在   org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:62)     在javax.servlet.http.HttpServlet.service(HttpServlet.java:848)at   org.springframework.mock.web.MockFilterChain $ ServletFilterProxy.doFilter(MockFilterChain.java:170)     在   org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:137)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:330)     在   org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:118)     在   org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:113)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:85)     在   org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57)     在   org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)     在   org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)     在   org.springframework.security.web.FilterChainProxy $ VirtualFilterChain.doFilter(FilterChainProxy.java:342)     在   org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)     在   org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)     在   org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:137)     在   org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:141)     在   ltistarter.controllers.AppControllersTest.testLoadRoot(AppControllersTest.java:70)

然而,只有在启用了两个测试并且包含springSecurityFilter时才会发生这种情况。如果我禁用其中一个测试并删除springSecurityFilter代码(.addFilter(springSecurityFilter, "/*")),那么我不再收到该错误。我怀疑某些东西可能会弄乱WebApplicationContext或者让安全资料处于某种故障状态但我不确定我需要重置或更改。

因此,如果我取出第二个测试并删除springSecurityFilter,那么我的第一个测试仍将失败(特别是assertTrue(content.contains("Form Login endpoint"))),但我不再收到任何错误。当我查看生成的HTML时,我没有看到任何使用sec:authorize属性的标签内容。

所以我四处搜索并找到了一个建议,我需要添加springSecurityFilter(我在上面的代码示例中已经完成),然而,一旦我这样做,我立即得到了失败(它如果没有它,它甚至无法达到失败的程度)。有关导致该异常的原因以及如何解决该问题的任何建议?

2 个答案:

答案 0 :(得分:9)

我有一个解决方案似乎完全解决了spring-boot的这个问题:1.1.4,spring-security:3.2.4和thymeleaf:2.1.3(虽然它有点像黑客)。 / p>

这是修改后的单元测试类:

@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);
    }
...

这里的神奇之处在于强迫WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE成为实际的Web应用程序上下文(我注入了它)。 这允许实际的sec:属性工作但是我的第二次测试我尝试设置权限,因此用户登录时没有通过(看起来用户仍然是ANONYMOUS)。

更新

缺少一些东西(我认为这是弹簧安全性如何工作的一个空白),但幸运的是相当容易解决(虽然它有点像黑客)。有关此问题的详细信息,请参阅此处:Spring Test & Security: How to mock authentication?

我需要添加一个为测试创建模拟会话的方法。此方法将设置安全性Principal / Authentication并强制SecurityContext加入HttpSession,然后可将其添加到测试请求中(请参阅下面的测试代码段和{{1}类示例)。

NamedOAuthPrincipal

用于创建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; } 的类(通过ConsumerCredentials提供OAuth支持)。如果您没有使用OAuth,那么您可以跳过ConsumerCredentials部分,只需实现Principal(但您应该返回GrantedAuthority的集合)。

Principal

然后像这样修改测试(创建会话然后在模拟请求上设置它):

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;
    }
}

答案 1 :(得分:0)

如果您不关心测试返回的视图而只想测试控制器,只需在您的测试应用程序属性文件中禁用 Thymeleaf is Spring boot 2+

spring.thymeleaf.enabled=false