Add fake HttpServlet{Request,Response} for testing

Instead of relying on mocks, provide mostly-featureful fakes that
allow inspection of the structured data passed to them. The fake
methods other than path getters are not really used yet.

These classes are largely copied from the Gitiles project at 4434b800;
the entirety of the copied code was authored by Google employees at
Google, so it is safe to include in this project.

Change-Id: I57af5d32cfabda6498744382100b4781e4f23d49
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index da224d5..3f81d86 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -58,6 +58,7 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//gerrit-util-http:http',
+    '//gerrit-util-http:testutil',
     '//lib:junit',
     '//lib:gson',
     '//lib:gwtorm',
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
index 10a35a8..94f3768 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
 import com.google.inject.Key;
 import com.google.inject.util.Providers;
 
@@ -88,8 +90,8 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock(FilterConfig.class);
-    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
-    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
 
     FilterChain chain = ems.createMock(FilterChain.class);
     chain.doFilter(req, res);
@@ -110,8 +112,8 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock("config", FilterConfig.class);
-    HttpServletRequest req = ems.createMock("req", HttpServletRequest.class);
-    HttpServletResponse res = ems.createMock("res", HttpServletResponse.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
 
     FilterChain chain = ems.createMock("chain", FilterChain.class);
 
@@ -137,8 +139,8 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock(FilterConfig.class);
-    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
-    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
 
     IMocksControl mockControl = ems.createStrictControl();
     FilterChain chain = mockControl.createMock(FilterChain.class);
@@ -169,8 +171,8 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock(FilterConfig.class);
-    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
-    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
 
     IMocksControl mockControl = ems.createStrictControl();
     FilterChain chain = mockControl.createMock(FilterChain.class);
@@ -202,8 +204,8 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock(FilterConfig.class);
-    HttpServletRequest req = ems.createMock(HttpServletRequest.class);
-    HttpServletResponse res = ems.createMock(HttpServletResponse.class);
+    HttpServletRequest req = new FakeHttpServletRequest();
+    HttpServletResponse res = new FakeHttpServletResponse();
 
     IMocksControl mockControl = ems.createStrictControl();
     FilterChain chain = mockControl.createMock(FilterChain.class);
@@ -242,10 +244,10 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock(FilterConfig.class);
-    HttpServletRequest req1 = ems.createMock("req1", HttpServletRequest.class);
-    HttpServletRequest req2 = ems.createMock("req2", HttpServletRequest.class);
-    HttpServletResponse res1 = ems.createMock("res1", HttpServletResponse.class);
-    HttpServletResponse res2 = ems.createMock("res2", HttpServletResponse.class);
+    HttpServletRequest req1 = new FakeHttpServletRequest();
+    HttpServletRequest req2 = new FakeHttpServletRequest();
+    HttpServletResponse res1 = new FakeHttpServletResponse();
+    HttpServletResponse res2 = new FakeHttpServletResponse();
 
     IMocksControl mockControl = ems.createStrictControl();
     FilterChain chain = mockControl.createMock("chain", FilterChain.class);
@@ -294,12 +296,12 @@
     EasyMockSupport ems = new EasyMockSupport();
 
     FilterConfig config = ems.createMock(FilterConfig.class);
-    HttpServletRequest req1 = ems.createMock("req1", HttpServletRequest.class);
-    HttpServletRequest req2 = ems.createMock("req2", HttpServletRequest.class);
-    HttpServletRequest req3 = ems.createMock("req3", HttpServletRequest.class);
-    HttpServletResponse res1 = ems.createMock("res1", HttpServletResponse.class);
-    HttpServletResponse res2 = ems.createMock("res2", HttpServletResponse.class);
-    HttpServletResponse res3 = ems.createMock("res3", HttpServletResponse.class);
+    HttpServletRequest req1 = new FakeHttpServletRequest();
+    HttpServletRequest req2 = new FakeHttpServletRequest();
+    HttpServletRequest req3 = new FakeHttpServletRequest();
+    HttpServletResponse res1 = new FakeHttpServletResponse();
+    HttpServletResponse res2 = new FakeHttpServletResponse();
+    HttpServletResponse res3 = new FakeHttpServletResponse();
 
     Plugin plugin = ems.createMock(Plugin.class);
 
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
index e032823..9559e13 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/plugins/ContextMapperTest.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.httpd.plugins;
 
-import static org.easymock.EasyMock.createNiceMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 
 import org.junit.Test;
 
@@ -34,16 +33,17 @@
     ContextMapper classUnderTest = new ContextMapper(CONTEXT);
 
     HttpServletRequest originalRequest =
-        createMockRequest("/plugins/", PLUGIN_NAME + "/" + RESOURCE);
+        createFakeRequest("/plugins/", PLUGIN_NAME + "/" + RESOURCE);
 
     HttpServletRequest result =
         classUnderTest.create(originalRequest, PLUGIN_NAME);
 
-    assertEquals(CONTEXT + "/plugins/" + PLUGIN_NAME, result.getContextPath());
-    assertEquals("", result.getServletPath());
-    assertEquals("/" + RESOURCE, result.getPathInfo());
-    assertEquals(CONTEXT + "/plugins/" + PLUGIN_NAME + "/" + RESOURCE,
-        result.getRequestURI());
+    assertThat(result.getContextPath())
+        .isEqualTo(CONTEXT + "/plugins/" + PLUGIN_NAME);
+    assertThat(result.getServletPath()).isEqualTo("");
+    assertThat(result.getPathInfo()).isEqualTo("/" + RESOURCE);
+    assertThat(result.getRequestURI())
+        .isEqualTo(CONTEXT + "/plugins/" + PLUGIN_NAME + "/" + RESOURCE);
   }
 
   @Test
@@ -51,29 +51,22 @@
     ContextMapper classUnderTest = new ContextMapper(CONTEXT);
 
     HttpServletRequest originalRequest =
-        createMockRequest("/a/plugins/", PLUGIN_NAME + "/" + RESOURCE);
+        createFakeRequest("/a/plugins/", PLUGIN_NAME + "/" + RESOURCE);
 
     HttpServletRequest result =
         classUnderTest.create(originalRequest, PLUGIN_NAME);
 
-    assertEquals(CONTEXT + "/a/plugins/" + PLUGIN_NAME,
-        result.getContextPath());
-    assertEquals("", result.getServletPath());
-    assertEquals("/" + RESOURCE, result.getPathInfo());
-    assertEquals(CONTEXT + "/a/plugins/" + PLUGIN_NAME + "/" + RESOURCE,
-        result.getRequestURI());
+    assertThat(result.getContextPath())
+        .isEqualTo(CONTEXT + "/a/plugins/" + PLUGIN_NAME);
+    assertThat(result.getServletPath()).isEqualTo("");
+    assertThat(result.getPathInfo()).isEqualTo("/" + RESOURCE);
+    assertThat(result.getRequestURI())
+        .isEqualTo(CONTEXT + "/a/plugins/" + PLUGIN_NAME + "/" + RESOURCE);
   }
 
-  private static HttpServletRequest createMockRequest(String servletPath,
+  private static FakeHttpServletRequest createFakeRequest(String servletPath,
       String pathInfo) {
-    HttpServletRequest req = createNiceMock(HttpServletRequest.class);
-    expect(req.getContextPath()).andStubReturn(CONTEXT);
-    expect(req.getServletPath()).andStubReturn(servletPath);
-    expect(req.getPathInfo()).andStubReturn(pathInfo);
-    String uri = CONTEXT + servletPath + pathInfo;
-    expect(req.getRequestURI()).andStubReturn(uri);
-    replay(req);
-
-    return req;
+    return new FakeHttpServletRequest(
+        "gerrit.example.com", 80, CONTEXT, servletPath).setPathInfo(pathInfo);
   }
 }
diff --git a/gerrit-util-http/BUCK b/gerrit-util-http/BUCK
index e770f60..4c3816b 100644
--- a/gerrit-util-http/BUCK
+++ b/gerrit-util-http/BUCK
@@ -5,11 +5,29 @@
   visibility = ['PUBLIC'],
 )
 
+TESTUTIL_SRCS = glob(['src/test/**/testutil/**/*.java'])
+
+java_library(
+  name = 'testutil',
+  srcs = TESTUTIL_SRCS,
+  deps = [
+    '//gerrit-extension-api:api',
+    '//lib:guava',
+    '//lib:servlet-api-3_1',
+    '//lib/jgit:jgit',
+  ],
+  visibility = ['PUBLIC'],
+)
+
 java_test(
   name = 'http_tests',
-  srcs = glob(['src/test/java/**/*.java']),
+  srcs = glob(
+    ['src/test/java/**/*.java'],
+    excludes = TESTUTIL_SRCS,
+  ),
   deps = [
     ':http',
+    ':testutil',
     '//lib:junit',
     '//lib:servlet-api-3_1',
     '//lib:truth',
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
index 42fcb16..e656e56 100644
--- a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
@@ -15,77 +15,50 @@
 package com.google.gerrit.util.http;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
 
-import org.junit.After;
-import org.junit.Before;
+import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
+
 import org.junit.Test;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
-
 public class RequestUtilTest {
-  private List<Object> mocks;
-
-  @Before
-  public void setUp() {
-    mocks = Collections.synchronizedList(new ArrayList<>());
-  }
-
-  @After
-  public void tearDown() {
-    for (Object mock : mocks) {
-      verify(mock);
-    }
-  }
-
   @Test
   public void emptyContextPath() {
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/s/foo/bar", "", "/s"))).isEqualTo("/foo/bar");
+        fakeRequest("", "/s", "/foo/bar"))).isEqualTo("/foo/bar");
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/s/foo%2Fbar", "", "/s"))).isEqualTo("/foo%2Fbar");
+        fakeRequest("", "/s", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
   }
 
   @Test
   public void emptyServletPath() {
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/foo/bar", "", "/c"))).isEqualTo("/foo/bar");
+        fakeRequest("", "/c", "/foo/bar"))).isEqualTo("/foo/bar");
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/foo%2Fbar", "", "/c"))).isEqualTo("/foo%2Fbar");
+        fakeRequest("", "/c", "/foo%2Fbar"))).isEqualTo("/foo%2Fbar");
   }
 
   @Test
   public void trailingSlashes() {
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo/bar/", "/c", "/s"))).isEqualTo("/foo/bar/");
+        fakeRequest("/c", "/s", "/foo/bar/"))).isEqualTo("/foo/bar/");
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo/bar///", "/c", "/s"))).isEqualTo("/foo/bar/");
+        fakeRequest("/c", "/s", "/foo/bar///"))).isEqualTo("/foo/bar/");
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo%2Fbar/", "/c", "/s"))).isEqualTo("/foo%2Fbar/");
+        fakeRequest("/c", "/s", "/foo%2Fbar/"))).isEqualTo("/foo%2Fbar/");
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s/foo%2Fbar///", "/c", "/s"))).isEqualTo("/foo%2Fbar/");
+        fakeRequest("/c", "/s", "/foo%2Fbar///"))).isEqualTo("/foo%2Fbar/");
   }
 
   @Test
-  public void servletPathMatchesRequestPath() {
+  public void emptyPathInfo() {
     assertThat(RequestUtil.getEncodedPathInfo(
-        mockRequest("/c/s", "/c", "/s"))).isNull();
+        fakeRequest("/c", "/s", ""))).isNull();
   }
 
-  private HttpServletRequest mockRequest(String uri, String contextPath, String servletPath) {
-    HttpServletRequest req = createMock(HttpServletRequest.class);
-    expect(req.getRequestURI()).andStubReturn(uri);
-    expect(req.getContextPath()).andStubReturn(contextPath);
-    expect(req.getServletPath()).andStubReturn(servletPath);
-    replay(req);
-    mocks.add(req);
-    return req;
+  private FakeHttpServletRequest fakeRequest(String contextPath,
+      String servletPath, String pathInfo) {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "gerrit.example.com", 80, contextPath, servletPath);
+    return req.setPathInfo(pathInfo);
   }
 }
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
new file mode 100644
index 0000000..04c7b48
--- /dev/null
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -0,0 +1,480 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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 com.google.gerrit.util.http.testutil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.Url;
+
+import java.io.BufferedReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+import javax.servlet.http.Part;
+
+/** Simple fake implementation of {@link HttpServletRequest}. */
+public class FakeHttpServletRequest implements HttpServletRequest {
+  public static final String SERVLET_PATH = "/b";
+
+  private final Map<String, Object> attributes;
+  private final ListMultimap<String, String> headers;
+
+  private ListMultimap<String, String> parameters;
+  private String hostName;
+  private int port;
+  private String contextPath;
+  private String servletPath;
+  private String path;
+
+  public FakeHttpServletRequest() {
+    this("gerrit.example.com", 80, "", SERVLET_PATH);
+  }
+
+  public FakeHttpServletRequest(String hostName, int port, String contextPath,
+      String servletPath) {
+    this.hostName = checkNotNull(hostName, "hostName");
+    checkArgument(port > 0);
+    this.port = port;
+    this.contextPath = checkNotNull(contextPath, "contextPath");
+    this.servletPath = checkNotNull(servletPath, "servletPath");
+    attributes = Maps.newConcurrentMap();
+    parameters = LinkedListMultimap.create();
+    headers = LinkedListMultimap.create();
+  }
+
+  @Override
+  public Object getAttribute(String name) {
+    return attributes.get(name);
+  }
+
+  @Override
+  public Enumeration<String> getAttributeNames() {
+    return Collections.enumeration(attributes.keySet());
+  }
+
+  @Override
+  public String getCharacterEncoding() {
+    return UTF_8.name();
+  }
+
+  @Override
+  public int getContentLength() {
+    return -1;
+  }
+
+  @Override
+  public String getContentType() {
+    return null;
+  }
+
+  @Override
+  public ServletInputStream getInputStream() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getLocalAddr() {
+    return "1.2.3.4";
+  }
+
+  @Override
+  public String getLocalName() {
+    return hostName;
+  }
+
+  @Override
+  public int getLocalPort() {
+    return port;
+  }
+
+  @Override
+  public Locale getLocale() {
+    return Locale.US;
+  }
+
+  @Override
+  public Enumeration<Locale> getLocales() {
+    return Collections.enumeration(Collections.singleton(Locale.US));
+  }
+
+  @Override
+  public String getParameter(String name) {
+    return Iterables.getFirst(parameters.get(name), null);
+  }
+
+  private static final Function<Collection<String>, String[]> STRING_COLLECTION_TO_ARRAY =
+      new Function<Collection<String>, String[]>() {
+        @Override
+        public String[] apply(Collection<String> values) {
+          return values.toArray(new String[0]);
+        }
+      };
+
+  @Override
+  public Map<String, String[]> getParameterMap() {
+    return Collections.unmodifiableMap(
+        Maps.transformValues(parameters.asMap(), STRING_COLLECTION_TO_ARRAY));
+  }
+
+  @Override
+  public Enumeration<String> getParameterNames() {
+    return Collections.enumeration(parameters.keySet());
+  }
+
+  @Override
+  public String[] getParameterValues(String name) {
+    return STRING_COLLECTION_TO_ARRAY.apply(parameters.get(name));
+  }
+
+  public void setQueryString(String qs) {
+    ListMultimap<String, String> params = LinkedListMultimap.create();
+    for (String entry : Splitter.on('&').split(qs)) {
+      List<String> kv = Splitter.on('=').limit(2).splitToList(entry);
+      try {
+        params.put(URLDecoder.decode(kv.get(0), UTF_8.name()),
+            kv.size() == 2 ? URLDecoder.decode(kv.get(1), UTF_8.name()) : "");
+      } catch (UnsupportedEncodingException e) {
+        throw new IllegalArgumentException(e);
+      }
+    }
+    parameters = params;
+  }
+
+  @Override
+  public String getProtocol() {
+    return "HTTP/1.1";
+  }
+
+  @Override
+  public BufferedReader getReader() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public String getRealPath(String path) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getRemoteAddr() {
+    return "5.6.7.8";
+  }
+
+  @Override
+  public String getRemoteHost() {
+    return "remotehost";
+  }
+
+  @Override
+  public int getRemotePort() {
+    return 1234;
+  }
+
+  @Override
+  public RequestDispatcher getRequestDispatcher(String path) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getScheme() {
+    return port == 443 ? "https" : "http";
+  }
+
+  @Override
+  public String getServerName() {
+    return hostName;
+  }
+
+  @Override
+  public int getServerPort() {
+    return port;
+  }
+
+  @Override
+  public boolean isSecure() {
+    return port == 443;
+  }
+
+  @Override
+  public void removeAttribute(String name) {
+    attributes.remove(name);
+  }
+
+  @Override
+  public void setAttribute(String name, Object value) {
+    attributes.put(name, value);
+  }
+
+  @Override
+  public void setCharacterEncoding(String env) throws UnsupportedOperationException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getAuthType() {
+    return null;
+  }
+
+  @Override
+  public String getContextPath() {
+    return contextPath;
+  }
+
+  @Override
+  public Cookie[] getCookies() {
+    return new Cookie[0];
+  }
+
+  @Override
+  public long getDateHeader(String name) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getHeader(String name) {
+    return Iterables.getFirst(headers.get(name), null);
+  }
+
+  @Override
+  public Enumeration<String> getHeaderNames() {
+    return Collections.enumeration(headers.keySet());
+  }
+
+  @Override
+  public Enumeration<String> getHeaders(String name) {
+    return Collections.enumeration(headers.get(name));
+  }
+
+  @Override
+  public int getIntHeader(String name) {
+    return Integer.parseInt(getHeader(name));
+  }
+
+  @Override
+  public String getMethod() {
+    return "GET";
+  }
+
+  @Override
+  public String getPathInfo() {
+    return path;
+  }
+
+  public FakeHttpServletRequest setPathInfo(String path) {
+    this.path = checkNotNull(path);
+    return this;
+  }
+
+  @Override
+  public String getPathTranslated() {
+    return path;
+  }
+
+  @Override
+  public String getQueryString() {
+    return paramsToString(parameters);
+  }
+
+  @Override
+  public String getRemoteUser() {
+    return null;
+  }
+
+  @Override
+  public String getRequestURI() {
+    String uri = contextPath + servletPath + path;
+    if (!parameters.isEmpty()) {
+      uri += "?" + paramsToString(parameters);
+    }
+    return uri;
+  }
+
+  @Override
+  public StringBuffer getRequestURL() {
+    return null;
+  }
+
+  @Override
+  public String getRequestedSessionId() {
+    return null;
+  }
+
+  @Override
+  public String getServletPath() {
+    return servletPath;
+  }
+
+  @Override
+  public HttpSession getSession() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public HttpSession getSession(boolean create) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Principal getUserPrincipal() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isRequestedSessionIdFromCookie() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isRequestedSessionIdFromURL() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public boolean isRequestedSessionIdFromUrl() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isRequestedSessionIdValid() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isUserInRole(String role) {
+    throw new UnsupportedOperationException();
+  }
+
+  private static String paramsToString(ListMultimap<String, String> params) {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    for (Map.Entry<String, String> e : params.entries()) {
+      if (!first) {
+        sb.append('&');
+      } else {
+        first = false;
+      }
+      sb.append(Url.encode(e.getKey()));
+      if (!"".equals(e.getValue())) {
+        sb.append('=').append(Url.encode(e.getValue()));
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public AsyncContext getAsyncContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DispatcherType getDispatcherType() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ServletContext getServletContext() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean isAsyncStarted() {
+    return false;
+  }
+
+  @Override
+  public boolean isAsyncSupported() {
+    return false;
+  }
+
+  @Override
+  public AsyncContext startAsync() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public AsyncContext startAsync(ServletRequest req, ServletResponse res) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean authenticate(HttpServletResponse res) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Part getPart(String name) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Collection<Part> getParts() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void login(String username, String password) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void logout() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long getContentLengthLong() {
+    return getContentLength();
+  }
+
+  @Override
+  public String changeSessionId() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T extends HttpUpgradeHandler> T upgrade(
+      Class<T> httpUpgradeHandlerClass) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
new file mode 100644
index 0000000..6201bb0
--- /dev/null
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletResponse.java
@@ -0,0 +1,290 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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 com.google.gerrit.util.http.testutil;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.net.HttpHeaders;
+
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Locale;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+/** Simple fake implementation of {@link HttpServletResponse}. */
+public class FakeHttpServletResponse implements HttpServletResponse {
+  private final ByteArrayOutputStream actualBody = new ByteArrayOutputStream();
+  private final ListMultimap<String, String> headers = LinkedListMultimap.create();
+
+  private int status = SC_OK;
+  private boolean committed;
+  private ServletOutputStream outputStream;
+  private PrintWriter writer;
+
+  public FakeHttpServletResponse() {
+  }
+
+  @Override
+  public synchronized void flushBuffer() throws IOException {
+    if (outputStream != null) {
+      outputStream.flush();
+    }
+    if (writer != null) {
+      writer.flush();
+    }
+  }
+
+  @Override
+  public int getBufferSize() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String getCharacterEncoding() {
+    return UTF_8.name();
+  }
+
+  @Override
+  public String getContentType() {
+    return null;
+  }
+
+  @Override
+  public Locale getLocale() {
+    return Locale.US;
+  }
+
+  @Override
+  public synchronized ServletOutputStream getOutputStream() {
+    checkState(writer == null, "getWriter() already called");
+    if (outputStream == null) {
+      final PrintWriter osWriter = new PrintWriter(actualBody);
+      outputStream = new ServletOutputStream() {
+        @Override
+        public void write(int c) throws IOException {
+          osWriter.write(c);
+          osWriter.flush();
+        }
+
+        @Override
+        public boolean isReady() {
+          return true;
+        }
+
+        @Override
+        public void setWriteListener(WriteListener listener) {
+          throw new UnsupportedOperationException();
+        }
+      };
+    }
+    return outputStream;
+  }
+
+  @Override
+  public synchronized PrintWriter getWriter() {
+    checkState(outputStream == null, "getOutputStream() already called");
+    if (writer == null) {
+      writer = new PrintWriter(actualBody);
+    }
+    return writer;
+  }
+
+  @Override
+  public synchronized boolean isCommitted() {
+    return committed;
+  }
+
+  @Override
+  public void reset() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void resetBuffer() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setBufferSize(int sz) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setCharacterEncoding(String name) {
+    checkArgument(UTF_8.equals(Charset.forName(name)),
+        "unsupported charset: %s", name);
+  }
+
+  @Override
+  public void setContentLength(int length) {
+    setContentLengthLong(length);
+  }
+
+  @Override
+  public void setContentLengthLong(long length) {
+    headers.removeAll(HttpHeaders.CONTENT_LENGTH);
+    headers.put(HttpHeaders.CONTENT_LENGTH, Long.toString(length));
+  }
+
+  @Override
+  public void setContentType(String type) {
+    headers.removeAll(HttpHeaders.CONTENT_TYPE);
+    headers.put(HttpHeaders.CONTENT_TYPE, type);
+  }
+
+  @Override
+  public void setLocale(Locale locale) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addCookie(Cookie cookie) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addDateHeader(String name, long value) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addHeader(String name, String value) {
+    headers.put(name, value);
+  }
+
+  @Override
+  public void addIntHeader(String name, int value) {
+    headers.put(name, Integer.toString(value));
+  }
+
+  @Override
+  public boolean containsHeader(String name) {
+    return headers.containsKey(name);
+  }
+
+  @Override
+  public String encodeRedirectURL(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public String encodeRedirectUrl(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String encodeURL(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  @Deprecated
+  public String encodeUrl(String url) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public synchronized void sendError(int sc) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  public synchronized void sendError(int sc, String msg) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  public synchronized void sendRedirect(String loc) {
+    status = SC_FOUND;
+    setHeader(HttpHeaders.LOCATION, loc);
+    committed = true;
+  }
+
+  @Override
+  public void setDateHeader(String name, long value) {
+    setHeader(name, Long.toString(value));
+  }
+
+  @Override
+  public void setHeader(String name, String value) {
+    headers.removeAll(name);
+    addHeader(name, value);
+  }
+
+  @Override
+  public void setIntHeader(String name, int value) {
+    headers.removeAll(name);
+    addIntHeader(name, value);
+  }
+
+  @Override
+  public synchronized void setStatus(int sc) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  @Deprecated
+  public synchronized void setStatus(int sc, String msg) {
+    status = sc;
+    committed = true;
+  }
+
+  @Override
+  public synchronized int getStatus() {
+    return status;
+  }
+
+  @Override
+  public String getHeader(String name) {
+    return Iterables.getFirst(headers.get(checkNotNull(name)), null);
+  }
+
+  @Override
+  public Collection<String> getHeaderNames() {
+    return headers.keySet();
+  }
+
+  @Override
+  public Collection<String> getHeaders(String name) {
+    return headers.get(checkNotNull(name));
+  }
+
+  public byte[] getActualBody() {
+    return actualBody.toByteArray();
+  }
+
+  public String getActualBodyString() {
+    return RawParseUtils.decode(getActualBody());
+  }
+}