Spring MVC Test with HtmlUnit - How to test a JSON response - java

Background: Search form in a webapp, with an auto-complete/suggestion. Using jQuery's autocomplete, suggestions are shown after typing a few characters. These suggesitons are retrieved as JSON data from one of the webapp's controllers.
Issue: I am testing the application via the HtmlUnit Integration of Spring Test, which works fine for Text/Html Pages, but for the JSON responses here, the setup seems to fail (see error below).
Manually testing (the actual webapp) via Browser works and fetching JSON from "real" pages works as well ( see "json()" test) -> Should testing JSON responses work via HtmlUnit / Spring Test setup and if yes, what am I doing wrong?
Update (2017-06-21):
Using
#ResponseBody String
and building the JSON "manually" (not letting Spring automagically doing it) works; not really what I wanted, but at least I can properly test it this way ...
Test:
package my.project;
import com.gargoylesoftware.htmlunit.*;
import org.junit.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.*;
import org.springframework.test.*;
import org.springframework.web.context.support.GenericWebApplicationContext;
import javax.servlet.ServletException;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {TestWebAppConfig.class,
// avoid context caching
MyControllerIT.class})
#WebAppConfiguration
public class MyControllerIT {
protected static final String SERVER_URL = "http://localhost";
protected WebClient webClient;
protected MockHttpServletRequest request;
protected MockHttpServletResponse response;
#Autowired
protected GenericWebApplicationContext webApplicationContext;
#Before
public void setUp() throws Exception {
this.request = new MockHttpServletRequest();
this.request.setServerName("Gondor.Osgiliath");
this.response = new MockHttpServletResponse();
this.webClient = this.initClient();
}
protected final WebClient initClient() throws ServletException {
DefaultMockMvcBuilder mockMvcBuilder = MockMvcBuilders.webAppContextSetup(this.webApplicationContext);
MockMvc mockMvc = mockMvcBuilder.build();
WebClient webClient = new WebClient();
webClient.setWebConnection(new MockMvcWebConnection(mockMvc));
return webClient;
}
// https://stackoverflow.com/questions/2932857/html-handling-a-json-response
#Test
public void json() throws Exception {
webClient = new WebClient(BrowserVersion.INTERNET_EXPLORER, "some.proxy", 7890);
Page page = webClient.getPage("https://stackoverflow.com/users/flair/97901.json");
WebResponse webResponse = page.getWebResponse();
String contentType = webResponse.getContentType();
String contentAsString = webResponse.getContentAsString();
}
#Test
public void suggestShouldReturnJSON() throws Exception {
Page page = webClient.getPage(SERVER_URL + MyController.SUGGEST_URL + "?term=asdf");
WebResponse webResponse = page.getWebResponse();
String contentType = webResponse.getContentType();
String contentAsString = webResponse.getContentAsString();
}
#Test
public void suggestShouldReturnJSONViaMockMvc() throws Exception {
MockMvc springMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
ResultActions resultActions = springMvc.perform(MockMvcRequestBuilders
.get(MyController.SUGGEST_URL + "?term=asdf")
.accept(MediaType.APPLICATION_JSON_VALUE));
resultActions.andDo(MockMvcResultHandlers.print());
}
}
Controller:
package my.project;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Arrays;
import java.util.List;
#Controller
public class MyController {
public static final String SUGGEST_URL = "/suggest";
//http://api.jqueryui.com/autocomplete/#option-source
#RequestMapping(value = {SUGGEST_URL,}, produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody
List<Suggestion> suggest(#RequestParam(name = "term") String term) {
return Arrays.asList(new Suggestion("label1", "value1"), new Suggestion("label2", "value2"));
}
}
Config (Thymeleaf omitted for the moment):
package my.project;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#EnableWebMvc
#ComponentScan(basePackages = {"my.project.*",})
#Configuration
#Import(ThymeleafConfig.class)
abstract class TestWebAppConfig extends WebMvcConfigurerAdapter {
}
Error:
com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException: 406 Not Acceptable for http://localhost/suggest?term=asdf
at com.gargoylesoftware.htmlunit.WebClient.throwFailingHttpStatusCodeExceptionIfNecessary(WebClient.java:571)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:396)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:304)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:451)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:436)
at my.project.MyControllerIt.suggestShouldReturnJSON(MyControllerIt.java:
...

Related

Why does this Spring Boot MVC test fail when autowired dependency replaced with interface or abstract class?

This is the Controller as it appears at the moment.
import com.myorg.service.MyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
#Controller
public class MyController {
private final MyService myService;
#Autowired
public MyController(MyService myService) {
this.myService = myService;
}
#GetMapping(value = "/my/homepage")
public String someMethod(ModelMap model, Authentication authentication) {
myService.doSomething(authentication);
return "homepage";
}
}
Here is the Controller Advice to handle exceptions
#ExceptionHandler(HttpStatusCodeException.class)
public ModelAndView handleException(HttpServletRequest req, HttpStatusCodeException ex) {
log.error(ex.getMessage(), ex);
ModelAndView mav = new ModelAndView();
// do stuff
return mav;
}
And this is the current test, which works fine
import com.myorg.service.MyService
import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.security.test.context.support.WithMockUser
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import spock.lang.Specification
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
#SpringBootTest
class MyControllerSpec extends Specification {
#Autowired
private WebApplicationContext webApplicationContext
#SpringBean
private MyService myService = Mock()
#WithMockUser(username = "test#email.com", roles = ["USER"])
def "MyController(test=should not authenticate)"() {
setup:
def notAuthorizedException = new HttpClientErrorException(HttpStatus.UNAUTHORIZED, "blah", null, Charset.defaultCharset())
MockMvc mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build()
myService.doSomething(_) >> { throw notAuthorizedException }
when:
MvcResult mvcResult = mvc.perform(get("/my/homepage"))
.andExpect(status().is3xxRedirection())
.andReturn()
then:
mvcResult.response.status == 302
mvcResult.modelAndView.viewName == "redirect:/my/login?logout"
}
}
The test passes successfully in that the mock throws the HttpClientErrorException, which the advice catches and then redirects the user to a logout page.
But when I replace the MyService member in the MyController class with a class that implements an interface, the test fails because the HttpClientErrorException is no longer being caught by the advice and instead is being thrown as a nested exception.
Here is the new Controller
import com.myorg.service.MyServiceThatImplementsAnInterface;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
#Controller
public class MyController {
private final MyInterface myInterface;
#Autowired
public MyController(MyInterface myInterface) {
this.myInterface = myInterface;
}
#GetMapping(value = "/my/homepage")
public String someMethod(ModelMap model, Authentication authentication) {
myInterface.doSomething(authentication);
return "homepage";
}
}
Here is the interface implementation
#Component("myInterface")
public class MyServiceThatImplementsAnInterfaceImpl implements MyInterface {
private final SomeComponent someComponent;
public MyServiceThatImplementsAnInterfaceImpl(SomeComponent comeComponent) {
this.someComponent = someComponent;
}
#Override
public void doSomething(Authentication authenticationToken) {
someComponent.doSomethingElse();
}
}
And here is the new test
#SpringBootTest
class MyControllerSpec extends Specification {
#Autowired
private WebApplicationContext webApplicationContext
#SpringBean
private MyServiceThatImplementsAnInterfaceImpl myServiceThatImplementsAnInterface = Mock()
#WithMockUser(username = "test#email.com", roles = ["USER"])
def "MyController(test=should not authenticate)"() {
setup:
def notAuthorizedException = new HttpClientErrorException(HttpStatus.UNAUTHORIZED, "blah", null, Charset.defaultCharset())
MockMvc mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build()
myServiceThatImplementsAnInterface.doSomething(_) >> { throw notAuthorizedException }
when:
MvcResult mvcResult = mvc.perform(get("/my/homepage"))
.andExpect(status().is3xxRedirection())
.andReturn()
then:
mvcResult.response.status == 302
mvcResult.modelAndView.viewName == "redirect:/my/login?logout"
}
}
Here is the failing test stack trace
Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at app//org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
at app//org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at app//javax.servlet.http.HttpServlet.service(HttpServlet.java:497)
at app//org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at app//org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
at app//javax.servlet.http.HttpServlet.service(HttpServlet.java:584)
at app//org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
at app//org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:337)
at app//org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
at app//org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122)
at app//org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126)
at app//org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:109)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:219)
at app//org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:213)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)
at app//org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
at app//org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
at app//org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
at app//org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:110)
at app//org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:80)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)
at app//org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at app//org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at app//org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:221)
at app//org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186)
at app//org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurer$DelegateFilter.doFilter(SecurityMockMvcConfigurer.java:132)
at app//org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at app//org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:201)
at com.myorg.controller.MyControllerSpec.<where my test is>
Caused by: org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at com.myorg.controller.MyControllerSpec.<where my test is>
Any help is appreciated
Problem solved and, as with about 90% of these kinds of problems, the error was simple to fix. In my test class, I simply hadn't assigned the exception handler class to the MockMvc.
def "MyController(test=should not authenticate)"() {
setup:
Authentication authentication = //Just mock out an Authentication object here
def notAuthorizedException = //As before, just mock out an exception
myInterface.doSomething(_) >> { throw notAuthorizedException }
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver()
viewResolver.setPrefix("prefix")
MyController myController = new MyController(myServiceThatImplementsAnInterface)
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(myController)
.setControllerAdvice(new MyExceptionHandler())
.setViewResolvers(viewResolver)
.build()
when:
MvcResult mvcResult = mockMvc.perform(get("/my/homepage")
.principal(authentication))
.andExpect(status().is3xxRedirection())
.andReturn()
then:
mvcResult.response.status == 302
mvcResult.modelAndView.viewName == "redirect:/app/login?logout"
}
#ControllerAdvice(assignableTypes = MyInterface.class)
public class MyExceptionHandler {
#ExceptionHandler(HttpStatusCodeException.class)
public ModelAndView handleException(HttpServletRequest req, HttpStatusCodeException ex) {
log.error(ex.getMessage(), ex);
ModelAndView mav = new ModelAndView();
// do stuff
return mav;
}
}

Spring Mvc integration test failed

I have done a Spring Mvc application and I have been trying to write some integration tests, but I keep getting errors. Here is my AgentService:
#Service
public class AgentService {
#Autowired
private AgentRepository agentRepository;
#Autowired
private AgencyRepository repoAg;
#Autowired
private AgentMapper mapper;
public List<AgentDto> listByAgency(Long id) {
List<Agent> agents = agentRepository.findAll().stream().filter(ag -> ag.getAgency().getId().equals(id)).collect(Collectors.toList());
List<AgentDto> mappedAgents = agents.stream().map(agent -> mapper.mapToDto(agent)).collect(Collectors.toList());
return mappedAgents;
}
}
And my AgentController:
#Controller
public class AgentController {
#Autowired
private AgentService agentService;
#GetMapping("/viewAgentsPage/{id}")
public String viewAgentsPage(#PathVariable(value = "id") Long id, Model model) {
model.addAttribute("listAgents", agentService.listByAgency(id));
return "agents";
}
}
This is the test I've written:
package com.example.awbdproject.controller;
import com.example.awbdproject.models.Agency;
import com.example.awbdproject.models.Agent;
import com.example.awbdproject.services.AgentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.ui.Model;
import static org.mockito.Mockito.doReturn;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
#SpringBootTest
#AutoConfigureMockMvc
public class AgentControllerTest {
#Autowired
MockMvc mockMvc;
#MockBean
AgentService agentService;
#MockBean
Model model;
#Test
#WithMockUser
public void showAgentsByAgency() throws Exception {
List<AgentDto> savedAgents = new ArrayList<AgentDto>();
Long id = 1L;
Agency agencyTest = new Agency();
agencyTest.setId(id);
AgentDto agentTest = new AgentDto();
agentTest.setId(1L);
agentTest.setFirstName("test");
agentTest.setLastName("test");
agentTest.setEmail("email#gmail.com");
agentTest.setAgency(agencyTest);
savedAgents.add(agentTest);
when(agentService.listByAgency(id)).thenReturn(savedAgents);
mockMvc.perform(get("/viewAgentsPage/{id}", "1"))
.andExpect(status().isOk())
.andExpect(view().name("agents"))
.andExpect(model().attribute("listAgents", savedAgents))
.andExpect(content().contentType("text/html;charset=UTF-8"));
}
When I run the test I'm getting this error:
MockHttpServletRequest:
HTTP Method = GET
Request URI = /viewAgentsPage/1
Parameters = {}
Headers = []
Body = null
Session Attrs = {SPRING_SECURITY_SAVED_REQUEST=DefaultSavedRequest [http://localhost/viewAgentsPage/1]}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 302
Error message = null
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY", Location:"http://localhost/login"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = http://localhost/login
Cookies = []
What seems to be the issue here? I have never used Mockito and can't seem to wrap my head around it. I think it might have something to do with Spring Security. If it helps, this is my security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().antMatchers(
"/register/**",
"/js/**",
"/css/**",
"/img/**"
).permitAll().anyRequest().authenticated()
.and().formLogin().loginPage("/login")
.permitAll()
.and().logout().invalidateHttpSession(true).clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout").permitAll();
}

After disabled Http GET method, how to unit test it?

So in my Spring controller, i have this route
#RequestMapping(value = "myroute", method = POST)
public String myRoute(HttpServletRequest) {
...
}
We use MockMvcBuilder to mock the POST calls.
MockHttpServletRequestBuilder post = post(myRouteUrl);
mockMvc.perform(post).andExpect(status().isOK());
Now if i just change post to get, the status back is still 200.
So how to verify GET method is failing ?
Using Java 1.8.
Thanks !
I got a HTTP error 405 when I tried the following. Is there something else missing from the example shown?
This is the controller class I used.
package demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
#RestController
public class MyController {
#RequestMapping(value = "/myroute", method = GET)
public String myRoute(HttpServletRequest request) {
System.out.println("HERE");
return "good";
}
}
This is the test class I used.
package demo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
#ExtendWith(MockitoExtension.class)
#WebMvcTest(MyController.class)
class MyControllerTest {
#Autowired
private MockMvc mockMvc;
#Test
void myRoute() throws Exception {
MockHttpServletRequestBuilder post = post("/myroute");
mockMvc.perform(post).andExpect(status().isOk());
}
}
Error Message:
Error message = Request method 'POST' not supported
Headers = [Allow:"GET"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Status expected:<200> but was:<405>
Expected :200
Actual :405

Why requests are returned 404 status when using WebFilter corsFilter in webflux?

I want to build a REST api in Spring Webflux using functional endpoints. For CORS I use a WebFilter corsFilter method which sets the required headers. I do see that the method is called (I see the log messages from it) and I see that the headers on the response are indeed the ones I set in my Webflux api. However, as soon as I started to use the corsFilter the requests return 404 status (earlier they would return JSON). I suspect that corsFilter doesn't pass over the request to the router functions. Why would that be?
Specifically I'm wondering if this line is enough to connect the cors config with the routes:
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(RouterFunctions.toWebHandler(route))
.applicationContext(ctx).build();
This is my main class:
package com.mypackage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import reactor.ipc.netty.http.server.HttpServer;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static org.springframework.web.reactive.function.server.RequestPredicates.method;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
import static org.springframework.web.reactive.function.server.RouterFunctions.nest;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RouterFunctions.toHttpHandler;
#SpringBootApplication
public class Server {
private static final Logger log = LogManager.getLogger(Server.class);
public static final String HOST = "localhost";
public static final int PORT = 8080;
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CorsConfiguration.class);
Server server = new Server();
server.startReactorServer(ctx);
System.out.println("Press ENTER to exit.");
System.in.read();
}
public RouterFunction<ServerResponse> routingFunction() {
PersonRepository repository = new DummyPersonRepository();
PersonHandler handler = new PersonHandler(repository);
return nest(path("/person"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), handler::getPerson)
.andRoute(method(HttpMethod.GET), handler::listPeople)
).andRoute(POST("/").and(contentType(APPLICATION_JSON)), handler::createPerson));
}
public void startReactorServer(AnnotationConfigApplicationContext ctx) {
RouterFunction<ServerResponse> route = this.routingFunction().filter((request, next) -> {
log.warn(request.path());
if (request.path().contains("person")) {
log.warn("calling next()");
return next.handle(request);
} else {
return ServerResponse.status(UNAUTHORIZED).build();
}
});
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(RouterFunctions.toWebHandler(route))
.applicationContext(ctx).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create(HOST, PORT);
server.newHandler(adapter).block();
}
}
and this is my CORS config class:
package com.mypackage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Configuration
#EnableWebFlux
public class CorsConfiguration {
private static final Logger log = LogManager.getLogger(CorsConfiguration.class);
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN, mode";
private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
private static final String ALLOWED_ORIGIN = "*";
private static final String MAX_AGE = "3600";
#Bean
public WebFilter corsFilter() {
log.warn("from CorsConfiguration!!!");
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
log.warn("after ServerHttpRequest");
if (CorsUtils.isCorsRequest(request)) {
log.warn("inside isCorsRequest");
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
To use the functional approach when defining your endpoints Spring Boot's official documentation has a very simple example.
FooBarApplication.class this is our main class.
#SpringBootApplication
public class FooBarApplication {
public static void main(String[] args) {
SpringApplication.run(FooBarApplication.class, args);
}
}
RoutingConfiguration.class (or whatever you wanna call it)
#Configuration
public class RoutingConfiguration {
#Bean
public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) {
return route(GET("/{user}").and(accept(APPLICATION_JSON)), userHandler::getUser)
.andRoute(GET("/{user}/customers").and(accept(APPLICATION_JSON)), userHandler::getUserCustomers)
.andRoute(DELETE("/{user}").and(accept(APPLICATION_JSON)), userHandler::deleteUser);
}
}
#Component
public class UserHandler {
public Mono<ServerResponse> getUser(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
// ...
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
// ...
}
}
any class annotated with #Configuration will be run at startup and run all #Bean annotated methods. So this will run the monoRouterFunction and set up all our routes for us.
Example taken from the official spring boot documentation Spring boot webflux scroll down a little bit.
EDIT:
and as a side note the #EnableWebFlux annotation means that you will disable the auto-configuration of webflux and set upp configuration manually. I do not recommend this if you are just starting out (i know the name is very misleading) you can read about the webflux auto-configuration here Spring WebFlux Auto-configuration
EDIT2:
WebFlux has a built in CorsFilter that you can use all you need is to configure it.
#Bean
CorsWebFilter corsWebFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList("http://allowed-origin.com"));
corsConfig.setMaxAge(8000L);
corsConfig.addAllowedMethod("PUT");
corsConfig.addAllowedHeader("Baeldung-Allowed");
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsWebFilter(source);
}
Example taken from Enabling CORS with a WebFilter

JUnit test is not triggering method in Spring controller

I want to test my controller with the help of JUnit. I am new to this. I have written some code for this but it is not coming to my function listCard. My controller is:
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
#RequestMapping("/v1/card")
#Configuration
public class CardManagementController {
private final static Logger LOG = LoggerFactory
.getLogger(CardManagementController.class);
#Autowired
ICardService cardService;
#RequestMapping(value = "", produces = RestURIConstants.APPLICATION_JSON, method = RequestMethod.GET)
public #ResponseBody GetCardResponse getCard(
#ModelAttribute #Valid GetCardRequest request, BindingResult results)
throws RuntimeException, ValidationException {
if (results.hasErrors()) {
LOG.error("error occured occur while fetching card response");
throw new ValidationException(
"Error Occoured while validiating card request");
}
GetCardResponse response = null;
response = cardService.getCard(request);
return response;
}
#RequestMapping(value = "", produces = RestURIConstants.APPLICATION_JSON, method = RequestMethod.POST)
public #ResponseBody AddCardResponse addCard(
#ModelAttribute AddCardRequest request, BindingResult results)
throws RuntimeException, ValidationException {
if (results.hasErrors()) {
LOG.error("error occured while adding the card");
throw new ValidationException(
"Error Occoured while validiating addcard request");
}
LOG.debug("add Card with POST method");
AddCardResponse response = null;
response = cardService.addCard(request);
return response;
}
#RequestMapping(value = "", produces = RestURIConstants.APPLICATION_JSON, method = RequestMethod.DELETE)
public #ResponseBody DeleteCardResponse deleteCard(
#ModelAttribute #Valid DeleteCardRequest request,
BindingResult results) throws RuntimeException, ValidationException {
if (results.hasErrors()) {
LOG.debug("error occured while delting the card");
throw new ValidationException(
"Error Occoured while validiating delete card request");
}
DeleteCardResponse response = null;
response = cardService.deleteCard(request);
return response;
}
#RequestMapping(value = RestURIConstants.LISTCARD, produces = RestURIConstants.APPLICATION_JSON, method = RequestMethod.GET)
public #ResponseBody ListCardResponse listCard(
#ModelAttribute #Valid ListCardRequest request) throws RuntimeException, ValidationException {
ListCardResponse response = null;
response = cardService.listCards(request);
return response;
}
#ExceptionHandler({ ValidationException.class})
#ResponseBody
public CPPException handleValidationException(ValidationException ex) {
LOG.error("Exception occoured",ex);
CPPException exception = new CPPException(ex.getMessage());
exception.setStatus(500);
return exception;
}
#ExceptionHandler({RuntimeException.class})
#ResponseBody
public CPPException handleException(RuntimeException ex) {
LOG.error("Exception occoured", ex);
CPPException exception = new CPPException("Internal Server Error");
exception.setStatus(500);
return exception;
}
}
and I have written the following code for testing:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.my.cpp.controller.CardManagementController;
import com.my.cpp.request.ListCardRequest;
import com.my.cpp.service.impl.CardServiceImpl;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations={"classpath:/spring/application-context.xml"})
public class CardApiTest {
private MockMvc mockMvc;
//#Autowired
private CardManagementController cm=new CardManagementController();
#Autowired
private CardServiceImpl cmr;
#Before
public void setUp() throws Exception {
mockMvc= MockMvcBuilders.standaloneSetup(cm).build();
}
#Test
public void testList() throws Exception{
final ListCardRequest lr=new ListCardRequest();
this.mockMvc.perform(get("/v1/card/list?"));
}
}
First - Remove the #Configuration annotation from your controller. It doesn't belong here.
Second - Consider using Mockito while testing, since you have a service injected in your controller. Your updated test class should look something similar as below
#RunWith(MockitoJUnitRunner.class)
public class CardApiTest {
private MockMvc mockMvc;
#InjectMocks
private CardManagementController cm;
#Mock
private ICardService cardService;
#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(cm).build();
// Instantiate cardListRequest and cardListResponse here
when(cardService.listCards(cardListRequest)).thenReturn(cardListResponse);
}
#Test
public void testList() throws Exception{
this.mockMvc.perform(get("/v1/card/list?"));
}
}
Let know in comments if you need further info / assistance.

Categories

Resources