Reputation: 685
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.
Upvotes: 30
Views: 30528
Reputation: 14467
After lots of tests, here's what we adopted:
@RestController
use MockMvc.TestRestTemplate
. With MockMvc, addFilter(Filter)
did not result in the execution of the filter at all. The solution with TestRestTemplate
is more primitive, but all Filters configured in your application/libraries are executed. Example:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MySpringBootApplication.class, webEnvironment=
SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyRestControllerTest {
@LocalServerPort
private int port;
@Test
public void myTestCase() throws Exception {
HttpStatus expectedStatusCode = HttpStatus.OK;
String expectedResponseBody = "{\"someProperty\" : \"someValue\" }";
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer YourTokenJwtForExample");
HttpEntity<String> entity = new HttpEntity<>(null, headers);
TestRestTemplate restTemplate = new TestRestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:" + port + "/my-rest-uri",
HttpMethod.GET, entity, String.class);
Assert.assertEquals(expectedStatusCode, response.getStatusCode());
Assert.assertEquals(expectedResponseBody, response.getBody());
}
}
Upvotes: 8
Reputation: 3059
For a Spring Boot
app, if @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
is used then filter.init() is called automatically. if @SpringBootTest
is used with default parameters then filter.init() needs to be invoked manually.
Upvotes: 3
Reputation: 1870
If you want a true unit test instead of an integration test, you may also want to take a look at org.springframework.mock.web.MockServletConfig
available from
org.springframework:spring-test
maven artifact
You can set up config parameters on the mock object. There are also mocks for HttpServletRequest, HttpServletResponse and FilterChain
Upvotes: 2
Reputation: 279960
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();
}
Upvotes: 42