/* * Copyright (c) 2012-2017, Inversoft Inc., All Rights Reserved * * 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.primeframework.mvc.test; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequestWrapper; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Locale; import java.util.function.Consumer; import org.primeframework.mock.servlet.MockHttpServletRequest; import org.primeframework.mock.servlet.MockHttpServletRequest.Method; import org.primeframework.mock.servlet.MockHttpServletResponse; import org.primeframework.mock.servlet.MockHttpSession; import org.primeframework.mock.servlet.MockServletInputStream; import org.primeframework.mvc.parameter.DefaultParameterParser; import org.primeframework.mvc.servlet.PrimeFilter; import org.primeframework.mvc.servlet.ServletObjectsHolder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Injector; /** * This class is a builder that helps create a test HTTP request that is sent to the MVC. * * @author Brian Pontarelli */ public class RequestBuilder { public final PrimeFilter filter; public final Injector injector; public final MockHttpServletRequest request; public final MockHttpServletResponse response; private Class<? extends Throwable> expectedException; public RequestBuilder(String uri, MockHttpSession session, PrimeFilter filter, Injector injector) { this.request = new MockHttpServletRequest(uri, Locale.getDefault(), false, "UTF-8", session); this.response = new MockHttpServletResponse(); this.filter = filter; this.injector = injector; } /** * Sends the HTTP request to the MVC as a CONNECT. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult connect() throws IOException, ServletException { request.setMethod(Method.CONNECT); run(); return new RequestResult(request, response, injector); } /** * Sends the HTTP request to the MVC as a DELETE. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult delete() throws IOException, ServletException { request.setMethod(Method.DELETE); run(); return new RequestResult(request, response, injector); } /** * Indicates then when the HTTP method is called an exception is expected to be thrown. * <p> * An {@link AssertionError} will be thrown if the exception is not thrown. * * @param expectedException The expected exception. * @return This. */ public RequestBuilder expectException(Class<? extends Throwable> expectedException) { this.expectedException = expectedException; return this; } /** * Sends the HTTP request to the MVC as a GET. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult get() throws IOException, ServletException { request.setPost(false); run(); return new RequestResult(request, response, injector); } public MockHttpServletRequest getRequest() { return request; } /** * Sends the HTTP request to the MVC as a HEAD. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult head() throws IOException, ServletException { request.setMethod(Method.HEAD); run(); return new RequestResult(request, response, injector); } /** * Sends the HTTP request to the MVC as a OPTIONS. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult options() throws IOException, ServletException { request.setMethod(Method.OPTIONS); run(); return new RequestResult(request, response, injector); } /** * Sends the HTTP request to the MVC as a POST. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult post() throws IOException, ServletException { request.setPost(true); run(); return new RequestResult(request, response, injector); } /** * Sends the HTTP request to the MVC as a PUT. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult put() throws IOException, ServletException { request.setMethod(Method.PUT); run(); return new RequestResult(request, response, injector); } /** * Provides the ability to setup the MockHttpServletRequest object before making the request. * * @param consumer A consumer that takes the MockHttpServletRequest. * @return This. */ public RequestBuilder setup(Consumer<MockHttpServletRequest> consumer) { consumer.accept(request); return this; } /** * Sends the HTTP request to the MVC as a TRACE. * * @return The response. * @throws IOException If the MVC throws an exception. * @throws ServletException If the MVC throws an exception. */ public RequestResult trace() throws IOException, ServletException { request.setMethod(Method.TRACE); run(); return new RequestResult(request, response, injector); } /** * Sets the method as HTTPS and server port as 443. * * @return This. */ public RequestBuilder usingHTTPS() { request.setScheme("HTTPS"); request.setServerPort(443); return this; } /** * Adds an Authorization header to the request using the specified value. * <p>Shorthand for calling * <pre> * withHeader("Authorization", value) * </pre> * * @param value The value of the <code>Authorization</code> header * @return This. */ public RequestBuilder withAuthorizationHeader(String value) { request.addHeader("Authorization", value); return this; } /** * Sets the body content. * * @param bytes The bytes. * @return This. */ public RequestBuilder withBody(byte[] bytes) { request.setInputStream(new MockServletInputStream(bytes)); return this; } /** * Sets the body content. * * @param body The body as a String. * @return This. */ public RequestBuilder withBody(String body) { try { return withBody(body.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * Sets the body content. This processes the file using FreeMarker. Use {@link #withBodyFileRaw(Path)} to skip FreeMarker processing. * * @param body The body as a {@link Path} to the file. * @param values key value pairs of replacement values for use in the file. * @return This. * @throws IOException If the file could not be loaded. */ public RequestBuilder withBodyFile(Path body, Object... values) throws IOException { return withBody(BodyTools.processTemplate(body, values)); } /** * Sets the body content. * * @param body The body as a {@link Path} to the raw file. * @return This. * @throws IOException If the file could not be loaded. */ public RequestBuilder withBodyFileRaw(Path body) throws IOException { return withBody(Files.readAllBytes(body)); } /** * Sets an HTTP request parameter as a Prime MVC checkbox widget. This can be called multiple times with the same * name it it will create a list of values for the HTTP parameter. * * @param name The name of the parameter. * @param checkedValue The checked value of the checkbox. * @param uncheckedValue The checked value of the checkbox. * @param checked If the checkbox is checked. * @return This. */ public RequestBuilder withCheckbox(String name, String checkedValue, String uncheckedValue, boolean checked) { if (checked) { request.setParameter(name, checkedValue); } request.setParameter(DefaultParameterParser.CHECKBOX_PREFIX + name, uncheckedValue); return this; } /** * Sets the content type. * * @param contentType The content type. * @return This. */ public RequestBuilder withContentType(String contentType) { request.setContentType(contentType); return this; } /** * Sets the context path. * * @param contextPath The context path. * @return This. */ public RequestBuilder withContextPath(String contextPath) { request.setContextPath(contextPath); return this; } /** * Add a cookie to the request. * * @param name The name of the cookie. * @param value The value of the cookie. * @return This. */ public RequestBuilder withCookie(String name, String value) { if (name != null) { request.addCookie(new Cookie(name, value)); } return this; } /** * Add a cookie to the request. * * @param cookie The cookie. * @return This. */ public RequestBuilder withCookie(Cookie cookie) { if (cookie != null) { request.addCookie(cookie); } return this; } /** * Sets the encoding. * * @param encoding The encoding. * @return This. */ public RequestBuilder withEncoding(String encoding) { request.setEncoding(encoding); return this; } /** * Adds a file. * * @param name The name of the file form field. * @param file The file. * @param contentType The content type. * @return This. */ public RequestBuilder withFile(String name, File file, String contentType) { request.addFile(name, file, contentType); return this; } /** * Adds a header to the request. * * @param name The name of the header. * @param value The value of the header. * @return This. */ public RequestBuilder withHeader(String name, String value) { request.addHeader(name, value); return this; } /** * Uses the given object as the JSON body for the request. This object is converted into JSON using Jackson. * * @param object The object to send in the request. * @return This. * @throws JsonProcessingException If the Jackson marshalling failed. */ public RequestBuilder withJSON(Object object) throws JsonProcessingException { ObjectMapper objectMapper = injector.getInstance(ObjectMapper.class); byte[] json = objectMapper.writeValueAsBytes(object); return withContentType("application/json").withBody(json); } /** * Uses the given string as the JSON body for the request. * * @param json The string representation of the JSON to send in the request. * @return This. */ public RequestBuilder withJSON(String json) throws JsonProcessingException { return withContentType("application/json").withBody(json); } /** * Uses the given {@link Path} object to a JSON file as the JSON body for the request. * * @param jsonFile The string representation of the JSON to send in the request. * @param values key value pairs of replacement values for use in the JSON file. * @return This. * @throws IOException If the file could not be loaded. */ public RequestBuilder withJSONFile(Path jsonFile, Object... values) throws IOException { return withContentType("application/json").withBodyFile(jsonFile, values); } /** * Sets the locale that will be used. * * @param locale The locale. * @return This. */ public RequestBuilder withLocale(Locale locale) { request.addLocale(locale); return this; } /** * Sets an HTTP request parameter. This can be called multiple times with the same name it it will create a list of * values for the HTTP parameter. * * @param name The name of the parameter. * @param value The parameter value. This is an object so toString is called on it to convert it to a String. * @return This. */ public RequestBuilder withParameter(String name, Object value) { request.setParameter(name, value.toString()); return this; } /** * Sets an HTTP request parameter as a Prime MVC radio button widget. This can be called multiple times with the same * name it it will create a list of values for the HTTP parameter. * * @param name The name of the parameter. * @param checkedValue The checked value of the checkbox. * @param uncheckedValue The checked value of the checkbox. * @param checked If the checkbox is checked. * @return This. */ public RequestBuilder withRadio(String name, String checkedValue, String uncheckedValue, boolean checked) { if (checked) { request.setParameter(name, checkedValue); } request.setParameter(DefaultParameterParser.RADIOBUTTON_PREFIX + name, uncheckedValue); return this; } /** * Append a url path segment to the current request URI. * <p> * For Example: * <pre> * .simulator.test("/user/delete") * .withUrlSegment("bar") * </pre> * This will result in a url of <code>/user/delete/bar</code>, this is equivalent to the following code: * <pre> * .simulator.test("/user/delete/" + "bar") * </pre> * * @param value The url path segment. A null value will be ignored. * @return This. */ public RequestBuilder withUrlSegment(Object value) { if (value != null) { String uri = request.getRequestURI(); if (uri.charAt(uri.length() - 1) != '/') { uri += ('/'); } request.setUri(uri + value.toString()); } return this; } void run() throws IOException, ServletException { // Remove the web objects if this instance is being used across multiple invocations ServletObjectsHolder.clearServletRequest(); ServletObjectsHolder.clearServletResponse(); try { // Build the request and response for this pass filter.doFilter(this.request, this.response, (req, resp) -> { throw new UnsupportedOperationException("The RequestSimulator class doesn't support testing " + "URIs that don't map to Prime resources"); }); } catch (Throwable e) { Class clazz = e.getClass(); if (expectedException == null || !expectedException.equals(clazz)) { throw new AssertionError("\n\tUnexpected Exception thrown: [" + clazz.getCanonicalName() + "]", e); } expectedException = null; } if (expectedException != null) { throw new AssertionError("Expected Exception were not thrown: [" + expectedException.getCanonicalName() + "]"); } // Add these back so that anything that needs them can be retrieved from the Injector after // the run has completed (i.e. MessageStore for the MVC and such) ServletObjectsHolder.setServletRequest(new HttpServletRequestWrapper(this.request)); ServletObjectsHolder.setServletResponse(this.response); } }