diff --git a/build.gradle b/build.gradle index aeae50994f7..216662d3f2c 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ configure(allprojects) { project -> ext.httpclientVersion = "4.5" ext.httpasyncVersion = "4.1" ext.jackson2Version = "2.6.0" + ext.htmlunitVersion = "2.18" ext.jasperreportsVersion = "6.1.0" ext.javamailVersion = "1.5.4" ext.jettyVersion = "9.3.1.v20150714" @@ -56,6 +57,7 @@ configure(allprojects) { project -> ext.poiVersion = "3.12" ext.protobufVersion = "2.6.1" ext.reactorVersion = "2.0.4.RELEASE" + ext.seleniumVersion = "2.46.0" ext.slf4jVersion = "1.7.12" ext.snakeyamlVersion = "1.15" ext.snifferVersion = "1.14" @@ -1009,6 +1011,8 @@ project("spring-test") { optional("com.jayway.jsonpath:json-path:2.0.0") optional("org.skyscreamer:jsonassert:1.2.3") optional("xmlunit:xmlunit:${xmlunitVersion}") + optional("net.sourceforge.htmlunit:htmlunit:$htmlunitVersion") + optional("org.seleniumhq.selenium:selenium-htmlunit-driver:$seleniumVersion") testCompile(project(":spring-context-support")) testCompile(project(":spring-oxm")) testCompile("javax.mail:javax.mail-api:${javamailVersion}") @@ -1028,6 +1032,7 @@ project("spring-test") { } testCompile("org.hsqldb:hsqldb:${hsqldbVersion}") testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}") + testCompile("org.apache.httpcomponents:httpclient:$httpclientVersion") } task testNG(type: Test) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java new file mode 100644 index 00000000000..1fc2e176709 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.springframework.util.Assert; + +import com.gargoylesoftware.htmlunit.WebConnection; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; + +/** + * Implementation of {@link WebConnection} that allows delegating to various + * {@code WebConnection} implementations. + * + *

For example, if you host your JavaScript on the domain {@code code.jquery.com}, + * you might want to use the following. + * + *

+ * WebClient webClient = new WebClient();
+ *
+ * MockMvc mockMvc = ...
+ * MockMvcWebConnection mockConnection = new MockMvcWebConnection(mockMvc);
+ * mockConnection.setWebClient(webClient);
+ *
+ * WebRequestMatcher cdnMatcher = new UrlRegexRequestMatcher(".*?//code.jquery.com/.*");
+ * WebConnection httpConnection = new HttpWebConnection(webClient);
+ * WebConnection webConnection = new DelegatingWebConnection(mockConnection, new DelegateWebConnection(cdnMatcher, httpConnection));
+ *
+ * webClient.setWebConnection(webConnection);
+ *
+ * WebClient webClient = new WebClient();
+ * webClient.setWebConnection(webConnection);
+ * 
+ * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + */ +public final class DelegatingWebConnection implements WebConnection { + + private final List connections; + + private final WebConnection defaultConnection; + + + public DelegatingWebConnection(WebConnection defaultConnection, List connections) { + Assert.notNull(defaultConnection, "defaultConnection must not be null"); + Assert.notEmpty(connections, "connections must not be empty"); + this.connections = connections; + this.defaultConnection = defaultConnection; + } + + public DelegatingWebConnection(WebConnection defaultConnection, DelegateWebConnection... connections) { + this(defaultConnection, Arrays.asList(connections)); + } + + @Override + public WebResponse getResponse(WebRequest request) throws IOException { + for (DelegateWebConnection connection : this.connections) { + if (connection.getMatcher().matches(request)) { + return connection.getDelegate().getResponse(request); + } + } + return this.defaultConnection.getResponse(request); + } + + + public static final class DelegateWebConnection { + + private final WebRequestMatcher matcher; + + private final WebConnection delegate; + + + public DelegateWebConnection(WebRequestMatcher matcher, WebConnection delegate) { + this.matcher = matcher; + this.delegate = delegate; + } + + private WebRequestMatcher getMatcher() { + return matcher; + } + + private WebConnection getDelegate() { + return delegate; + } + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/ForwardRequestPostProcessor.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/ForwardRequestPostProcessor.java new file mode 100644 index 00000000000..75284bf4475 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/ForwardRequestPostProcessor.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.util.Assert; + +/** + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + */ +final class ForwardRequestPostProcessor implements RequestPostProcessor { + + private final String forwardUrl; + + + public ForwardRequestPostProcessor(String forwardUrl) { + Assert.hasText(forwardUrl, "forwardUrl must not be null or empty"); + this.forwardUrl = forwardUrl; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + request.setServletPath(this.forwardUrl); + return request; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java new file mode 100644 index 00000000000..e6cee1e665a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcher.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.gargoylesoftware.htmlunit.WebRequest; + +/** + * A {@link WebRequestMatcher} that allows matching on the host and optionally + * the port of {@code WebRequest#getUrl()}. + * + *

For example, the following would match any request to the host + * {@code "code.jquery.com"} without regard for the port. + * + *

WebRequestMatcher cdnMatcher = new HostMatcher("code.jquery.com");
+ * + *

Multiple hosts can also be passed in. For example, the following would + * match any request to the host {@code "code.jquery.com"} or the host + * {@code "cdn.com"} without regard for the port. + * + *

WebRequestMatcher cdnMatcher = new HostMatcher("code.jquery.com", "cdn.com");
+ * + *

Alternatively, one can also specify the port. For example, the following would match + * any request to the host {@code "code.jquery.com"} with the port of {@code 80}. + * + *

WebRequestMatcher cdnMatcher = new HostMatcher("code.jquery.com:80");
+ * + *

The above {@code cdnMatcher} would match {@code "http://code.jquery.com/jquery.js"} + * which has a default port of {@code 80} and {@code "http://code.jquery.com:80/jquery.js"}. + * However, it would not match {@code "https://code.jquery.com/jquery.js"} + * which has a default port of {@code 443}. + * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + * @see UrlRegexRequestMatcher + * @see org.springframework.test.web.servlet.htmlunit.DelegatingWebConnection + */ +public final class HostRequestMatcher implements WebRequestMatcher { + + private final Set hosts = new HashSet(); + + + /** + * Create a new {@code HostRequestMatcher} for the given hosts — + * for example: {@code "localhost"}, {@code "example.com:443"}, etc. + * @param hosts the hosts to match on + */ + public HostRequestMatcher(String... hosts) { + this.hosts.addAll(Arrays.asList(hosts)); + } + + @Override + public boolean matches(WebRequest request) { + URL url = request.getUrl(); + String host = url.getHost(); + + if (this.hosts.contains(host)) { + return true; + } + + int port = url.getPort(); + if (port == -1) { + port = url.getDefaultPort(); + } + String hostAndPort = host + ":" + port; + + return this.hosts.contains(hostAndPort); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java new file mode 100644 index 00000000000..d9ef067c6ce --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -0,0 +1,516 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.springframework.beans.Mergeable; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.SmartRequestBuilder; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.util.Assert; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import com.gargoylesoftware.htmlunit.CookieManager; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.util.NameValuePair; + +/** + * Internal class used to transform a {@link WebRequest} into a + * {@link MockHttpServletRequest} using Spring MVC Test's {@link RequestBuilder}. + * + *

By default the first path segment of the URL is used as the contextPath. + * To override this default see {@link #setContextPath(String)}. + * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + * @see MockMvcWebConnection + */ +final class HtmlUnitRequestBuilder implements RequestBuilder, Mergeable { + + private static final Pattern LOCALE_PATTERN = Pattern.compile("^\\s*(\\w{2})(?:-(\\w{2}))?(?:;q=(\\d+\\.\\d+))?$"); + + private final Map sessions; + + private final WebClient webClient; + + private final WebRequest webRequest; + + private String contextPath; + + private RequestBuilder parentBuilder; + + private SmartRequestBuilder parentPostProcessor; + + private RequestPostProcessor forwardPostProcessor; + + + /** + * Construct a new {@code HtmlUnitRequestBuilder}. + * @param sessions a {@link Map} from session {@linkplain HttpSession#getId() IDs} + * to currently managed {@link HttpSession} objects; never {@code null} + * @param webClient the WebClient for retrieving cookies + * @param webRequest the {@link WebRequest} to transform into a + * {@link MockHttpServletRequest}; never {@code null} + */ + public HtmlUnitRequestBuilder(Map sessions, WebClient webClient, WebRequest webRequest) { + Assert.notNull(sessions, "sessions map must not be null"); + Assert.notNull(webClient, "webClient must not be null"); + Assert.notNull(webRequest, "webRequest must not be null"); + + this.sessions = sessions; + this.webClient = webClient; + this.webRequest = webRequest; + } + + public MockHttpServletRequest buildRequest(ServletContext servletContext) { + String charset = getCharset(); + String httpMethod = this.webRequest.getHttpMethod().name(); + UriComponents uriComponents = uriComponents(); + + MockHttpServletRequest result = new HtmlUnitMockHttpServletRequest(servletContext, httpMethod, + uriComponents.getPath()); + parent(result, this.parentBuilder); + result.setServerName(uriComponents.getHost()); // needs to be first for additional headers + authType(result); + result.setCharacterEncoding(charset); + content(result, charset); + contextPath(result, uriComponents); + contentType(result); + cookies(result); + headers(result); + locales(result); + servletPath(uriComponents, result); + params(result, uriComponents); + ports(uriComponents, result); + result.setProtocol("HTTP/1.1"); + result.setQueryString(uriComponents.getQuery()); + result.setScheme(uriComponents.getScheme()); + pathInfo(uriComponents,result); + + return postProcess(result); + } + + private MockHttpServletRequest postProcess(MockHttpServletRequest request) { + if (this.parentPostProcessor != null) { + request = this.parentPostProcessor.postProcessRequest(request); + } + if (this.forwardPostProcessor != null) { + request = this.forwardPostProcessor.postProcessRequest(request); + } + return request; + } + + private void parent(MockHttpServletRequest result, RequestBuilder parent) { + if (parent == null) { + return; + } + + MockHttpServletRequest parentRequest = parent.buildRequest(result.getServletContext()); + + // session + HttpSession parentSession = parentRequest.getSession(false); + if (parentSession != null) { + Enumeration attrNames = parentSession.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = attrNames.nextElement(); + Object attrValue = parentSession.getAttribute(attrName); + result.getSession().setAttribute(attrName, attrValue); + } + } + + // header + Enumeration headerNames = parentRequest.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String attrName = headerNames.nextElement(); + Enumeration attrValues = parentRequest.getHeaders(attrName); + while (attrValues.hasMoreElements()) { + String attrValue = attrValues.nextElement(); + result.addHeader(attrName, attrValue); + } + } + + // parameter + Map parentParams = parentRequest.getParameterMap(); + for (Map.Entry parentParam : parentParams.entrySet()) { + String paramName = parentParam.getKey(); + String[] paramValues = parentParam.getValue(); + result.addParameter(paramName, paramValues); + } + + // cookie + Cookie[] parentCookies = parentRequest.getCookies(); + if (parentCookies != null) { + result.setCookies(parentCookies); + } + + // request attribute + Enumeration parentAttrNames = parentRequest.getAttributeNames(); + while (parentAttrNames.hasMoreElements()) { + String parentAttrName = parentAttrNames.nextElement(); + result.setAttribute(parentAttrName, parentRequest.getAttribute(parentAttrName)); + } + } + + /** + * Set the contextPath to be used. + *

The value may be null in which case the first path segment of the + * URL is turned into the contextPath. Otherwise it must conform to + * {@link HttpServletRequest#getContextPath()} which states it can be + * an empty string, or it must start with a "/" and not end with a "/". + * @param contextPath a valid contextPath + * @throws IllegalArgumentException if the contextPath is not a valid {@link HttpServletRequest#getContextPath()} + */ + public void setContextPath(String contextPath) { + MockMvcWebConnection.validateContextPath(contextPath); + this.contextPath = contextPath; + } + + public void setForwardPostProcessor(RequestPostProcessor forwardPostProcessor) { + this.forwardPostProcessor = forwardPostProcessor; + } + + private void authType(MockHttpServletRequest request) { + String authorization = header("Authorization"); + if (authorization != null) { + String[] authzParts = authorization.split(": "); + request.setAuthType(authzParts[0]); + } + } + + private void content(MockHttpServletRequest result, String charset) { + String requestBody = this.webRequest.getRequestBody(); + if (requestBody == null) { + return; + } + try { + result.setContent(requestBody.getBytes(charset)); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private void contentType(MockHttpServletRequest result) { + String contentType = header("Content-Type"); + result.setContentType(contentType == null ? MediaType.ALL_VALUE.toString() : contentType); + } + + private void contextPath(MockHttpServletRequest result, UriComponents uriComponents) { + if (this.contextPath == null) { + List pathSegments = uriComponents.getPathSegments(); + if (pathSegments.isEmpty()) { + result.setContextPath(""); + } + else { + result.setContextPath("/" + pathSegments.get(0)); + } + } + else { + if (!uriComponents.getPath().startsWith(this.contextPath)) { + throw new IllegalArgumentException(uriComponents.getPath() + " should start with contextPath " + + this.contextPath); + } + result.setContextPath(this.contextPath); + } + } + + private void cookies(MockHttpServletRequest result) { + String cookieHeaderValue = header("Cookie"); + Cookie[] parentCookies = result.getCookies(); + List cookies = new ArrayList(); + if (cookieHeaderValue != null) { + StringTokenizer tokens = new StringTokenizer(cookieHeaderValue, "=;"); + while (tokens.hasMoreTokens()) { + String cookieName = tokens.nextToken().trim(); + if (!tokens.hasMoreTokens()) { + throw new IllegalArgumentException("Expected value for cookie name '" + cookieName + + "'. Full cookie was " + cookieHeaderValue); + } + String cookieValue = tokens.nextToken().trim(); + processCookie(result, cookies, new Cookie(cookieName, cookieValue)); + } + } + + Set managedCookies = this.webClient.getCookies(this.webRequest.getUrl()); + for (com.gargoylesoftware.htmlunit.util.Cookie cookie : managedCookies) { + processCookie(result, cookies, new Cookie(cookie.getName(), cookie.getValue())); + } + if (parentCookies != null) { + for (Cookie cookie : parentCookies) { + cookies.add(cookie); + } + } + if (!cookies.isEmpty()) { + result.setCookies(cookies.toArray(new Cookie[0])); + } + } + + private void processCookie(MockHttpServletRequest result, List cookies, Cookie cookie) { + cookies.add(cookie); + if ("JSESSIONID".equals(cookie.getName())) { + result.setRequestedSessionId(cookie.getValue()); + result.setSession(httpSession(result, cookie.getValue())); + } + } + + private String getCharset() { + String charset = this.webRequest.getCharset(); + if (charset == null) { + return "ISO-8859-1"; + } + return charset; + } + + private String header(String headerName) { + return this.webRequest.getAdditionalHeaders().get(headerName); + } + + private void headers(MockHttpServletRequest result) { + for (Entry header : this.webRequest.getAdditionalHeaders().entrySet()) { + result.addHeader(header.getKey(), header.getValue()); + } + } + + private MockHttpSession httpSession(MockHttpServletRequest request, final String sessionid) { + MockHttpSession session; + synchronized (this.sessions) { + session = this.sessions.get(sessionid); + if (session == null) { + session = new HtmlUnitMockHttpSession(request, sessionid); + session.setNew(true); + synchronized (this.sessions) { + this.sessions.put(sessionid, session); + } + addSessionCookie(request, sessionid); + } + else { + session.setNew(false); + } + } + return session; + } + + private void addSessionCookie(MockHttpServletRequest request, String sessionid) { + getCookieManager().addCookie(createCookie(request, sessionid)); + } + + private void removeSessionCookie(MockHttpServletRequest request, String sessionid) { + getCookieManager().removeCookie(createCookie(request, sessionid)); + } + + private com.gargoylesoftware.htmlunit.util.Cookie createCookie(MockHttpServletRequest request, String sessionid) { + return new com.gargoylesoftware.htmlunit.util.Cookie(request.getServerName(), "JSESSIONID", sessionid, + request.getContextPath() + "/", null, request.isSecure(), true); + } + + private void locales(MockHttpServletRequest result) { + String locale = header("Accept-Language"); + if (locale == null) { + result.addPreferredLocale(Locale.getDefault()); + } + else { + String[] locales = locale.split(", "); + for (int i = locales.length - 1; i >= 0; i--) { + result.addPreferredLocale(parseLocale(locales[i])); + } + } + } + + private void params(MockHttpServletRequest result, UriComponents uriComponents) { + for (Entry> values : uriComponents.getQueryParams().entrySet()) { + String name = values.getKey(); + for (String value : values.getValue()) { + try { + result.addParameter(name, URLDecoder.decode(value, "UTF-8")); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + } + for (NameValuePair param : this.webRequest.getRequestParameters()) { + result.addParameter(param.getName(), param.getValue()); + } + } + + private Locale parseLocale(String locale) { + Matcher matcher = LOCALE_PATTERN.matcher(locale); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid locale " + locale); + } + String language = matcher.group(1); + String country = matcher.group(2); + if (country == null) { + country = ""; + } + String qualifier = matcher.group(3); + if (qualifier == null) { + qualifier = ""; + } + return new Locale(language, country, qualifier); + } + + private void pathInfo(UriComponents uriComponents, MockHttpServletRequest result) { + result.setPathInfo(null); + } + + private void servletPath(MockHttpServletRequest result, String requestPath) { + String servletPath = requestPath.substring(result.getContextPath().length()); + if ("".equals(servletPath)) { + servletPath = null; + } + result.setServletPath(servletPath); + } + + private void servletPath(UriComponents uriComponents, MockHttpServletRequest result) { + if ("".equals(result.getPathInfo())) { + result.setPathInfo(null); + } + servletPath(result, uriComponents.getPath()); + } + + private void ports(UriComponents uriComponents, MockHttpServletRequest result) { + int serverPort = uriComponents.getPort(); + result.setServerPort(serverPort); + if (serverPort == -1) { + int portConnection = this.webRequest.getUrl().getDefaultPort(); + result.setLocalPort(serverPort); + result.setRemotePort(portConnection); + } + else { + result.setRemotePort(serverPort); + } + } + + private UriComponents uriComponents() { + URL url = this.webRequest.getUrl(); + UriComponentsBuilder uriBldr = UriComponentsBuilder.fromUriString(url.toExternalForm()); + return uriBldr.build(); + } + + @Override + public boolean isMergeEnabled() { + return true; + } + + @Override + public Object merge(Object parent) { + if (parent == null) { + return this; + } + if (parent instanceof RequestBuilder) { + this.parentBuilder = (RequestBuilder) parent; + } + if (parent instanceof SmartRequestBuilder) { + this.parentPostProcessor = (SmartRequestBuilder) parent; + } + + return this; + } + + + /** + * An extension to {@link MockHttpServletRequest} that ensures that + * when a new {@link HttpSession} is created, it is added to the managed sessions. + */ + private final class HtmlUnitMockHttpServletRequest extends MockHttpServletRequest { + + private HtmlUnitMockHttpServletRequest(ServletContext servletContext, String method, String requestURI) { + super(servletContext, method, requestURI); + } + + public HttpSession getSession(boolean create) { + HttpSession result = super.getSession(false); + if (result == null && create) { + HtmlUnitMockHttpSession newSession = new HtmlUnitMockHttpSession(this); + setSession(newSession); + newSession.setNew(true); + String sessionid = newSession.getId(); + synchronized (HtmlUnitRequestBuilder.this.sessions) { + HtmlUnitRequestBuilder.this.sessions.put(sessionid, newSession); + } + addSessionCookie(this, sessionid); + result = newSession; + } + return result; + } + + public HttpSession getSession() { + return super.getSession(); + } + + public void setSession(HttpSession session) { + super.setSession(session); + } + } + + /** + * An extension to {@link MockHttpSession} that ensures when + * {@link #invalidate()} is called that the {@link HttpSession} is + * removed from the managed sessions. + */ + private final class HtmlUnitMockHttpSession extends MockHttpSession { + + private final MockHttpServletRequest request; + + private HtmlUnitMockHttpSession(MockHttpServletRequest request) { + super(request.getServletContext()); + this.request = request; + } + + private HtmlUnitMockHttpSession(MockHttpServletRequest request, String id) { + super(request.getServletContext(), id); + this.request = request; + } + + public void invalidate() { + super.invalidate(); + synchronized (HtmlUnitRequestBuilder.this.sessions) { + HtmlUnitRequestBuilder.this.sessions.remove(getId()); + } + removeSessionCookie(request, getId()); + } + } + + private CookieManager getCookieManager() { + return this.webClient.getCookieManager(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java new file mode 100644 index 00000000000..9764cea5c67 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; + +import com.gargoylesoftware.htmlunit.WebClient; + +/** + * {@code MockMvcWebClientBuilder} simplifies the creation of a {@link WebClient} + * that delegates to a {@link MockMvc} instance. + * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + */ +public class MockMvcWebClientBuilder extends MockMvcWebConnectionBuilderSupport { + + protected MockMvcWebClientBuilder(MockMvc mockMvc) { + super(mockMvc); + } + + protected MockMvcWebClientBuilder(WebApplicationContext context) { + super(context); + } + + protected MockMvcWebClientBuilder(WebApplicationContext context, MockMvcConfigurer configurer) { + super(context, configurer); + } + + /** + * Create a new instance with the supplied {@link WebApplicationContext}. + * @param context the {@code WebApplicationContext} to create a {@link MockMvc} + * instance from; never {@code null} + * @return the MockMvcWebClientBuilder to customize + */ + public static MockMvcWebClientBuilder webAppContextSetup(WebApplicationContext context) { + Assert.notNull(context, "WebApplicationContext must not be null"); + return new MockMvcWebClientBuilder(context); + } + + /** + * Create a new instance with the supplied {@link WebApplicationContext} + * and {@link MockMvcConfigurer}. + * @param context the {@code WebApplicationContext} to create a {@link MockMvc} + * instance from; never {@code null} + * @param configurer the MockMvcConfigurer to apply; never {@code null} + * @return the MockMvcWebClientBuilder to customize + */ + public static MockMvcWebClientBuilder webAppContextSetup(WebApplicationContext context, MockMvcConfigurer configurer) { + Assert.notNull(context, "WebApplicationContext must not be null"); + Assert.notNull(configurer, "MockMvcConfigurer must not be null"); + return new MockMvcWebClientBuilder(context, configurer); + } + + /** + * Create a new instance with the supplied {@link MockMvc} instance. + * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + * @return the MockMvcWebClientBuilder to customize + */ + public static MockMvcWebClientBuilder mockMvcSetup(MockMvc mockMvc) { + Assert.notNull(mockMvc, "MockMvc must not be null"); + return new MockMvcWebClientBuilder(mockMvc); + } + + /** + * Create a {@link WebClient} that uses the configured {@link MockMvc} + * instance for any matching requests and a {@code WebClient} with all + * the default settings for any other requests. + * @return the {@code WebClient} to use + * @see #configureWebClient(WebClient) + */ + public WebClient createWebClient() { + return configureWebClient(new WebClient()); + } + + /** + * Configure the supplied {@link WebClient} to use the configured + * {@link MockMvc} instance for any matching requests and the supplied + * {@code WebClient} for any other requests. + * @param webClient the WebClient to delegate to for requests that do not + * match; never {@code null} + * @return the WebClient to use + */ + public WebClient configureWebClient(WebClient webClient) { + Assert.notNull(webClient, "webClient must not be null"); + webClient.setWebConnection(createConnection(webClient.getWebConnection())); + return webClient; + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java new file mode 100644 index 00000000000..d277f4cef8e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.htmlunit.webdriver.WebConnectionHtmlUnitDriver; +import org.springframework.util.Assert; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebConnection; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; + +/** + * {@code MockMvcWebConnection} enables {@link MockMvc} to transform a + * {@link WebRequest} into a {@link WebResponse}. + *

This is the core integration with HtmlUnit. + *

Example usage can be seen below. + * + *

+ * WebClient webClient = new WebClient();
+ * MockMvc mockMvc = ...
+ * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc);
+ * mockConnection.setWebClient(webClient);
+ * webClient.setWebConnection(webConnection);
+ *
+ * // Use webClient as normal ...
+ * 
+ * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + * @see WebConnectionHtmlUnitDriver + */ +public final class MockMvcWebConnection implements WebConnection { + + private final Map sessions = new HashMap(); + + private final MockMvc mockMvc; + + private final String contextPath; + + private WebClient webClient; + + + /** + * Create a new instance that assumes the context path of the application + * is {@code ""} (i.e., the root context). + *

For example, the URL {@code http://localhost/test/this} would use + * {@code ""} as the context path. + * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + */ + public MockMvcWebConnection(MockMvc mockMvc) { + this(mockMvc, ""); + } + + /** + * Create a new instance with the specified context path. + *

The path may be {@code null} in which case the first path segment + * of the URL is turned into the contextPath. Otherwise it must conform + * to {@link javax.servlet.http.HttpServletRequest#getContextPath()} + * which states that it can be an empty string and otherwise must start + * with a "/" character and not end with a "/" character. + * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + * @param contextPath the contextPath to use + */ + public MockMvcWebConnection(MockMvc mockMvc, String contextPath) { + Assert.notNull(mockMvc, "mockMvc must not be null"); + validateContextPath(contextPath); + + this.webClient = new WebClient(); + this.mockMvc = mockMvc; + this.contextPath = contextPath; + } + + public WebResponse getResponse(WebRequest webRequest) throws IOException { + long startTime = System.currentTimeMillis(); + HtmlUnitRequestBuilder requestBuilder = new HtmlUnitRequestBuilder(this.sessions, this.webClient, webRequest); + requestBuilder.setContextPath(this.contextPath); + + MockHttpServletResponse httpServletResponse = getResponse(requestBuilder); + + String forwardedUrl = httpServletResponse.getForwardedUrl(); + while (forwardedUrl != null) { + requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl)); + httpServletResponse = getResponse(requestBuilder); + forwardedUrl = httpServletResponse.getForwardedUrl(); + } + + return new MockWebResponseBuilder(startTime, webRequest, httpServletResponse).build(); + } + + public void setWebClient(WebClient webClient) { + Assert.notNull(webClient, "webClient must not be null"); + this.webClient = webClient; + } + + private MockHttpServletResponse getResponse(RequestBuilder requestBuilder) throws IOException { + ResultActions resultActions; + try { + resultActions = this.mockMvc.perform(requestBuilder); + } + catch (Exception e) { + throw (IOException) new IOException(e.getMessage()).initCause(e); + } + + return resultActions.andReturn().getResponse(); + } + + /** + * Validate the supplied {@code contextPath}. + *

If the value is not {@code null}, it must conform to + * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which + * states that it can be an empty string and otherwise must start with + * a "/" character and not end with a "/" character. + * @param contextPath the path to validate + */ + static void validateContextPath(String contextPath) { + if (contextPath == null || "".equals(contextPath)) { + return; + } + if (!contextPath.startsWith("/")) { + throw new IllegalArgumentException("contextPath '" + contextPath + "' must start with '/'."); + } + if (contextPath.endsWith("/")) { + throw new IllegalArgumentException("contextPath '" + contextPath + "' must not end with '/'."); + } + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java new file mode 100644 index 00000000000..10bedebaf4f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; + +import com.gargoylesoftware.htmlunit.WebConnection; + +/** + * Support class that simplifies the creation of a {@link WebConnection} that + * uses {@link MockMvc} and optionally delegates to a real {@link WebConnection} + * for specific requests. + * + *

The default is to use {@link MockMvc} for requests to {@code localhost} + * and otherwise use a real {@link WebConnection}. + * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + */ +public abstract class MockMvcWebConnectionBuilderSupport> { + + private final MockMvc mockMvc; + + private final List mockMvcRequestMatchers = new ArrayList(); + + private String contextPath = ""; + + private boolean alwaysUseMockMvc; + + + /** + * Create a new instance using the supplied {@link MockMvc} instance. + * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + */ + protected MockMvcWebConnectionBuilderSupport(MockMvc mockMvc) { + Assert.notNull(mockMvc, "mockMvc must not be null"); + this.mockMvc = mockMvc; + this.mockMvcRequestMatchers.add(new HostRequestMatcher("localhost")); + } + + /** + * Create a new instance using the supplied {@link WebApplicationContext}. + * @param context the {@code WebApplicationContext} to create a {@code MockMvc} + * instance from; never {@code null} + */ + protected MockMvcWebConnectionBuilderSupport(WebApplicationContext context) { + this(MockMvcBuilders.webAppContextSetup(context).build()); + } + + /** + * Create a new instance using the supplied {@link WebApplicationContext} + * and {@link MockMvcConfigurer}. + * @param context the {@code WebApplicationContext} to create a {@code MockMvc} + * instance from; never {@code null} + * @param configurer the MockMvcConfigurer to apply; never {@code null} + */ + protected MockMvcWebConnectionBuilderSupport(WebApplicationContext context, MockMvcConfigurer configurer) { + this(MockMvcBuilders.webAppContextSetup(context).apply(configurer).build()); + } + + /** + * Set the context path to use. + *

If the supplied value is {@code null} or empty, the first path + * segment of the request URL is assumed to be the context path. + *

Default is {@code ""}. + * @param contextPath the context path to use + * @return this builder for further customization + */ + @SuppressWarnings("unchecked") + public T contextPath(String contextPath) { + this.contextPath = contextPath; + return (T) this; + } + + /** + * Specify that {@link MockMvc} should always be used regardless of + * what the request looks like. + * @return this builder for further customization + */ + @SuppressWarnings("unchecked") + public T alwaysUseMockMvc() { + this.alwaysUseMockMvc = true; + return (T) this; + } + + /** + * Add additional {@link WebRequestMatcher} instances that will ensure + * that {@link MockMvc} is used to process the request, if such a matcher + * matches against the web request. + * @param matchers additional {@code WebRequestMatcher} instances + * @return this builder for further customization + */ + @SuppressWarnings("unchecked") + public T useMockMvc(WebRequestMatcher... matchers) { + for (WebRequestMatcher matcher : matchers) { + this.mockMvcRequestMatchers.add(matcher); + } + return (T) this; + } + + /** + * Add additional {@link WebRequestMatcher} instances that return {@code true} + * if a supplied host matches — for example, {@code "example.com"} or + * {@code "example.com:8080"}. + * @param hosts additional hosts that ensure {@code MockMvc} gets invoked + * @return this builder for further customization + */ + @SuppressWarnings("unchecked") + public T useMockMvcForHosts(String... hosts) { + this.mockMvcRequestMatchers.add(new HostRequestMatcher(hosts)); + return (T) this; + } + + /** + * Create a new {@link WebConnection} that will use a {@link MockMvc} + * instance if one of the specified {@link WebRequestMatcher} instances + * matches. + * @param defaultConnection the default WebConnection to use if none of + * the specified {@code WebRequestMatcher} instances matches; never {@code null} + * @return a new {@code WebConnection} that will use a {@code MockMvc} + * instance if one of the specified {@code WebRequestMatcher} matches + * @see #alwaysUseMockMvc() + * @see #useMockMvc(WebRequestMatcher...) + * @see #useMockMvcForHosts(String...) + */ + protected final WebConnection createConnection(WebConnection defaultConnection) { + Assert.notNull(defaultConnection, "defaultConnection must not be null"); + MockMvcWebConnection mockMvcWebConnection = new MockMvcWebConnection(this.mockMvc, this.contextPath); + + if (this.alwaysUseMockMvc) { + return mockMvcWebConnection; + } + + List delegates = new ArrayList( + this.mockMvcRequestMatchers.size()); + for (WebRequestMatcher matcher : this.mockMvcRequestMatchers) { + delegates.add(new DelegatingWebConnection.DelegateWebConnection(matcher, mockMvcWebConnection)); + } + + return new DelegatingWebConnection(defaultConnection, delegates); + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java new file mode 100644 index 00000000000..7cf99f13291 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.WebResponseData; +import com.gargoylesoftware.htmlunit.util.NameValuePair; + +/** + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + */ +final class MockWebResponseBuilder { + + private static final String DEFAULT_STATUS_MESSAGE = "N/A"; + + private final long startTime; + + private final WebRequest webRequest; + + private final MockHttpServletResponse response; + + + public MockWebResponseBuilder(long startTime, WebRequest webRequest, MockHttpServletResponse response) { + Assert.notNull(webRequest, "webRequest must not be null"); + Assert.notNull(response, "response must not be null"); + this.startTime = startTime; + this.webRequest = webRequest; + this.response = response; + } + + public WebResponse build() throws IOException { + WebResponseData webResponseData = webResponseData(); + long endTime = System.currentTimeMillis(); + return new WebResponse(webResponseData, webRequest, endTime - startTime); + } + + private WebResponseData webResponseData() throws IOException { + List responseHeaders = responseHeaders(); + int statusCode = (this.response.getRedirectedUrl() != null ? HttpStatus.MOVED_PERMANENTLY.value() + : this.response.getStatus()); + String statusMessage = statusMessage(statusCode); + return new WebResponseData(this.response.getContentAsByteArray(), statusCode, statusMessage, responseHeaders); + } + + private String statusMessage(int statusCode) { + String errorMessage = this.response.getErrorMessage(); + if (StringUtils.hasText(errorMessage)) { + return errorMessage; + } + + try { + return HttpStatus.valueOf(statusCode).getReasonPhrase(); + } + catch (IllegalArgumentException ex) { + // ignore + } + + return DEFAULT_STATUS_MESSAGE; + } + + private List responseHeaders() { + Collection headerNames = this.response.getHeaderNames(); + List responseHeaders = new ArrayList(headerNames.size()); + for (String headerName : headerNames) { + List headerValues = this.response.getHeaderValues(headerName); + for (Object value : headerValues) { + responseHeaders.add(new NameValuePair(headerName, String.valueOf(value))); + } + } + String location = this.response.getRedirectedUrl(); + if (location != null) { + responseHeaders.add(new NameValuePair("Location", location)); + } + return responseHeaders; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java new file mode 100644 index 00000000000..13d6b6e5463 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcher.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.util.regex.Pattern; + +import com.gargoylesoftware.htmlunit.WebRequest; + +/** + * A {@link WebRequestMatcher} that allows matching on + * {@code WebRequest#getUrl().toExternalForm()} using a regular expression. + * + *

For example, if you would like to match on the domain {@code code.jquery.com}, + * you might want to use the following. + * + *

WebRequestMatcher cdnMatcher = new UrlRegexRequestMatcher(".*?//code.jquery.com/.*");
+ * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + * @see org.springframework.test.web.servlet.htmlunit.DelegatingWebConnection + */ +public final class UrlRegexRequestMatcher implements WebRequestMatcher { + + private final Pattern pattern; + + public UrlRegexRequestMatcher(String regex) { + this.pattern = Pattern.compile(regex); + } + + public UrlRegexRequestMatcher(Pattern pattern) { + this.pattern = pattern; + } + + @Override + public boolean matches(WebRequest request) { + String url = request.getUrl().toExternalForm(); + return this.pattern.matcher(url).matches(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java new file mode 100644 index 00000000000..f4f3a328882 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/WebRequestMatcher.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import com.gargoylesoftware.htmlunit.WebRequest; + +/** + * Strategy for matching on a {@link WebRequest}. + * + * @author Rob Winch + * @since 4.2 + */ +public interface WebRequestMatcher { + + /** + * Whether this matcher matches on the supplied web request. + * @param request the {@link WebRequest} to attempt to match on + * @return {@code true} if this matcher matches on the {@code WebRequest} + */ + boolean matches(WebRequest request); + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java new file mode 100644 index 00000000000..e94ee986675 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java @@ -0,0 +1,6 @@ +/** + * Server-side support for testing Spring MVC applications with {@code MockMvc} + * and HtmlUnit. + * @see org.springframework.test.web.servlet.MockMvc + */ +package org.springframework.test.web.servlet.htmlunit; \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java new file mode 100644 index 00000000000..711ec484eaa --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit.webdriver; + +import org.openqa.selenium.htmlunit.HtmlUnitDriver; + +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.htmlunit.MockMvcWebConnectionBuilderSupport; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; + +import com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.WebClient; + +/** + * Convenience class for building an {@link HtmlUnitDriver} that delegates + * to {@link MockMvc} and optionally delegates to an actual connection for + * specific requests. + * + *

By default, the driver will delegate to {@code MockMvc} to handle + * requests to {@code localhost} and to a {@link WebClient} to handle any + * other URL (i.e. to perform an actual HTTP request). + * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + */ +public class MockMvcHtmlUnitDriverBuilder extends MockMvcWebConnectionBuilderSupport { + + private boolean javascriptEnabled = true; + + + protected MockMvcHtmlUnitDriverBuilder(MockMvc mockMvc) { + super(mockMvc); + } + + protected MockMvcHtmlUnitDriverBuilder(WebApplicationContext context) { + super(context); + } + + protected MockMvcHtmlUnitDriverBuilder(WebApplicationContext context, MockMvcConfigurer configurer) { + super(context, configurer); + } + + /** + * Create a new instance using the supplied {@link WebApplicationContext}. + * @param context the WebApplicationContext to use; never {@code null} + * @return the MockMvcHtmlUnitDriverBuilder to customize + */ + public static MockMvcHtmlUnitDriverBuilder webAppContextSetup(WebApplicationContext context) { + return new MockMvcHtmlUnitDriverBuilder(context); + } + + /** + * Create a new instance using the supplied {@link WebApplicationContext} + * and {@link MockMvcConfigurer}. + * @param context the WebApplicationContext to create a MockMvc instance from; + * never {@code null} + * @param configurer the MockMvcConfigurer to apply; never {@code null} + * @return the MockMvcHtmlUnitDriverBuilder to customize + */ + public static MockMvcHtmlUnitDriverBuilder webAppContextSetup(WebApplicationContext context, + MockMvcConfigurer configurer) { + return new MockMvcHtmlUnitDriverBuilder(context, configurer); + } + + /** + * Create a new instance using the supplied {@link MockMvc} instance. + * @param mockMvc the MockMvc instance to use; never {@code null} + * @return the MockMvcHtmlUnitDriverBuilder to customize + */ + public static MockMvcHtmlUnitDriverBuilder mockMvcSetup(MockMvc mockMvc) { + return new MockMvcHtmlUnitDriverBuilder(mockMvc); + } + + /** + * Specify whether JavaScript should be enabled. + *

Default is {@code true}. + * @param javascriptEnabled if JavaScript should be enabled or not. + * @return the builder for further customizations + */ + public MockMvcHtmlUnitDriverBuilder javascriptEnabled(boolean javascriptEnabled) { + this.javascriptEnabled = javascriptEnabled; + return this; + } + + /** + * Create a new {@link HtmlUnitDriver} with the {@link BrowserVersion} + * set to {@link BrowserVersion#CHROME CHROME}. + *

For additional configuration options, use {@link #configureDriver}. + * @return the {@code HtmlUnitDriver} to use + * @see #configureDriver(WebConnectionHtmlUnitDriver) + */ + public HtmlUnitDriver createDriver() { + return configureDriver(new WebConnectionHtmlUnitDriver(BrowserVersion.CHROME)); + } + + /** + * Configure an existing {@link WebConnectionHtmlUnitDriver}. + * @param driver the WebConnectionHtmlUnitDriver to configure + * @return the {@code HtmlUnitDriver} to use + */ + public HtmlUnitDriver configureDriver(WebConnectionHtmlUnitDriver driver) { + driver.setJavascriptEnabled(javascriptEnabled); + driver.setWebConnection(createConnection(driver.getWebConnection())); + return driver; + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java new file mode 100644 index 00000000000..bdfc41dd9b8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit.webdriver; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; + +import org.springframework.test.web.servlet.htmlunit.MockMvcWebConnection; +import org.springframework.util.Assert; + +import com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebConnection; + +/** + * {@code WebConnectionHtmlUnitDriver} enables configuration of the + * {@link WebConnection} for an {@link HtmlUnitDriver} instance. + * + *

This is useful because it allows a {@link MockMvcWebConnection} to + * be injected. + * + * @author Rob Winch + * @author Sam Brannen + * @since 4.2 + * @see MockMvcHtmlUnitDriverBuilder + */ +public class WebConnectionHtmlUnitDriver extends HtmlUnitDriver { + + private WebClient webClient; + + + public WebConnectionHtmlUnitDriver(BrowserVersion browserVersion) { + super(browserVersion); + } + + public WebConnectionHtmlUnitDriver() { + } + + public WebConnectionHtmlUnitDriver(boolean enableJavascript) { + super(enableJavascript); + } + + public WebConnectionHtmlUnitDriver(Capabilities capabilities) { + super(capabilities); + } + + /** + * Modify the supplied {@link WebClient}, {@linkplain #configureWebClient + * configure} it, and retain a reference to it so that its {@link WebConnection} + * is {@linkplain #getWebConnection accessible} for later use. + * @param client the client to modify + * @return the modified client + * @see org.openqa.selenium.htmlunit.HtmlUnitDriver#modifyWebClient(WebClient) + */ + @Override + protected final WebClient modifyWebClient(WebClient webClient) { + this.webClient = super.modifyWebClient(webClient); + this.webClient = configureWebClient(this.webClient); + return this.webClient; + } + + /** + * Configure the supplied {@link WebClient}. + *

The default implementation simply returns the supplied client + * unmodified. + *

Subclasses can override this method to customize the {@code WebClient} + * that the {@link HtmlUnitDriver} driver uses. + * @param client the client to configure + * @return the configured client + */ + protected WebClient configureWebClient(WebClient client) { + return client; + } + + /** + * Access the current {@link WebConnection} for the {@link WebClient}. + * @return the current {@code WebConnection} + */ + public WebConnection getWebConnection() { + return this.webClient.getWebConnection(); + } + + /** + * Set the {@link WebConnection} to be used with the {@link WebClient}. + * @param webConnection the {@code WebConnection} to use; never {@code null} + */ + public void setWebConnection(WebConnection webConnection) { + Assert.notNull(webConnection, "WebConnection must not be null"); + this.webClient.setWebConnection(webConnection); + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java new file mode 100644 index 00000000000..afc0f6881df --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java @@ -0,0 +1,7 @@ +/** + * Server-side support for testing Spring MVC applications with {@code MockMvc} + * and the Selenium {@code HtmlUnitDriver}. + * @see org.springframework.test.web.servlet.MockMvc + * @see org.openqa.selenium.htmlunit.HtmlUnitDriver + */ +package org.springframework.test.web.servlet.htmlunit.webdriver; \ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java new file mode 100644 index 00000000000..eed3271f979 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnectionTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.net.URL; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import org.springframework.stereotype.Controller; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.htmlunit.DelegatingWebConnection.DelegateWebConnection; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.gargoylesoftware.htmlunit.HttpWebConnection; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebConnection; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.WebResponseData; +import com.gargoylesoftware.htmlunit.util.NameValuePair; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; + +/** + * @author Rob Winch + * @since 4.2 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingWebConnectionTests { + + @Mock + private WebRequestMatcher matcher1; + @Mock + private WebRequestMatcher matcher2; + @Mock + private WebConnection defaultConnection; + @Mock + private WebConnection connection1; + @Mock + private WebConnection connection2; + + private DelegatingWebConnection webConnection; + private WebRequest request; + private WebResponse expectedResponse; + + @Before + public void setUp() throws Exception { + request = new WebRequest(new URL("http://localhost/")); + WebResponseData data = new WebResponseData("".getBytes("UTF-8"),200, "", Collections.emptyList()); + expectedResponse = new WebResponse(data, request, 100L); + webConnection = new DelegatingWebConnection(defaultConnection, new DelegateWebConnection(matcher1,connection1), new DelegateWebConnection(matcher2,connection2)); + } + + @Test + public void getResponseDefault() throws Exception { + when(defaultConnection.getResponse(request)).thenReturn(expectedResponse); + + WebResponse response = webConnection.getResponse(request); + + assertThat(response, sameInstance(expectedResponse)); + verify(matcher1).matches(request); + verify(matcher2).matches(request); + verifyNoMoreInteractions(connection1,connection2); + verify(defaultConnection).getResponse(request); + } + + @Test + public void getResponseAllMatches() throws Exception { + when(matcher1.matches(request)).thenReturn(true); + when(matcher2.matches(request)).thenReturn(true); + when(connection1.getResponse(request)).thenReturn(expectedResponse); + + WebResponse response = webConnection.getResponse(request); + + assertThat(response, sameInstance(expectedResponse)); + verify(matcher1).matches(request); + verifyNoMoreInteractions(matcher2,connection2,defaultConnection); + verify(connection1).getResponse(request); + } + + @Test + public void getResponseSecondMatches() throws Exception { + when(matcher2.matches(request)).thenReturn(true); + when(connection2.getResponse(request)).thenReturn(expectedResponse); + + WebResponse response = webConnection.getResponse(request); + + assertThat(response, sameInstance(expectedResponse)); + verify(matcher1).matches(request); + verify(matcher2).matches(request); + verifyNoMoreInteractions(connection1,defaultConnection); + verify(connection2).getResponse(request); + } + + @Test + public void classlevelJavadoc() throws Exception { + WebClient webClient = new WebClient(); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(TestController.class).build(); + MockMvcWebConnection mockConnection = new MockMvcWebConnection(mockMvc); + mockConnection.setWebClient(webClient); + + WebRequestMatcher cdnMatcher = new UrlRegexRequestMatcher(".*?//code.jquery.com/.*"); + WebConnection httpConnection = new HttpWebConnection(webClient); + WebConnection webConnection = new DelegatingWebConnection(mockConnection, new DelegateWebConnection(cdnMatcher, httpConnection)); + + webClient.setWebConnection(webConnection); + + Page page = webClient.getPage("http://code.jquery.com/jquery-1.11.0.min.js"); + assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); + assertThat(page.getWebResponse().getContentAsString(), not(isEmptyString())); + } + + @Controller + static class TestController {} + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java new file mode 100644 index 00000000000..367b7177385 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/ForwardController.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Rob Winch + * @since 4.2 + */ +@Controller +public class ForwardController { + + @RequestMapping("/forward") + public String forward() { + return "forward:/"; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HelloController.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HelloController.java new file mode 100644 index 00000000000..80b64c75131 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HelloController.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Rob Winch + * @since 4.2 + */ +@Controller +public class HelloController { + + @RequestMapping + @ResponseBody + public String header(HttpServletRequest request) { + return "hello"; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcherTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcherTests.java new file mode 100644 index 00000000000..5d69bd953c1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HostRequestMatcherTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.net.URL; + +import org.junit.Test; + +import com.gargoylesoftware.htmlunit.WebRequest; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class HostRequestMatcherTests { + + @Test + public void localhostMatches() throws Exception { + WebRequestMatcher matcher = new HostRequestMatcher("localhost"); + + boolean matches = matcher.matches(new WebRequest(new URL("http://localhost/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true));; + + matches = matcher.matches(new WebRequest(new URL("http://example.com/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(false));; + } + + @Test + public void multipleHosts() throws Exception { + WebRequestMatcher matcher = new HostRequestMatcher("localhost","example.com"); + + boolean matches = matcher.matches(new WebRequest(new URL("http://localhost/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true));; + + matches = matcher.matches(new WebRequest(new URL("http://example.com/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true));; + } + + @Test + public void specificPort() throws Exception { + WebRequestMatcher matcher = new HostRequestMatcher("localhost:8080"); + + boolean matches = matcher.matches(new WebRequest(new URL("http://localhost:8080/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true));; + + matches = matcher.matches(new WebRequest(new URL("http://localhost:9090/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(false));; + } + + @Test + public void defaultPortInMatcher() throws Exception { + WebRequestMatcher matcher = new HostRequestMatcher("localhost:80"); + + boolean matches = matcher.matches(new WebRequest(new URL("http://localhost:80/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true));; + + matches = matcher.matches(new WebRequest(new URL("http://localhost/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true));; + + matches = matcher.matches(new WebRequest(new URL("http://localhost:9090/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(false));; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java new file mode 100644 index 00000000000..a7810b65610 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilderTests.java @@ -0,0 +1,785 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; + +import org.apache.commons.io.IOUtils; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.util.NameValuePair; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class HtmlUnitRequestBuilderTests { + + private WebRequest webRequest; + + private ServletContext servletContext; + + private Map sessions; + + private WebClient webClient; + + private HtmlUnitRequestBuilder requestBuilder; + + + @Before + public void setUp() throws Exception { + sessions = new HashMap<>(); + webRequest = new WebRequest(new URL("http://example.com:80/test/this/here")); + webRequest.setHttpMethod(HttpMethod.GET); + webRequest.setRequestParameters(new ArrayList<>()); + webClient = new WebClient(); + requestBuilder = new HtmlUnitRequestBuilder(sessions, webClient, webRequest); + servletContext = new MockServletContext(); + } + + // --- constructor + + @Test(expected = IllegalArgumentException.class) + public void constructorNullSessions() { + new HtmlUnitRequestBuilder(null, webClient, webRequest); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullWebClient() { + new HtmlUnitRequestBuilder(sessions, null, webRequest); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullWebRequest() { + new HtmlUnitRequestBuilder(sessions, webClient, null); + } + + // --- buildRequest + + @Test + public void buildRequestBasicAuth() { + String base64Credentials = "dXNlcm5hbWU6cGFzc3dvcmQ="; + String authzHeaderValue = "Basic: " + base64Credentials; + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(base64Credentials); + webRequest.setCredentials(credentials); + webRequest.setAdditionalHeader("Authorization", authzHeaderValue); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getAuthType(), equalTo("Basic")); + assertThat(actualRequest.getHeader("Authorization"), equalTo(authzHeaderValue)); + } + + @Test + public void buildRequestCharacterEncoding() { + String charset = "UTF-8"; + webRequest.setCharset(charset); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getCharacterEncoding(), equalTo(charset)); + } + + @Test + public void buildRequestDefaultCharacterEncoding() { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getCharacterEncoding(), equalTo("ISO-8859-1")); + } + + @Test + public void buildRequestContentLength() { + String content = "some content that has length"; + webRequest.setHttpMethod(HttpMethod.POST); + webRequest.setRequestBody(content); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getContentLength(), equalTo(content.length())); + } + + @Test + public void buildRequestContentType() { + String contentType = "text/html;charset=UTF-8"; + webRequest.setAdditionalHeader("Content-Type", contentType); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getContentType(), equalTo(contentType)); + assertThat(actualRequest.getHeader("Content-Type"), equalTo(contentType)); + } + + @Test + public void buildRequestContextPathUsesFirstSegmentByDefault() { + String contextPath = requestBuilder.buildRequest(servletContext).getContextPath(); + + assertThat(contextPath, equalTo("/test")); + } + + @Test + public void buildRequestContextPathUsesNoFirstSegmentWithDefault() throws MalformedURLException { + webRequest.setUrl(new URL("http://example.com/")); + String contextPath = requestBuilder.buildRequest(servletContext).getContextPath(); + + assertThat(contextPath, equalTo("")); + } + + @Test(expected = IllegalArgumentException.class) + public void buildRequestContextPathInvalid() { + requestBuilder.setContextPath("/invalid"); + + requestBuilder.buildRequest(servletContext).getContextPath(); + } + + @Test + public void buildRequestContextPathEmpty() { + String expected = ""; + requestBuilder.setContextPath(expected); + + String contextPath = requestBuilder.buildRequest(servletContext).getContextPath(); + + assertThat(contextPath, equalTo(expected)); + } + + @Test + public void buildRequestContextPathExplicit() { + String expected = "/test"; + requestBuilder.setContextPath(expected); + + String contextPath = requestBuilder.buildRequest(servletContext).getContextPath(); + + assertThat(contextPath, equalTo(expected)); + } + + @Test + public void buildRequestContextPathMulti() { + String expected = "/test/this"; + requestBuilder.setContextPath(expected); + + String contextPath = requestBuilder.buildRequest(servletContext).getContextPath(); + + assertThat(contextPath, equalTo(expected)); + } + + @Test + public void buildRequestCookiesNull() { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getCookies(), nullValue()); + } + + @Test + public void buildRequestCookiesSingle() { + webRequest.setAdditionalHeader("Cookie", "name=value"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + Cookie[] cookies = actualRequest.getCookies(); + assertThat(cookies.length, equalTo(1)); + assertThat(cookies[0].getName(), equalTo("name")); + assertThat(cookies[0].getValue(), equalTo("value")); + } + + @Test + public void buildRequestCookiesMulti() { + webRequest.setAdditionalHeader("Cookie", "name=value; name2=value2"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + Cookie[] cookies = actualRequest.getCookies(); + assertThat(cookies.length, equalTo(2)); + Cookie cookie = cookies[0]; + assertThat(cookie.getName(), equalTo("name")); + assertThat(cookie.getValue(), equalTo("value")); + cookie = cookies[1]; + assertThat(cookie.getName(), equalTo("name2")); + assertThat(cookie.getValue(), equalTo("value2")); + } + + @Test + public void buildRequestInputStream() throws Exception { + String content = "some content that has length"; + webRequest.setHttpMethod(HttpMethod.POST); + webRequest.setRequestBody(content); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(IOUtils.toString(actualRequest.getInputStream()), equalTo(content)); + } + + @Test + public void buildRequestLocalAddr() { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocalAddr(), equalTo("127.0.0.1")); + } + + @Test + public void buildRequestLocaleDefault() { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocale(), equalTo(Locale.getDefault())); + } + + @Test + public void buildRequestLocaleDa() { + webRequest.setAdditionalHeader("Accept-Language", "da"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocale(), equalTo(new Locale("da"))); + } + + @Test + public void buildRequestLocaleEnGbQ08() { + webRequest.setAdditionalHeader("Accept-Language", "en-gb;q=0.8"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocale(), equalTo(new Locale("en", "gb", "0.8"))); + } + + @Test + public void buildRequestLocaleEnQ07() { + webRequest.setAdditionalHeader("Accept-Language", "en;q=0.7"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocale(), equalTo(new Locale("en", "", "0.7"))); + } + + @Test + public void buildRequestLocaleEnUs() { + webRequest.setAdditionalHeader("Accept-Language", "en-US"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocale(), equalTo(Locale.US)); + } + + @Test + public void buildRequestLocaleFr() { + webRequest.setAdditionalHeader("Accept-Language", "fr"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocale(), equalTo(Locale.FRENCH)); + } + + @Test + public void buildRequestLocaleMulti() { + webRequest.setAdditionalHeader("Accept-Language", "da, en-gb;q=0.8, en;q=0.7"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + // FIXME Locale.ENGLISH is due to fact cannot remove it from MockHttpServletRequest + List expected = Arrays.asList(new Locale("da"), new Locale("en", "gb", "0.8"), new Locale("en", "", + "0.7"), Locale.ENGLISH); + assertThat(Collections.list(actualRequest.getLocales()), equalTo(expected)); + } + + @Test + public void buildRequestLocaleName() { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocalName(), equalTo("localhost")); + } + + @Test + public void buildRequestLocalPort() { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocalPort(), equalTo(80)); + } + + @Test + public void buildRequestLocalMissing() throws Exception { + webRequest.setUrl(new URL("http://localhost/test/this")); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getLocalPort(), equalTo(-1)); + } + + @Test + public void buildRequestMethods() { + HttpMethod[] methods = HttpMethod.values(); + + for (HttpMethod expectedMethod : methods) { + webRequest.setHttpMethod(expectedMethod); + String actualMethod = requestBuilder.buildRequest(servletContext).getMethod(); + assertThat(actualMethod, equalTo(actualMethod)); + } + } + + @Test + public void buildRequestParameterMap() throws Exception { + setParameter("name", "value"); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParameterMap().size(), equalTo(1)); + assertThat(actualRequest.getParameter("name"), equalTo("value")); + } + + @Test + public void buildRequestParameterMapQuery() throws Exception { + webRequest.setUrl(new URL("http://example.com/example/?name=value")); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParameterMap().size(), equalTo(1)); + assertThat(actualRequest.getParameter("name"), equalTo("value")); + } + + @Test + public void buildRequestParameterMapQueryMulti() throws Exception { + webRequest.setUrl(new URL("http://example.com/example/?name=value¶m2=value+2")); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getParameterMap().size(), equalTo(2)); + assertThat(actualRequest.getParameter("name"), equalTo("value")); + assertThat(actualRequest.getParameter("param2"), equalTo("value 2")); + } + + @Test + public void buildRequestPathInfo() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getPathInfo(), nullValue()); + } + + @Test + public void buildRequestPathInfoNull() throws Exception { + webRequest.setUrl(new URL("http://example.com/example")); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getPathInfo(), nullValue()); + } + + @Test + public void buildRequestAndAntPathRequestMatcher() throws Exception { + webRequest.setUrl(new URL("http://example.com/app/login/authenticate")); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + // verify it is going to work with Spring Security's AntPathRequestMatcher + assertThat(actualRequest.getPathInfo(), nullValue()); + assertThat(actualRequest.getServletPath(), equalTo("/login/authenticate")); + } + + @Test + public void buildRequestProtocol() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getProtocol(), equalTo("HTTP/1.1")); + } + + @Test + public void buildRequestQuery() throws Exception { + String expectedQuery = "aparam=avalue"; + webRequest.setUrl(new URL("http://example.com/example?" + expectedQuery)); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getQueryString(), equalTo(expectedQuery)); + } + + @Test + public void buildRequestReader() throws Exception { + String expectedBody = "request body"; + webRequest.setHttpMethod(HttpMethod.POST); + webRequest.setRequestBody(expectedBody); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(IOUtils.toString(actualRequest.getReader()), equalTo(expectedBody)); + } + + @Test + public void buildRequestRemoteAddr() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRemoteAddr(), equalTo("127.0.0.1")); + } + + @Test + public void buildRequestRemoteHost() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRemoteAddr(), equalTo("127.0.0.1")); + } + + @Test + public void buildRequestRemotePort() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRemotePort(), equalTo(80)); + } + + @Test + public void buildRequestRemotePort8080() throws Exception { + webRequest.setUrl(new URL("http://example.com:8080/")); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRemotePort(), equalTo(8080)); + } + + @Test + public void buildRequestRemotePort80WithDefault() throws Exception { + webRequest.setUrl(new URL("http://example.com/")); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRemotePort(), equalTo(80)); + } + + @Test + public void buildRequestRequestedSessionId() throws Exception { + String sessionId = "session-id"; + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + sessionId); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRequestedSessionId(), equalTo(sessionId)); + } + + @Test + public void buildRequestRequestedSessionIdNull() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getRequestedSessionId(), nullValue()); + } + + @Test + public void buildRequestUri() { + String uri = requestBuilder.buildRequest(servletContext).getRequestURI(); + assertThat(uri, equalTo("/test/this/here")); + } + + @Test + public void buildRequestUrl() { + String uri = requestBuilder.buildRequest(servletContext).getRequestURL().toString(); + assertThat(uri, equalTo("http://example.com/test/this/here")); + } + + @Test + public void buildRequestSchemeHttp() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getScheme(), equalTo("http")); + } + + @Test + public void buildRequestSchemeHttps() throws Exception { + webRequest.setUrl(new URL("https://example.com/")); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getScheme(), equalTo("https")); + } + + @Test + public void buildRequestServerName() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getServerName(), equalTo("example.com")); + } + + @Test + public void buildRequestServerPort() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getServerPort(), equalTo(80)); + } + + @Test + public void buildRequestServerPortDefault() throws Exception { + webRequest.setUrl(new URL("https://example.com/")); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getServerPort(), equalTo(-1)); + } + + @Test + public void buildRequestServletContext() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getServletContext(), equalTo(servletContext)); + } + + @Test + public void buildRequestServletPath() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getServletPath(), equalTo("/this/here")); + } + + @Test + public void buildRequestSession() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + HttpSession newSession = actualRequest.getSession(); + assertThat(newSession, notNullValue()); + assertSingleSessionCookie( + "JSESSIONID=" + newSession.getId() + "; Path=/test; Domain=example.com"); + + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + newSession.getId()); + + requestBuilder = new HtmlUnitRequestBuilder(sessions, webClient, webRequest); + actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getSession(), sameInstance(newSession)); + } + + @Test + public void buildRequestSessionWithExistingSession() throws Exception { + String sessionId = "session-id"; + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + sessionId); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + HttpSession session = actualRequest.getSession(); + assertThat(session.getId(), equalTo(sessionId)); + assertSingleSessionCookie("JSESSIONID=" + session.getId() + "; Path=/test; Domain=example.com"); + + requestBuilder = new HtmlUnitRequestBuilder(sessions, webClient, webRequest); + actualRequest = requestBuilder.buildRequest(servletContext); + assertThat(actualRequest.getSession(), equalTo(session)); + + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + sessionId + "NEW"); + actualRequest = requestBuilder.buildRequest(servletContext); + assertThat(actualRequest.getSession(), not(equalTo(session))); + assertSingleSessionCookie("JSESSIONID=" + actualRequest.getSession().getId() + + "; Path=/test; Domain=example.com"); + } + + @Test + public void buildRequestSessionTrue() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + HttpSession session = actualRequest.getSession(true); + assertThat(session, notNullValue()); + } + + @Test + public void buildRequestSessionFalseIsNull() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + HttpSession session = actualRequest.getSession(false); + assertThat(session, nullValue()); + } + + @Test + public void buildRequestSessionFalseWithExistingSession() throws Exception { + String sessionId = "session-id"; + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + sessionId); + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + HttpSession session = actualRequest.getSession(false); + assertThat(session, notNullValue()); + } + + @Test + public void buildRequestSessionIsNew() throws Exception { + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getSession().isNew(), equalTo(true)); + } + + @Test + public void buildRequestSessionIsNewFalse() throws Exception { + String sessionId = "session-id"; + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + sessionId); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getSession().isNew(), equalTo(false)); + } + + @Test + public void buildRequestSessionInvalidate() throws Exception { + String sessionId = "session-id"; + webRequest.setAdditionalHeader("Cookie", "JSESSIONID=" + sessionId); + + MockHttpServletRequest actualRequest = requestBuilder.buildRequest(servletContext); + HttpSession sessionToRemove = actualRequest.getSession(); + sessionToRemove.invalidate(); + + assertThat(sessions.containsKey(sessionToRemove.getId()), equalTo(false)); + assertSingleSessionCookie("JSESSIONID=" + sessionToRemove.getId() + + "; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Path=/test; Domain=example.com"); + + webRequest.removeAdditionalHeader("Cookie"); + requestBuilder = new HtmlUnitRequestBuilder(sessions, webClient, webRequest); + + actualRequest = requestBuilder.buildRequest(servletContext); + + assertThat(actualRequest.getSession().isNew(), equalTo(true)); + assertThat(sessions.containsKey(sessionToRemove.getId()), equalTo(false)); + } + + // --- setContextPath + + @Test + public void setContextPathNull() { + requestBuilder.setContextPath(null); + + assertThat(getContextPath(), nullValue()); + } + + @Test + public void setContextPathEmptyString() { + requestBuilder.setContextPath(""); + + assertThat(getContextPath(), isEmptyString()); + } + + @Test(expected = IllegalArgumentException.class) + public void setContextPathDoesNotStartWithSlash() { + requestBuilder.setContextPath("abc/def"); + } + + @Test(expected = IllegalArgumentException.class) + public void setContextPathEndsWithSlash() { + requestBuilder.setContextPath("/abc/def/"); + } + + @Test + public void setContextPath() { + String expectedContextPath = "/abc/def"; + requestBuilder.setContextPath(expectedContextPath); + + assertThat(getContextPath(), equalTo(expectedContextPath)); + } + + @Test + public void mergeHeader() throws Exception { + String headerName = "PARENT"; + String headerValue = "VALUE"; + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()) + .defaultRequest(get("/").header(headerName, headerValue)) + .build(); + + assertThat(mockMvc.perform(requestBuilder).andReturn().getRequest().getHeader(headerName), equalTo(headerValue)); + } + + @Test + public void mergeSession() throws Exception { + String attrName = "PARENT"; + String attrValue = "VALUE"; + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()) + .defaultRequest(get("/").sessionAttr(attrName, attrValue)) + .build(); + + assertThat(mockMvc.perform(requestBuilder).andReturn().getRequest().getSession().getAttribute(attrName), equalTo(attrValue)); + } + + @Test + public void mergeSessionNotInitialized() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()) + .defaultRequest(get("/")) + .build(); + + assertThat(mockMvc.perform(requestBuilder).andReturn().getRequest().getSession(false), nullValue()); + } + + @Test + public void mergeParameter() throws Exception { + String paramName = "PARENT"; + String paramValue = "VALUE"; + String paramValue2 = "VALUE2"; + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()) + .defaultRequest(get("/").param(paramName, paramValue, paramValue2)) + .build(); + + assertThat(Arrays.asList(mockMvc.perform(requestBuilder).andReturn().getRequest().getParameterValues(paramName)), contains(paramValue, paramValue2)); + } + + @Test + public void mergeCookie() throws Exception { + String cookieName = "PARENT"; + String cookieValue = "VALUE"; + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()) + .defaultRequest(get("/").cookie(new Cookie(cookieName,cookieValue))) + .build(); + + Cookie[] cookies = mockMvc.perform(requestBuilder).andReturn().getRequest().getCookies(); + assertThat(cookies, notNullValue()); + assertThat(cookies.length, equalTo(1)); + Cookie cookie = cookies[0]; + assertThat(cookie.getName(), equalTo(cookieName)); + assertThat(cookie.getValue(), equalTo(cookieValue)); + } + + @Test + public void mergeRequestAttribute() throws Exception { + String attrName = "PARENT"; + String attrValue = "VALUE"; + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()) + .defaultRequest(get("/").requestAttr(attrName,attrValue)) + .build(); + + assertThat(mockMvc.perform(requestBuilder).andReturn().getRequest().getAttribute(attrName), equalTo(attrValue)); + } + + + private void assertSingleSessionCookie(String expected) { + com.gargoylesoftware.htmlunit.util.Cookie jsessionidCookie = webClient.getCookieManager().getCookie("JSESSIONID"); + if (expected == null || expected.contains("Expires=Thu, 01-Jan-1970 00:00:01 GMT")) { + assertThat(jsessionidCookie, nullValue()); + return; + } + String actual = jsessionidCookie.getValue(); + assertThat("JSESSIONID=" + actual + + "; Path=/test; Domain=example.com", equalTo(expected)); + } + + private void setParameter(String name, String value) { + webRequest.getRequestParameters().add(new NameValuePair(name, value)); + } + + private String getContextPath() { + return (String) ReflectionTestUtils.getField(requestBuilder, "contextPath"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java new file mode 100644 index 00000000000..442c6072d13 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcConnectionBuilderSupportTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.IOException; +import java.net.URL; + +import javax.servlet.http.HttpServletRequest; + +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.Configuration; +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.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.gargoylesoftware.htmlunit.WebConnection; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Rob Winch + * @since 4.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +@SuppressWarnings("rawtypes") +public class MockMvcConnectionBuilderSupportTests { + + @Autowired + WebApplicationContext context; + + MockMvc mockMvc; + + WebConnection delegateConnection; + + WebConnection connection; + + @Before + public void setup() { + delegateConnection = mock(WebConnection.class); + mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + + connection = new MockMvcWebConnectionBuilderSupport(mockMvc){} + .createConnection(delegateConnection); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorMockMvcNull() { + new MockMvcWebConnectionBuilderSupport((MockMvc)null){}; + } + + @Test(expected = IllegalArgumentException.class) + public void constructorContextNull() { + new MockMvcWebConnectionBuilderSupport((WebApplicationContext)null){}; + } + + @Test + public void context() throws Exception { + connection = new MockMvcWebConnectionBuilderSupport(context){} + .createConnection(delegateConnection); + + assertMvcProcessed("http://localhost/"); + assertDelegateProcessed("http://example.com/"); + } + + @Test + public void mockMvc() throws Exception { + assertMvcProcessed("http://localhost/"); + assertDelegateProcessed("http://example.com/"); + } + + @Test + public void mockMvcExampleDotCom() throws Exception { + connection = new MockMvcWebConnectionBuilderSupport(context){} + .useMockMvcForHosts("example.com") + .createConnection(delegateConnection); + + assertMvcProcessed("http://localhost/"); + assertMvcProcessed("http://example.com/"); + assertDelegateProcessed("http://other.com/"); + } + + @Test + public void mockMvcAlwaysUseMockMvc() throws Exception { + connection = new MockMvcWebConnectionBuilderSupport(context){} + .alwaysUseMockMvc() + .createConnection(delegateConnection); + + assertMvcProcessed("http://other.com/"); + } + + @Test + public void defaultContextPathEmpty() throws Exception { + connection = new MockMvcWebConnectionBuilderSupport(context){} + .createConnection(delegateConnection); + + assertThat(getWebResponse("http://localhost/abc").getContentAsString(), equalTo(""));; + } + + @Test + public void defaultContextPathCustom() throws Exception { + connection = new MockMvcWebConnectionBuilderSupport(context) { + }.contextPath("/abc").createConnection(delegateConnection); + + assertThat(getWebResponse("http://localhost/abc/def").getContentAsString(), equalTo("/abc"));; + } + + private void assertMvcProcessed(String url) throws Exception { + assertThat(getWebResponse(url), notNullValue()); + } + + private void assertDelegateProcessed(String url) throws Exception { + assertThat(getWebResponse(url), nullValue()); + } + + private WebResponse getWebResponse(String url) throws IOException { + return connection.getResponse(new WebRequest(new URL(url))); + } + + @Configuration + @EnableWebMvc + static class Config { + @RestController + static class ContextPathController { + @RequestMapping + public String contextPath(HttpServletRequest request) { + return request.getContextPath(); + } + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java new file mode 100644 index 00000000000..71ebb43e3de --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.IOException; +import java.net.URL; +import javax.servlet.http.HttpServletRequest; + +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.Configuration; +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.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +/** + * @author Rob Winch + * @since 4.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class MockMvcWebClientBuilderTests { + + @Autowired + WebApplicationContext context; + + MockMvc mockMvc; + + WebClient webClient; + + @Before + public void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + webClient = new WebClient(); + } + + @Test(expected = IllegalArgumentException.class) + public void mockMvcSetupNull() { + MockMvcWebClientBuilder.mockMvcSetup(null); + } + + @Test(expected = IllegalArgumentException.class) + public void webAppContextSetupNull() { + MockMvcWebClientBuilder.webAppContextSetup(null); + } + + @Test + public void mockMvcSetupconfigureWebClient() throws Exception { + webClient = MockMvcWebClientBuilder + .mockMvcSetup(mockMvc) + .configureWebClient(webClient); + + assertMvcProcessed("http://localhost/test"); + assertDelegateProcessed("http://example.com/"); + } + + @Test + public void mockMvcSetupCreateWebClient() throws Exception { + webClient = MockMvcWebClientBuilder + .mockMvcSetup(mockMvc) + .createWebClient(); + + assertMvcProcessed("http://localhost/test"); + assertDelegateProcessed("http://example.com/"); + } + + private void assertMvcProcessed(String url) throws Exception { + assertThat(getWebResponse(url).getContentAsString(), equalTo("mvc"));; + } + + private void assertDelegateProcessed(String url) throws Exception { + assertThat(getWebResponse(url).getContentAsString(), not(equalTo("mvc"))); + } + + private WebResponse getWebResponse(String url) throws IOException { + return webClient.getWebConnection().getResponse(new WebRequest(new URL(url))); + } + + @Configuration + @EnableWebMvc + static class Config { + @RestController + static class ContextPathController { + @RequestMapping + public String contextPath(HttpServletRequest request) { + return "mvc"; + } + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java new file mode 100644 index 00000000000..c9b1386dae7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebClient; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class MockMvcWebConnectionTests { + + MockMvc mockMvc; + + WebClient webClient; + + @Before + public void setup() { + mockMvc = MockMvcBuilders + .standaloneSetup(new HelloController(), new ForwardController()) + .build(); + + webClient = new WebClient(); + } + + @Test + public void contextPathNull() throws IOException { + webClient.setWebConnection(new MockMvcWebConnection(mockMvc, null)); + + Page page = webClient.getPage("http://localhost/context/a"); + + assertThat(page.getWebResponse().getStatusCode(), equalTo(200));; + } + + @Test + public void contextPathExplicit() throws IOException { + webClient.setWebConnection(new MockMvcWebConnection(mockMvc, "/context")); + + Page page = webClient.getPage("http://localhost/context/a"); + + assertThat(page.getWebResponse().getStatusCode(), equalTo(200));; + } + + @Test + public void contextPathEmpty() throws IOException { + webClient.setWebConnection(new MockMvcWebConnection(mockMvc, "")); + + Page page = webClient.getPage("http://localhost/context/a"); + + assertThat(page.getWebResponse().getStatusCode(), equalTo(200));; + } + + @Test + public void forward() throws IOException { + webClient.setWebConnection(new MockMvcWebConnection(mockMvc, "")); + + Page page = webClient.getPage("http://localhost/forward"); + + assertThat(page.getWebResponse().getContentAsString(), equalTo("hello"));; + } + + @Test(expected = IllegalArgumentException.class) + public void contextPathDoesNotStartWithSlash() throws IOException { + new MockMvcWebConnection(mockMvc, "context"); + } + + @Test(expected = IllegalArgumentException.class) + public void contextPathEndsWithSlash() throws IOException { + new MockMvcWebConnection(mockMvc, "/context/"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java new file mode 100644 index 00000000000..770f1e06ee8 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.net.URL; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.util.NameValuePair; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class MockWebResponseBuilderTests { + + private WebRequest webRequest; + + private MockHttpServletResponse httpServletResponse; + + private MockWebResponseBuilder responseBuilder; + + @Before + public void setUp() throws Exception { + webRequest = new WebRequest(new URL("http://example.com:80/test/this/here")); + httpServletResponse = new MockHttpServletResponse(); + + responseBuilder = new MockWebResponseBuilder(System.currentTimeMillis(), webRequest, httpServletResponse); + } + + // --- constructor + + @Test(expected = IllegalArgumentException.class) + public void constructorNullWebRequest() { + new MockWebResponseBuilder(0L, null, httpServletResponse); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNullResponse() throws Exception { + new MockWebResponseBuilder(0L, new WebRequest(new URL("http://example.com:80/test/this/here")), null); + } + + // --- build + + @Test + public void buildContent() throws Exception { + httpServletResponse.getWriter().write("expected content"); + + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getContentAsString(), equalTo("expected content"));; + } + + @Test + public void buildContentCharset() throws Exception { + httpServletResponse.addHeader("Content-Type", "text/html; charset=UTF-8"); + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getContentCharset(), equalTo("UTF-8"));; + } + + @Test + public void buildContentType() throws Exception { + httpServletResponse.addHeader("Content-Type", "text/html; charset-UTF-8"); + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getContentType(), equalTo("text/html"));; + } + + @Test + public void buildResponseHeaders() throws Exception { + httpServletResponse.addHeader("Content-Type", "text/html"); + httpServletResponse.addHeader("X-Test", "value"); + WebResponse webResponse = responseBuilder.build(); + + List responseHeaders = webResponse.getResponseHeaders(); + assertThat(responseHeaders.size(), equalTo(2));; + NameValuePair header = responseHeaders.get(0); + assertThat(header.getName(), equalTo("Content-Type"));; + assertThat(header.getValue(), equalTo("text/html"));; + header = responseHeaders.get(1); + assertThat(header.getName(), equalTo("X-Test"));; + assertThat(header.getValue(), equalTo("value"));; + } + + @Test + public void buildStatus() throws Exception { + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getStatusCode(), equalTo(200));; + assertThat(webResponse.getStatusMessage(), equalTo("OK"));; + } + + @Test + public void buildStatusNotOk() throws Exception { + httpServletResponse.setStatus(401); + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getStatusCode(), equalTo(401));; + assertThat(webResponse.getStatusMessage(), equalTo("Unauthorized"));; + } + + @Test + public void buildStatusCustomMessage() throws Exception { + httpServletResponse.sendError(401, "Custom"); + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getStatusCode(), equalTo(401));; + assertThat(webResponse.getStatusMessage(), equalTo("Custom"));; + } + + @Test + public void buildWebRequest() throws Exception { + WebResponse webResponse = responseBuilder.build(); + + assertThat(webResponse.getWebRequest(), equalTo(webRequest));; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcherTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcherTests.java new file mode 100644 index 00000000000..76c9a5e757a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/UrlRegexRequestMatcherTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit; + +import java.net.URL; + +import org.junit.Test; + +import com.gargoylesoftware.htmlunit.WebRequest; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class UrlRegexRequestMatcherTests { + + @Test + public void classlevelJavadoc() throws Exception { + WebRequestMatcher cdnMatcher = new UrlRegexRequestMatcher(".*?//code.jquery.com/.*"); + + boolean matches = cdnMatcher.matches(new WebRequest(new URL("http://code.jquery.com/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(true)); + + matches = cdnMatcher.matches(new WebRequest(new URL("http://localhost/jquery-1.11.0.min.js"))); + assertThat(matches, equalTo(false)); + } + +} \ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java new file mode 100644 index 00000000000..42c3e265923 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit.webdriver; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +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.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * @author Rob Winch + * @since 4.2 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class MockMvcHtmlUnitDriverBuilderTests { + + public static final String EXPECTED_BODY = "MockMvcHtmlUnitDriverBuilderTests mvc"; + + @Autowired + WebApplicationContext context; + + MockMvc mockMvc; + + HtmlUnitDriver driver; + + @Before + public void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void mockMvcSetupNull() { + MockMvcHtmlUnitDriverBuilder.mockMvcSetup(null); + } + + @Test(expected = IllegalArgumentException.class) + public void webAppContextSetupNull() { + MockMvcHtmlUnitDriverBuilder.webAppContextSetup(null); + } + + @Test + public void mockMvcSetupConfigureDriver() throws Exception { + driver = MockMvcHtmlUnitDriverBuilder + .mockMvcSetup(mockMvc) + .configureDriver(new WebConnectionHtmlUnitDriver()); + + assertMvcProcessed("http://localhost/test"); + assertDelegateProcessed("http://example.com/"); + } + + @Test + public void mockMvcSetupCreateDriver() throws Exception { + driver = MockMvcHtmlUnitDriverBuilder + .mockMvcSetup(mockMvc) + .createDriver(); + + assertMvcProcessed("http://localhost/test"); + assertDelegateProcessed("http://example.com/"); + } + + @Test + public void javascriptEnabledDefaultEnabled() { + driver = MockMvcHtmlUnitDriverBuilder + .mockMvcSetup(mockMvc) + .createDriver(); + + assertThat(driver.isJavascriptEnabled(), equalTo(true)); + } + + @Test + public void javascriptEnabledDisabled() { + driver = MockMvcHtmlUnitDriverBuilder + .mockMvcSetup(mockMvc) + .javascriptEnabled(false) + .createDriver(); + + assertThat(driver.isJavascriptEnabled(), equalTo(false)); + } + + private void assertMvcProcessed(String url) throws Exception { + assertThat(get(url), containsString(EXPECTED_BODY)); + } + + private void assertDelegateProcessed(String url) throws Exception { + assertThat(get(url), not(containsString(EXPECTED_BODY))); + } + + private String get(String url) throws IOException { + driver.get(url); + return driver.getPageSource(); + } + + @Configuration + @EnableWebMvc + static class Config { + @RestController + static class ContextPathController { + @RequestMapping + public String contextPath(HttpServletRequest request) { + return EXPECTED_BODY; + } + } + } + +} \ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java new file mode 100644 index 00000000000..627a533f113 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.springframework.test.web.servlet.htmlunit.webdriver; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import com.gargoylesoftware.htmlunit.WebConnection; +import com.gargoylesoftware.htmlunit.WebRequest; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 4.2 + */ +@RunWith(MockitoJUnitRunner.class) +public class WebConnectionHtmlUnitDriverTests { + + @Mock + WebConnection connection; + + WebConnectionHtmlUnitDriver driver; + + @Before + public void setup() throws Exception { + driver = new WebConnectionHtmlUnitDriver(); + + when(connection.getResponse(any(WebRequest.class))).thenThrow(new InternalError("")); + } + + @Test + public void getWebConnectionDefaultNotNull() { + assertThat(driver.getWebConnection(), notNullValue()); + } + + @Test + public void setWebConnection() { + driver.setWebConnection(connection); + + assertThat(driver.getWebConnection(), equalTo(connection)); + try { + driver.get("https://example.com"); + fail("Expected Exception"); + } catch (InternalError success) {} + } + + @Test(expected = IllegalArgumentException.class) + public void setWebConnectionNull() { + driver.setWebConnection(null); + } + +} \ No newline at end of file diff --git a/src/asciidoc/testing.adoc b/src/asciidoc/testing.adoc index 12dadb237d3..745432ac7fa 100644 --- a/src/asciidoc/testing.adoc +++ b/src/asciidoc/testing.adoc @@ -4154,6 +4154,778 @@ https://github.com/spring-projects/spring-mvc-showcase[spring-mvc-showcase] has coverage based on Spring MVC Test. +[[spring-mvc-test-server-htmlunit]] +==== HtmlUnit Integration + +Spring provides integration between <> and +http://htmlunit.sourceforge.net/[HtmlUnit]. This simplifies performing end to end testing +when using HTML based views. This integration enables developers to: + +* Easily test pages using tools (i.e. http://htmlunit.sourceforge.net/[HtmlUnit], +http://seleniumhq.org/projects/webdriver/[WebDriver], & +http://www.gebish.org/manual/current/testing.html#spock_junit__testng[Geb]) that we +already use for integration testing without starting an application server +* Support testing of JavaScript +* Optionally test using mock services to speed up testing. +* Share logic between end-to-end tests and integration tests + + +[NOTE] +==== +MockMvc will work with templating technologies that do not rely on a Servlet Container +(i.e. Thymeleaf, Freemarker, Velocity, etc). It does not work with JSPs since they rely on +the Servlet Container. +==== + +[[spring-mvc-test-server-htmlunit-why]] +===== Why HtmlUnit Integration? + +The most obvious question that comes to mind is "Why do I need this?" The answer is best +found by exploring a very basic sample application. Assume you have a Spring MVC web +application that allows CRUD operations on a `Message` object. The application also allows +paging through all messages. How would you go about testing it? + +With Spring MVC Test, we can easily test if we are able to create a `Message`. + +[source,java] +---- +MockHttpServletRequestBuilder createMessage = post("/messages/") + .param("summary", "Spring Rocks") + .param("text", "In case you didn't know, Spring Rocks!"); + +mockMvc.perform(createMessage) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/messages/123")); +---- + +What if we want to test our form view that allows us to create the message? For example, +assume our form looks like the following snippet: + +[source,xml] +---- +

+ + + + + + + + +
+ +
+
+---- + +How do we ensure that our form will produce the correct request to create a new message? A +naive attempt would look like this: + +[source,java] +---- +mockMvc.perform(get("/messages/form")) + .andExpect(xpath("//input[@name='summary']").exists()) + .andExpect(xpath("//textarea[@name='text']").exists()); +---- + +This test has some obvious problems. If we updated our controller to use the parameter +"message" instead of "text", our test would would incorrectly pass. To resolve this we +could combine our two tests: + +[[spring-mvc-test-server-htmlunit-mock-mvc-test]] +[source,java] +---- +String summaryParamName = "summary"; +String textParamName = "text"; +mockMvc.perform(get("/messages/form")) + .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists()) + .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists()); + +MockHttpServletRequestBuilder createMessage = post("/messages/") + .param(summaryParamName, "Spring Rocks") + .param(textParamName, "In case you didn't know, Spring Rocks!"); + +mockMvc.perform(createMessage) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/messages/123")); +---- + +This would reduce the risk of our test incorrectly passing, but there are still some +problems: + +* What if we had multiple forms on our page? Admittedly we could update our xpath +expressions, but they get more complicated the more factors we take into account (are the +fields the correct type, are the fields enabled, etc). +* Another issue is that we are doing double the work we would expect. +We must first verify the view and then we submit the view with the same parameters we just +verified. +Ideally this could be done all at once. +* Last, there are some things that we still cannot account for. For example, what if the +form has JavaScript validation that we wish to validate too? + +The overall problem is that testing a web page is not a single interaction. Instead, it is +a combination of how the user interacts with a web page and how that web page interacts +with other resources. For example, the result of form view is used as an input to a user +for creating a message. Another example is that our form view utilizes additional +resources, like JavaScript validation, that impact the behavior of the page. + +[[spring-mvc-test-server-htmlunit-why-integration]] +====== Integration testing to the rescue? + +To resolve the issues above we could perform integration testing, but this has some +obvious drawbacks. Consider testing the view that allows us to page through the messages. +We might need the following tests: + +* Does our page display a message to the user indicating that no results are available +when the messages are empty? +* Does our page properly display a single message? +* Does our page properly support paging? + +To set these tests up we would need to ensure our database contained the proper messages +in it. This leads to a number of problems: + +* Ensuring the proper messages are in the database can be tedious (think possible foreign +keys). +* Testing would be slow since each test would require ensuring the database was in the +correct state. +* Since our database needs to be in a specific state, we cannot run the test in parallel. +* Assertions on things like auto generated ids, timestamps, etc can be challenging. + +These problems do not mean that we should abandon integration testing all together. +Instead, we can reduce the number of integration tests by moving our detailed tests to use +mock services which will perform much faster. We can then use fewer integration tests that +validate simple workflows to ensure that everything works together properly. + +[[spring-mvc-test-server-htmlunit-why-mockmvc]] +====== Enter HtmlUnit Integration + +So how can we provide a balance between testing the interactions of our pages and still +get performance? I'm sure you already guessed it...integrating with HtmlUnit +will allow us to: + +* Easily test our pages using tools (i.e. HtmlUnit, WebDriver, & Geb) that we already use +for integration testing without starting an application server +* Support testing of JavaScript +* Optionally test using mock services to speed up testing. +* Share logic between end-to-end tests and integration tests + + +[[spring-mvc-test-server-htmlunit-options]] +====== HtmlUnit Integration Options + +There are a number of ways to integrate with HtmlUnit. You can find a summary below: + +* <> - Use this option if you want the raw libraries +* <> - Use this option to ease development and be able to reuse code +between integration and end-to-end testing. +* <> - Use this option if you like using Groovy for testing, would like to +ease development, and be able to reuse code between integration and end-to-end testing. + +[[spring-mvc-test-server-htmlunit-mah]] +===== MockMvc and HtmlUnit + +This section describes how to integrate `MockMvc` and HtmlUnit. Use this option if you +want to use the raw HtmlUnit libraries. + +[[spring-mvc-test-server-htmlunit-mah-setup]] +====== MockMvc and HtmlUnit Setup + +We can easily create an HtmlUnit `WebClient` that integrates with `MockMvc` using the +following: + +[source,java] +---- +@Autowired +WebApplicationContext context; + +WebClient webClient; + +@Before +public void setup() { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .createWebClient(); +} +---- + +[NOTE] +==== +This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage see +<> +==== + +This will ensure any URL that has a host of "localhost" will be directed at our MockMvc +instance without the need for HTTP. Any other URL will be requested as normal. This allows +for easily testing with the use of CDNs. + +[[spring-mvc-test-server-htmlunit-mah-usage]] +====== MockMvc and HtmlUnit Usage + +Now we can use HtmlUnit as we normally would, but without the need to deploy our +application. For example, we can request the view to create a message with the following: + +[source,java] +---- +HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form"); +---- + +[NOTE] +==== +The the context path is "". Alternatively, we could have specified the context path as +illustrated in <>. +==== + +We can then fill out the form and submit it to create a message. + +[source,java] +---- +HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm"); +HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary"); +summaryInput.setValueAttribute("Spring Rocks"); +HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text"); +textInput.setText("In case you didn't know, Spring Rocks!"); +HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit"); +HtmlPage newMessagePage = submit.click(); +---- + +Finally, we can verify that a new message was created successfully + +[source,java] +---- +assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123"); +String id = newMessagePage.getHtmlElementById("id").getTextContent(); +assertThat(id).isEqualTo("123"); +String summary = newMessagePage.getHtmlElementById("summary").getTextContent(); +assertThat(summary).isEqualTo("Spring Rocks"); +String text = newMessagePage.getHtmlElementById("text").getTextContent(); +assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!"); +---- + +This improves on our <> in a +number of ways. First we no longer have to explicitly verify our form and then create a +request that looks like the form. Instead, we request the form, fill it out, and submit +it. This reduces the overhead significantly. + +Another important factor is that +http://htmlunit.sourceforge.net/javascript.html[HtmlUnit uses Mozilla Rhino engine] to +evaluate JavaScript on your pages. This means, that we can verify our JavaScript methods +as well! + +Refer to the http://htmlunit.sourceforge.net/gettingStarted.html[HtmlUnit documentation] +for additional information about using HtmlUnit. + +[[spring-mvc-test-server-htmlunit-mah-advanced-builder]] +====== Advanced MockMvcWebClientBuilder + +In our example above we used `MockMvcWebClientBuilder` in the simplest way possible. + +[source,java] +---- +@Autowired +WebApplicationContext context; + +WebClient webClient; + +@Before +public void setup() { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .createWebClient(); +} +---- + +We could also specify some optional arguments: + +[source,java] +---- +@Before +public void setup() { + webClient = MockMvcWebClientBuilder + // demonstrates applying a MockMvcConfigurer (Spring Security) + .webAppContextSetup(context, springSecurity()) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only + // the following will use MockMvc for example.com and example.org too + .useMockMvcForHosts("example.com","example.org") + .createWebClient(); +} +---- + + +We could also perform the exact same setup using the following: + +[source,java] +---- +MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .build(); + +webClient = MockMvcWebClientBuilder + .mockMvcSetup(mockMvc) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only + // the following will use MockMvc for example.com and example.org too + .useMockMvcForHosts("example.com","example.org") + .createWebClient(); +---- + +This is more verbose, but by building the `WebClient` with a `MockMvc` instance we have +the full power of `MockMvc` at our finger tips. Ultimately, this is simply performing the +following: + +[TIP] +==== +For additional information on creating a `MockMvc` instance refer to +<>. +==== + +[[spring-mvc-test-server-htmlunit-webdriver]] +===== MockMvc and WebDriver + +In the previous section, we have already seen how to use MockMvc with HtmlUnit. +In this section, we will leverage additional abstractions within +http://docs.seleniumhq.org/projects/webdriver/[WebDriver] to make things even easier. + +[[spring-mvc-test-server-htmlunit-webdriver-why]] +====== Why WebDriver and MockMvc? + +We can already use HtmlUnit and MockMvc, so why would we want to use WebDriver? WebDriver +provides a very elegant API and allows us to easily organize our code. To better +understand, let's explore an example. + +[NOTE] +==== +Despite being a part of http://docs.seleniumhq.org/[Selenium], WebDriver does not require +a Selenium Server to run your tests. +==== + +Suppose we need to ensure that a message is created properly. The tests involve finding +the html inputs, filling them out, and making various assertions. + +There are many tests because we want to test error conditions as well. For example, we +want to ensure that if we fill out only part of the form we get an error. If we fill out +the entire form, the newly created message is displayed afterwards. + +If one of the fields was named "summary", then we might have something like the following +repeated everywhere within our tests: + +[source,java] +---- +HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); +summaryInput.setValueAttribute(summary); +---- + +So what happens if we change the id to be "smmry". +This means we would have to update all of our tests! Instead we would hope that we wrote a +bit more elegant code where filling out the form was in its own method: + +[source,java] +---- +public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) { + setSummary(currentPage, summary); + ... +} + +public void setSummary(HtmlPage currentPage, String summary) { + HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); + summaryInput.setValueAttribute(summary); +} +---- + +This ensures that if we change the UI we do not have to update all of our tests. + +We might take it a step further and place this logic within an Object that represents the +`HtmlPage` we are currently on. + +[source,java] +---- +public class CreateMessagePage { + HtmlPage currentPage; + + HtmlTextInput summaryInput; + + HtmlSubmitInput submit; + + public CreateMessagePage(HtmlPage currentPage) { + this.currentPage = currentPage; + this.summaryInput = currentPage.getHtmlElementById("summary"); + this.submit = currentPage.getHtmlElementById("submit"); + } + + public T createMessage(String summary, String text) throws Exception { + setSummary(summary); + + HtmlPage result = submit.click(); + boolean error = CreateMessagePage.at(result); + + return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result)); + } + + public void setSummary(String summary) throws Exception { + summaryInput.setValueAttribute(summary); + } + + public static boolean at(HtmlPage page) { + return "Create Message".equals(page.getTitleText()); + } +} +---- + +Formerly, this pattern is known as the +https://code.google.com/p/selenium/wiki/PageObjects[Page Object Pattern]. While we can +certainly do this with HtmlUnit, WebDriver provides some tools that we will explore in the +following sections make this pattern much easier. + +[[spring-mvc-test-server-htmlunit-webdriver-setup]] +====== MockMvc and WebDriver Setup + +We can easily create a WebDriver implementation that integrates with MockMvc using the +following: + +[source,java] +---- +@Autowired +WebApplicationContext context; + +WebDriver driver; + +@Before +public void setup() { + driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context) + .createDriver(); +} +---- + +[NOTE] +==== +This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. +For more advanced usage, refer to <> +==== + +This will ensure any URL that has a host of "localhost" will be directed at our MockMvc +instance without the need for HTTP. Any other URL will be requested as normal. This allows +for easily testing with the use of CDNs. + +[[spring-mvc-test-server-htmlunit-webdriver-usage]] +====== MockMvc and WebDriver Usage + +Now we can use WebDriver as we normally would, but without the need to deploy our +application. For example, we can request the view to create a message with the following: + +[source,java] +---- +CreateMessagePage page = CreateMessagePage.to(driver); +---- + +We can then fill out the form and submit it to create a message. + +[source,java] +---- +ViewMessagePage viewMessagePage = + page.createMessage(ViewMessagePage.class, expectedSummary, expectedText); +---- + +This improves on the design of our +<> by leveraging the Page Object +Pattern. As we mentioned in <>, we could +use the Page Object Pattern with HtmlUnit, but it is much easier now. Let's take a look at +our `CreateMessagePage`. + +[source,java] +---- +public class CreateMessagePage + extends AbstractPage { // <1> + + // <2> + private WebElement summary; + private WebElement text; + + // <3> + @FindBy(css = "input[type=submit]") + private WebElement submit; + + public CreateMessagePage(WebDriver driver) { + super(driver); + } + + public T createMessage(Class resultPage, String summary, String details) { + this.summary.sendKeys(summary); + this.text.sendKeys(details); + this.submit.click(); + return PageFactory.initElements(driver, resultPage); + } + + public static CreateMessagePage to(WebDriver driver) { + driver.get("http://localhost:9990/mail/messages/form"); + return PageFactory.initElements(driver, CreateMessagePage.class); + } +} +---- + +<1> The first thing you will notice is that our `CreateMessagePage` extends the +`AbstractPage`. We won't go over the details of `AbstractPage`, but in summary it contains +all the common functionality of all our pages. For example, if your application has a +navigational bar, global error messages, etc. This logic can be placed in a shared +location. + +<2> The next thing you will find is that we have a member variable for each of the parts +of the HTML, `WebElement`, we are interested in. ``WebDriver``'s +https://code.google.com/p/selenium/wiki/PageFactory[PageFactory] allows us to remove a lot +of code from HtmlUnit version of `CreateMessagePage` by automatically resolving each +`WebElement`. The +http://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[PageFactory#initElements(WebDriver,Class)] +method will automatically resolve each `WebElement` by using the field name and trying to +look it up by id or name of the element on the HTML page. + +<3> We can use the +https://code.google.com/p/selenium/wiki/PageFactory#Making_the_Example_Work_Using_Annotations[@FindBy annotation] +to override the default. Our example demonstrates how we can use the `@FindBy` annotation +to lookup our submit button using the css selector of *input[type=submit]*. + +Finally, we can verify that a new message was created successfully + +[source,java] +---- +assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage); +assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message"); +---- + +We can see that our `ViewMessagePage` can allow us to interact with our custom domain +model. For example, it exposes a method that returns a `Message` object. + +[source,java] +---- +public Message getMessage() throws ParseException { + Message message = new Message(); + message.setId(getId()); + message.setCreated(getCreated()); + message.setSummary(getSummary()); + message.setText(getText()); + return message; +} +---- + +We can then leverage the rich domain objects in our assertions. + +Last, don't forget to close the `WebDriver` instance when we are done. + +[source,java] +---- +@After +public void destroy() { + if(driver != null) { + driver.close(); + } +} +---- + +For additional information on using WebDriver, refer to the +https://code.google.com/p/selenium/wiki/GettingStarted[WebDriver documentation]. + +[[spring-mvc-test-server-htmlunit-webdriver-advanced-builder]] +====== Advanced MockMvcHtmlUnitDriverBuilder + +In our example above we used `MockMvcHtmlUnitDriverBuilder` in the simplest way possible. + +[source,java] +---- +WebClient webClient; + +@Before +public void setup() { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .createWebClient(); +} +---- + +We could also specify some optional arguments: + + +[source,java] +---- +WebClient webClient; + +@Before +public void setup() { + webClient = MockMvcWebClientBuilder + // demonstrates applying a MockMvcConfigurer (Spring Security) + .webAppContextSetup(context, springSecurity()) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only + // the following will use MockMvc for example.com and example.org too + .useMockMvcForHosts("example.com","example.org") + .createWebClient(); +} +---- + +We could also perform the exact same setup using the following: + +[source,java] +---- +MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + +webClient = MockMvcWebClientBuilder + .mockMvcSetup(mockMvc) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only + // the following will use MockMvc for example.com and example.org too + .useMockMvcForHosts("example.com","example.org") + .createWebClient(); +---- + +This is more verbose, but by building the `WebDriver` with a `MockMvc` instance we have +the full power of `MockMvc` at our finger tips. Ultimately, this is simply performing the +following: + +[TIP] +==== +For additional information on creating a `MockMvc` instance refer to +<>. +==== + + +[[spring-mvc-test-server-htmlunit-geb]] +===== MockMvc and Geb + +In the previous section, we saw how to use MockMvc with WebDriver. +In this section, we will use http://www.gebish.org/[Geb] to make our tests more Groovy. + + +[[spring-mvc-test-server-htmlunit-geb-why]] +====== Why Geb and MockMvc? + +Geb is backed by WebDriver, so it offers many of the +<> we got from WebDriver. +However, Geb makes things even easier by taking care of some of the boiler plate code for +us. + +[[spring-mvc-test-server-htmlunit-geb-setup]] +====== MockMvc and Geb Setup + +We can easily initialize Geb with a WebDriver implementation that uses `MockMvc` with the +following: + +[source,groovy] +---- +def setup() { + browser.driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context, springSecurity()) + .createDriver() +} +---- + +[NOTE] +==== +This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. +For more advanced usage, refer to <> +==== + +This will ensure any URL that has a host of "localhost" will be directed at our MockMvc +instance without the need for HTTP. Any other URL will be requested as normal. This allows +for easily testing with the use of CDNs. + +[[spring-mvc-test-server-htmlunit-geb-usage]] +====== MockMvc and Geb Usage + +Now we can use Geb as we normally would, but without the need to deploy our application. +For example, we can request the view to create a message with the following: + +[source,groovy] +---- +to CreateMessagePage +---- + +We can then fill out the form and submit it to create a message. + +[source,groovy] +---- +when: +form.summary = expectedSummary +form.text = expectedMessage +submit.click(ViewMessagePage) +---- + +Any unrecognized method calls or property accesses/references that are not found will be +forwarded to the current page object. This removes a lot of the boilerplate code we needed +when using WebDriver directly. + +Additionally, this improves on the design of our +<>. The most obvious change is +that we are now using the Page Object Pattern. As we mentioned in +<>, we could use the Page Object Pattern +with HtmlUnit, but it is much easier now. + +Let's take a look at our `CreateMessagePage`. + +[source,groovy] +---- +class CreateMessagePage extends Page { + static at = { assert title == 'Messages : Create'; true } + static url = 'messages/form' + static content = { + submit { $('input[type=submit]') } + form { $('form') } + errors(required:false) { $('label.error, .alert-error')?.text() } + } +} +---- + +The first thing you will notice is that our `CreateMessagePage` extends the `Page`. +We won't go over the details of `Page`, but in summary it contains base functionality for all our pages. + +The next thing you will notice is that we define a URL in which this page can be found. +This allows us to navigate to the page with: + +[source,groovy] +---- +to CreateMessagePage +---- + +We also have a closure that determines if we are at the specified page. +It should return true if we are on the correct page. +This is why we can assert that we are on the correct page with: + +[source,groovy] +---- +then: +at CreateMessagePage +errors.contains('This field is required.') +---- + +[NOTE] +==== +We use an assertion in the closure, so we can determine where things went wrong if we were +at the wrong page. +==== + +We last create a content closure that specifies all the areas of interest within the page. +We can use a +http://www.gebish.org/manual/current/intro.html#the_jquery_ish_navigator_api[jQuery-ish Navigator API] +to select the content we are interested in. + +Finally, we can verify that a new message was created successfully + +[source,groovy] +---- +then: +at ViewMessagePage +success == 'Successfully created a new message' +id +date +summary == expectedSummary +message == expectedMessage +---- + + [[spring-mvc-test-client]] ==== Client-Side REST Tests Client-side tests are for code using the `RestTemplate`. The goal is to define expected diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc index 16ec877b444..5e37ce26ba2 100644 --- a/src/asciidoc/whats-new.adoc +++ b/src/asciidoc/whats-new.adoc @@ -562,6 +562,10 @@ public @interface MyTestConfig { @Rule public final SpringMethodRule springMethodRule = new SpringMethodRule(); ---- +* The Spring MVC Test framework now provides first-class support for HtmlUnit, + including integration with Selenium's WebDriver, allowing for page-based + web application testing without the need to deploy to a Servlet container. +** See <> for details. * `AopTestUtils` is a new testing utility that allows developers to obtain a reference to the underlying target object hidden behind one or more Spring proxies.