What's required to make mockMVC test a filter's init routine?

Donald F. Coffin picture Donald F. Coffin · Dec 28, 2013 · Viewed 19.8k times · Source

I have implemented the following CORS filter, which works when the code is executed on the server:

/*
 *    Copyright 2013 BrandsEye (http://www.brandseye.com)
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.energyos.espi.datacustodian.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.stereotype.Component;

/**
 * Adds CORS headers to requests to enable cross-domain access.
 */

@Component
public class CORSFilter implements Filter {

    private final Log logger = LogFactory.getLog(getClass());
    private final Map<String, String> optionsHeaders = new LinkedHashMap<String, String>();

    private Pattern allowOriginRegex;
    private String allowOrigin;
    private String exposeHeaders;

    public void init(FilterConfig cfg) throws ServletException {
        String regex = cfg.getInitParameter("allow.origin.regex");
        if (regex != null) {
            allowOriginRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        } else {
            optionsHeaders.put("Access-Control-Allow-Origin", "*");
        }

        optionsHeaders.put("Access-Control-Allow-Headers", "Origin, Authorization, Accept, Content-Type");
        optionsHeaders.put("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        optionsHeaders.put("Access-Control-Max-Age", "1800");
        for (Enumeration<String> i = cfg.getInitParameterNames(); i.hasMoreElements(); ) {
            String name = i.nextElement();
            if (name.startsWith("header:")) {
                optionsHeaders.put(name.substring(7), cfg.getInitParameter(name));
            }
        }

        //maintained for backward compatibility on how to set allowOrigin if not
        //using a regex
        allowOrigin = optionsHeaders.get("Access-Control-Allow-Origin");
        //since all methods now go through checkOrigin() to apply the Access-Control-Allow-Origin
        //header, and that header should have a single value of the requesting Origin since
        //Access-Control-Allow-Credentials is always true, we remove it from the options headers
        optionsHeaders.remove("Access-Control-Allow-Origin");

        exposeHeaders = cfg.getInitParameter("expose.headers");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {          
            logger.debug("CORSFilter processing: Checking for Cross Origin pre-flight OPTIONS message");
        }

        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            HttpServletRequest req = (HttpServletRequest)request;
            HttpServletResponse resp = (HttpServletResponse)response;
            if ("OPTIONS".equals(req.getMethod())) {
                allowOrigin = "*";                                          //%%%%% Test force of allowOrigin
                if (checkOrigin(req, resp)) {
                    for (Map.Entry<String, String> e : optionsHeaders.entrySet()) {
                        resp.addHeader(e.getKey(), e.getValue());
                    }

                    // We need to return here since we don't want the chain to further process
                    // a preflight request since this can lead to unexpected processing of the preflighted
                    // request or a 40x - Response Code
                    return;

                }
            } else if (checkOrigin(req, resp)) {
                if (exposeHeaders != null) {
                    resp.addHeader("Access-Control-Expose-Headers", exposeHeaders);
                }
            }
        }
        filterChain.doFilter(request, response);
    }

    private boolean checkOrigin(HttpServletRequest req, HttpServletResponse resp) {
        String origin = req.getHeader("Origin");
        if (origin == null) {
            //no origin; per W3C specification, terminate further processing for both pre-flight and actual requests
            return false;
        }

        boolean matches = false;
        //check if using regex to match origin
        if (allowOriginRegex != null) {
            matches = allowOriginRegex.matcher(origin).matches();
        } else if (allowOrigin != null) {
            matches = allowOrigin.equals("*") || allowOrigin.equals(origin);
        }

        if (matches) {

            // Activate next two lines and comment out third line if Credential Support is required
//          resp.addHeader("Access-Control-Allow-Origin", origin);
//          resp.addHeader("Access-Control-Allow-Credentials", "true");         
            resp.addHeader("Access-Control-Allow-Origin", "*");
            return true;
        } else {
            return false;
        }
    }

    public void destroy() {
    }
}

The following JUnit test uses mockMVC but fails, because the CORSFilter's "init" logic is not being executed (proven by breakpointing the JUnit test):

package org.energyos.espi.datacustodian.integration.web.filters;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.FilterConfig;

import org.energyos.espi.datacustodian.web.filter.CORSFilter;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("/spring/test-context.xml")
@Profile("test")
public class CORSFilterTests {

    private final Log logger = LogFactory.getLog(getClass());   

    @Autowired
    private CORSFilter filter;

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = webAppContextSetup(this.wac)
                .addFilters(filter).build();
    }

    @Test
    public void optionsResponse_hasCorrectFilters() throws Exception {

        RequestBuilder requestBuilder = MockMvcRequestBuilders.options("/DataCustodian/oauth/token")
                .header("Origin", "foobar")
                .header("Access-Control-Allow-Origin", "*"); 

        MvcResult result =   mockMvc.perform(requestBuilder)
                .andExpect(header().string("Access-Control-Allow-Origin", is("*")))
                .andExpect(header().string("Access-Control-Allow-Methods", is("GET, POST, PUT, DELETE, OPTIONS")))
                .andExpect(header().string("Access-Control-Allow-Headers", is("origin, authorization, accept, content-type")))
                .andExpect(header().string("Access-Control-Max-Age", is("1800")))               
                .andReturn();      
        }
    }
}

I have reviewed the available material on the internet, which seems to imply the ".addfilter(filter). element of the mockMVC @Before section should be executing the CORSFilter init routine. However, that is clearly NOT happening.

Any suggestions or recommendations would be greatly appreciated, as I am really stuck understanding how to get the "init" routine tested using the mockMVC capability.

Answer

Sotirios Delimanolis picture Sotirios Delimanolis · Dec 28, 2013

The Spring MVC Test suite is not meant to test the container configuration, it is meant to test your MVC (@Controller and other mappings) configuration . Filter#init(ServletConfig) is a container managed method.

If you really need to test it, you can mock that too

@Before
public void setup() {
    filter.init(someMockFilterConfig); // using a mock that you construct with init params and all
    this.mockMvc = webAppContextSetup(this.wac)
            .addFilters(filter).build();
}