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.
MockMvc is defined as a main entry point for server-side Spring MVC testing. Tests with MockMvc lie somewhere between between unit and integration tests.
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(); }
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()); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With