aboutsummaryrefslogtreecommitdiff
path: root/mockwebserver
diff options
context:
space:
mode:
authorNarayan Kamath <narayan@google.com>2013-11-04 16:04:55 +0000
committerNarayan Kamath <narayan@google.com>2013-11-04 17:24:27 +0000
commit166772be0e5cfdaea1a64b9f63e4c8dbfe48cba3 (patch)
treec49bccf68132fef4d5e3cee813a1fca794bf58af /mockwebserver
parented17abea772718d6a8fdaafbfa52f118e07bda92 (diff)
downloadokhttp-166772be0e5cfdaea1a64b9f63e4c8dbfe48cba3.tar.gz
Update okhttp.
Updated to commit 19a21936ffbb5e358799af9e4fb7306af45f38. This also moves src/ to okhttp/src/ to stay faithful to the original okhttp tree & to make it easier to pull updates. Change-Id: Ia1971823f31e5c6957d831f368e3a1fcce38d44d
Diffstat (limited to 'mockwebserver')
-rw-r--r--mockwebserver/pom.xml53
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java124
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java161
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java34
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java223
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java722
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java71
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java153
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java61
-rw-r--r--mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java100
-rw-r--r--mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java289
11 files changed, 1991 insertions, 0 deletions
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
new file mode 100644
index 0000000..f33f156
--- /dev/null
+++ b/mockwebserver/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>parent</artifactId>
+ <version>1.2.2-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>mockwebserver</artifactId>
+ <name>MockWebServer</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-protocols</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk15on</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mortbay.jetty.npn</groupId>
+ <artifactId>npn-boot</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <!-- Don't restrict test code to Java 1.5 APIs. -->
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-maven-plugin</artifactId>
+ <configuration>
+ <signature>
+ <groupId>org.codehaus.mojo.signature</groupId>
+ <artifactId>java16</artifactId>
+ <version>1.0</version>
+ </signature>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
new file mode 100644
index 0000000..0677263
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2012 Square, Inc.
+ *
+ * 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.squareup.okhttp.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
+
+/**
+ * Constructs an SSL context for testing. This uses Bouncy Castle to generate a
+ * self-signed certificate for a single hostname such as "localhost".
+ *
+ * <p>The crypto performed by this class is relatively slow. Clients should
+ * reuse SSL context instances where possible.
+ */
+public final class SslContextBuilder {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
+ private final String hostName;
+ private long notBefore = System.currentTimeMillis();
+ private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS;
+
+ /**
+ * @param hostName the subject of the host. For TLS this should be the
+ * domain name that the client uses to identify the server.
+ */
+ public SslContextBuilder(String hostName) {
+ this.hostName = hostName;
+ }
+
+ public SSLContext build() throws GeneralSecurityException {
+ char[] password = "password".toCharArray();
+
+ // Generate public and private keys and use them to make a self-signed certificate.
+ KeyPair keyPair = generateKeyPair();
+ X509Certificate certificate = selfSignedCertificate(keyPair);
+
+ // Put 'em in a key store.
+ KeyStore keyStore = newEmptyKeyStore(password);
+ Certificate[] certificateChain = { certificate };
+ keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
+ keyStore.setCertificateEntry("cert", certificate);
+
+ // Wrap it up in an SSL context.
+ KeyManagerFactory keyManagerFactory =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, password);
+ TrustManagerFactory trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keyStore);
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(),
+ new SecureRandom());
+ return sslContext;
+ }
+
+ private KeyPair generateKeyPair() throws GeneralSecurityException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+ keyPairGenerator.initialize(1024, new SecureRandom());
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ /**
+ * Generates a certificate for {@code hostName} containing {@code keyPair}'s
+ * public key, signed by {@code keyPair}'s private key.
+ */
+ @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
+ private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException {
+ X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
+ X500Principal issuer = new X500Principal("CN=" + hostName);
+ X500Principal subject = new X500Principal("CN=" + hostName);
+ generator.setSerialNumber(BigInteger.ONE);
+ generator.setIssuerDN(issuer);
+ generator.setNotBefore(new Date(notBefore));
+ generator.setNotAfter(new Date(notAfter));
+ generator.setSubjectDN(subject);
+ generator.setPublicKey(keyPair.getPublic());
+ generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
+ return generator.generateX509Certificate(keyPair.getPrivate(), "BC");
+ }
+
+ private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ InputStream in = null; // By convention, 'null' creates an empty key store.
+ keyStore.load(in, password);
+ return keyStore;
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
new file mode 100644
index 0000000..7371f2e
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011 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.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.SslContextBuilder;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.List;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import org.eclipse.jetty.npn.NextProtoNego;
+
+/** A basic SPDY server that serves the contents of a local directory. */
+public final class SpdyServer implements IncomingStreamHandler {
+ private final File baseDirectory;
+ private SSLSocketFactory sslSocketFactory;
+
+ public SpdyServer(File baseDirectory) {
+ this.baseDirectory = baseDirectory;
+ }
+
+ public void useHttps(SSLSocketFactory sslSocketFactory) {
+ this.sslSocketFactory = sslSocketFactory;
+ }
+
+ private void run() throws Exception {
+ ServerSocket serverSocket = new ServerSocket(8888);
+ serverSocket.setReuseAddress(true);
+
+ while (true) {
+ Socket socket = serverSocket.accept();
+ if (sslSocketFactory != null) {
+ socket = doSsl(socket);
+ }
+ new SpdyConnection.Builder(false, socket).handler(this).build();
+ }
+ }
+
+ private Socket doSsl(Socket socket) throws IOException {
+ SSLSocket sslSocket =
+ (SSLSocket) sslSocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(),
+ socket.getPort(), true);
+ sslSocket.setUseClientMode(false);
+ NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() {
+ @Override public void unsupported() {
+ System.out.println("UNSUPPORTED");
+ }
+ @Override public List<String> protocols() {
+ return Arrays.asList("spdy/3");
+ }
+ @Override public void protocolSelected(String protocol) {
+ System.out.println("PROTOCOL SELECTED: " + protocol);
+ }
+ });
+ return sslSocket;
+ }
+
+ @Override public void receive(final SpdyStream stream) throws IOException {
+ List<String> requestHeaders = stream.getRequestHeaders();
+ String path = null;
+ for (int i = 0; i < requestHeaders.size(); i += 2) {
+ String s = requestHeaders.get(i);
+ if (":path".equals(s)) {
+ path = requestHeaders.get(i + 1);
+ break;
+ }
+ }
+
+ if (path == null) {
+ // TODO: send bad request error
+ throw new AssertionError();
+ }
+
+ File file = new File(baseDirectory + path);
+
+ if (file.isDirectory()) {
+ serveDirectory(stream, file.list());
+ } else if (file.exists()) {
+ serveFile(stream, file);
+ } else {
+ send404(stream, path);
+ }
+ }
+
+ private void send404(SpdyStream stream, String path) throws IOException {
+ List<String> responseHeaders =
+ Arrays.asList(":status", "404", ":version", "HTTP/1.1", "content-type", "text/plain");
+ stream.reply(responseHeaders, true);
+ OutputStream out = stream.getOutputStream();
+ String text = "Not found: " + path;
+ out.write(text.getBytes("UTF-8"));
+ out.close();
+ }
+
+ private void serveDirectory(SpdyStream stream, String[] files) throws IOException {
+ List<String> responseHeaders =
+ Arrays.asList(":status", "200", ":version", "HTTP/1.1", "content-type",
+ "text/html; charset=UTF-8");
+ stream.reply(responseHeaders, true);
+ OutputStream out = stream.getOutputStream();
+ Writer writer = new OutputStreamWriter(out, "UTF-8");
+ for (String file : files) {
+ writer.write("<a href='" + file + "'>" + file + "</a><br>");
+ }
+ writer.close();
+ }
+
+ private void serveFile(SpdyStream stream, File file) throws IOException {
+ InputStream in = new FileInputStream(file);
+ byte[] buffer = new byte[8192];
+ stream.reply(
+ Arrays.asList(":status", "200", ":version", "HTTP/1.1", "content-type", contentType(file)),
+ true);
+ OutputStream out = stream.getOutputStream();
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ out.write(buffer, 0, count);
+ }
+ out.close();
+ }
+
+ private String contentType(File file) {
+ return file.getName().endsWith(".html") ? "text/html" : "text/plain";
+ }
+
+ public static void main(String... args) throws Exception {
+ if (args.length != 1 || args[0].startsWith("-")) {
+ System.out.println("Usage: SpdyServer <base directory>");
+ return;
+ }
+
+ SpdyServer server = new SpdyServer(new File(args[0]));
+ SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
+ server.useHttps(sslContext.getSocketFactory());
+ server.run();
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
new file mode 100644
index 0000000..ac6bac4
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+/** Handler for mock server requests. */
+public abstract class Dispatcher {
+ /**
+ * Returns a response to satisfy {@code request}. This method may block (for
+ * instance, to wait on a CountdownLatch).
+ */
+ public abstract MockResponse dispatch(RecordedRequest request) throws InterruptedException;
+
+ /**
+ * Returns the socket policy of the next request. Default implementation
+ * returns {@link SocketPolicy#KEEP_OPEN}. Mischievous implementations can
+ * return other values to test HTTP edge cases.
+ */
+ public SocketPolicy peekSocketPolicy() {
+ return SocketPolicy.KEEP_OPEN;
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
new file mode 100644
index 0000000..b073c11
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** A scripted response to be replayed by the mock web server. */
+public final class MockResponse implements Cloneable {
+ private static final String CHUNKED_BODY_HEADER = "Transfer-encoding: chunked";
+
+ private String status = "HTTP/1.1 200 OK";
+ private List<String> headers = new ArrayList<String>();
+
+ /** The response body content, or null if {@code bodyStream} is set. */
+ private byte[] body;
+ /** The response body content, or null if {@code body} is set. */
+ private InputStream bodyStream;
+
+ private int bytesPerSecond = Integer.MAX_VALUE;
+ private SocketPolicy socketPolicy = SocketPolicy.KEEP_OPEN;
+
+ /** Creates a new mock response with an empty body. */
+ public MockResponse() {
+ setBody(new byte[0]);
+ }
+
+ @Override public MockResponse clone() {
+ try {
+ MockResponse result = (MockResponse) super.clone();
+ result.headers = new ArrayList<String>(result.headers);
+ return result;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /** Returns the HTTP response line, such as "HTTP/1.1 200 OK". */
+ public String getStatus() {
+ return status;
+ }
+
+ public MockResponse setResponseCode(int code) {
+ this.status = "HTTP/1.1 " + code + " OK";
+ return this;
+ }
+
+ public MockResponse setStatus(String status) {
+ this.status = status;
+ return this;
+ }
+
+ /** Returns the HTTP headers, such as "Content-Length: 0". */
+ public List<String> getHeaders() {
+ return headers;
+ }
+
+ /**
+ * Removes all HTTP headers including any "Content-Length" and
+ * "Transfer-encoding" headers that were added by default.
+ */
+ public MockResponse clearHeaders() {
+ headers.clear();
+ return this;
+ }
+
+ /**
+ * Adds {@code header} as an HTTP header. For well-formed HTTP {@code header}
+ * should contain a name followed by a colon and a value.
+ */
+ public MockResponse addHeader(String header) {
+ headers.add(header);
+ return this;
+ }
+
+ /**
+ * Adds a new header with the name and value. This may be used to add multiple
+ * headers with the same name.
+ */
+ public MockResponse addHeader(String name, Object value) {
+ return addHeader(name + ": " + String.valueOf(value));
+ }
+
+ /**
+ * Removes all headers named {@code name}, then adds a new header with the
+ * name and value.
+ */
+ public MockResponse setHeader(String name, Object value) {
+ removeHeader(name);
+ return addHeader(name, value);
+ }
+
+ /** Removes all headers named {@code name}. */
+ public MockResponse removeHeader(String name) {
+ name += ":";
+ for (Iterator<String> i = headers.iterator(); i.hasNext(); ) {
+ String header = i.next();
+ if (name.regionMatches(true, 0, header, 0, name.length())) {
+ i.remove();
+ }
+ }
+ return this;
+ }
+
+ /** Returns the raw HTTP payload, or null if this response is streamed. */
+ public byte[] getBody() {
+ return body;
+ }
+
+ /** Returns an input stream containing the raw HTTP payload. */
+ InputStream getBodyStream() {
+ return bodyStream != null ? bodyStream : new ByteArrayInputStream(body);
+ }
+
+ public MockResponse setBody(byte[] body) {
+ setHeader("Content-Length", body.length);
+ this.body = body;
+ this.bodyStream = null;
+ return this;
+ }
+
+ public MockResponse setBody(InputStream bodyStream, long bodyLength) {
+ setHeader("Content-Length", bodyLength);
+ this.body = null;
+ this.bodyStream = bodyStream;
+ return this;
+ }
+
+ /** Sets the response body to the UTF-8 encoded bytes of {@code body}. */
+ public MockResponse setBody(String body) {
+ try {
+ return setBody(body.getBytes("UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Sets the response body to {@code body}, chunked every {@code maxChunkSize}
+ * bytes.
+ */
+ public MockResponse setChunkedBody(byte[] body, int maxChunkSize) {
+ removeHeader("Content-Length");
+ headers.add(CHUNKED_BODY_HEADER);
+
+ try {
+ ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+ int pos = 0;
+ while (pos < body.length) {
+ int chunkSize = Math.min(body.length - pos, maxChunkSize);
+ bytesOut.write(Integer.toHexString(chunkSize).getBytes(Util.US_ASCII));
+ bytesOut.write("\r\n".getBytes(Util.US_ASCII));
+ bytesOut.write(body, pos, chunkSize);
+ bytesOut.write("\r\n".getBytes(Util.US_ASCII));
+ pos += chunkSize;
+ }
+ bytesOut.write("0\r\n\r\n".getBytes(Util.US_ASCII)); // Last chunk + empty trailer + crlf.
+
+ this.body = bytesOut.toByteArray();
+ return this;
+ } catch (IOException e) {
+ throw new AssertionError(); // In-memory I/O doesn't throw IOExceptions.
+ }
+ }
+
+ /**
+ * Sets the response body to the UTF-8 encoded bytes of {@code body}, chunked
+ * every {@code maxChunkSize} bytes.
+ */
+ public MockResponse setChunkedBody(String body, int maxChunkSize) {
+ try {
+ return setChunkedBody(body.getBytes("UTF-8"), maxChunkSize);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+
+ public SocketPolicy getSocketPolicy() {
+ return socketPolicy;
+ }
+
+ public MockResponse setSocketPolicy(SocketPolicy socketPolicy) {
+ this.socketPolicy = socketPolicy;
+ return this;
+ }
+
+ public int getBytesPerSecond() {
+ return bytesPerSecond;
+ }
+
+ /**
+ * Set simulated network speed, in bytes per second. This applies to the
+ * response body only; response headers are not throttled.
+ */
+ public MockResponse setBytesPerSecond(int bytesPerSecond) {
+ this.bytesPerSecond = bytesPerSecond;
+ return this;
+ }
+
+ @Override public String toString() {
+ return status;
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
new file mode 100644
index 0000000..d036d53
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -0,0 +1,722 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
+import com.squareup.okhttp.internal.spdy.SpdyConnection;
+import com.squareup.okhttp.internal.spdy.SpdyStream;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
+
+/**
+ * A scriptable web server. Callers supply canned responses and the server
+ * replays them upon request in sequence.
+ */
+public final class MockWebServer {
+ private static final byte[] NPN_PROTOCOLS = {
+ // TODO: support HTTP/2.0.
+ // 17, 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', '2', '.', '0',
+ 6, 's', 'p', 'd', 'y', '/', '3',
+ 8, 'h', 't', 't', 'p', '/', '1', '.', '1'
+ };
+ private static final byte[] SPDY3 = new byte[] {
+ 's', 'p', 'd', 'y', '/', '3'
+ };
+ private static final byte[] HTTP_20_DRAFT_04 = new byte[] {
+ 'H', 'T', 'T', 'P', '-', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', '2', '.', '0'
+ };
+ private static final byte[] HTTP_11 = new byte[] {
+ 'h', 't', 't', 'p', '/', '1', '.', '1'
+ };
+
+ private static final X509TrustManager UNTRUSTED_TRUST_MANAGER = new X509TrustManager() {
+ @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ throw new CertificateException();
+ }
+
+ @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ throw new AssertionError();
+ }
+
+ @Override public X509Certificate[] getAcceptedIssuers() {
+ throw new AssertionError();
+ }
+ };
+
+ private static final Logger logger = Logger.getLogger(MockWebServer.class.getName());
+
+ private final BlockingQueue<RecordedRequest> requestQueue =
+ new LinkedBlockingQueue<RecordedRequest>();
+
+ /** All map values are Boolean.TRUE. (Collections.newSetFromMap isn't available in Froyo) */
+ private final Map<Socket, Boolean> openClientSockets = new ConcurrentHashMap<Socket, Boolean>();
+ private final Map<SpdyConnection, Boolean> openSpdyConnections
+ = new ConcurrentHashMap<SpdyConnection, Boolean>();
+ private final AtomicInteger requestCount = new AtomicInteger();
+ private int bodyLimit = Integer.MAX_VALUE;
+ private ServerSocket serverSocket;
+ private SSLSocketFactory sslSocketFactory;
+ private ExecutorService executor;
+ private boolean tunnelProxy;
+ private Dispatcher dispatcher = new QueueDispatcher();
+
+ private int port = -1;
+ private boolean npnEnabled = true;
+
+ public int getPort() {
+ if (port == -1) throw new IllegalStateException("Cannot retrieve port before calling play()");
+ return port;
+ }
+
+ public String getHostName() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public Proxy toProxyAddress() {
+ return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(getHostName(), getPort()));
+ }
+
+ /**
+ * Returns a URL for connecting to this server.
+ * @param path the request path, such as "/".
+ */
+ public URL getUrl(String path) {
+ try {
+ return sslSocketFactory != null
+ ? new URL("https://" + getHostName() + ":" + getPort() + path)
+ : new URL("http://" + getHostName() + ":" + getPort() + path);
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Returns a cookie domain for this server. This returns the server's
+ * non-loopback host name if it is known. Otherwise this returns ".local" for
+ * this server's loopback name.
+ */
+ public String getCookieDomain() {
+ String hostName = getHostName();
+ return hostName.contains(".") ? hostName : ".local";
+ }
+
+ /**
+ * Sets the number of bytes of the POST body to keep in memory to the given
+ * limit.
+ */
+ public void setBodyLimit(int maxBodyLength) {
+ this.bodyLimit = maxBodyLength;
+ }
+
+ /**
+ * Sets whether NPN is used on incoming HTTPS connections to negotiate a
+ * transport like HTTP/1.1 or SPDY/3. Call this method to disable NPN and
+ * SPDY.
+ */
+ public void setNpnEnabled(boolean npnEnabled) {
+ this.npnEnabled = npnEnabled;
+ }
+
+ /**
+ * Serve requests with HTTPS rather than otherwise.
+ * @param tunnelProxy true to expect the HTTP CONNECT method before
+ * negotiating TLS.
+ */
+ public void useHttps(SSLSocketFactory sslSocketFactory, boolean tunnelProxy) {
+ this.sslSocketFactory = sslSocketFactory;
+ this.tunnelProxy = tunnelProxy;
+ }
+
+ /**
+ * Awaits the next HTTP request, removes it, and returns it. Callers should
+ * use this to verify the request was sent as intended.
+ */
+ public RecordedRequest takeRequest() throws InterruptedException {
+ return requestQueue.take();
+ }
+
+ /**
+ * Returns the number of HTTP requests received thus far by this server. This
+ * may exceed the number of HTTP connections when connection reuse is in
+ * practice.
+ */
+ public int getRequestCount() {
+ return requestCount.get();
+ }
+
+ /**
+ * Scripts {@code response} to be returned to a request made in sequence. The
+ * first request is served by the first enqueued response; the second request
+ * by the second enqueued response; and so on.
+ *
+ * @throws ClassCastException if the default dispatcher has been replaced
+ * with {@link #setDispatcher(Dispatcher)}.
+ */
+ public void enqueue(MockResponse response) {
+ ((QueueDispatcher) dispatcher).enqueueResponse(response.clone());
+ }
+
+ /** Equivalent to {@code play(0)}. */
+ public void play() throws IOException {
+ play(0);
+ }
+
+ /**
+ * Starts the server, serves all enqueued requests, and shuts the server down.
+ *
+ * @param port the port to listen to, or 0 for any available port. Automated
+ * tests should always use port 0 to avoid flakiness when a specific port
+ * is unavailable.
+ */
+ public void play(int port) throws IOException {
+ if (executor != null) throw new IllegalStateException("play() already called");
+ executor = Executors.newCachedThreadPool();
+ serverSocket = new ServerSocket(port);
+ serverSocket.setReuseAddress(true);
+
+ this.port = serverSocket.getLocalPort();
+ executor.execute(namedRunnable("MockWebServer-accept-" + port, new Runnable() {
+ public void run() {
+ try {
+ acceptConnections();
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "MockWebServer connection failed", e);
+ }
+
+ // This gnarly block of code will release all sockets and all thread,
+ // even if any close fails.
+ Util.closeQuietly(serverSocket);
+ for (Iterator<Socket> s = openClientSockets.keySet().iterator(); s.hasNext(); ) {
+ Util.closeQuietly(s.next());
+ s.remove();
+ }
+ for (Iterator<SpdyConnection> s = openSpdyConnections.keySet().iterator(); s.hasNext(); ) {
+ Util.closeQuietly(s.next());
+ s.remove();
+ }
+ executor.shutdown();
+ }
+
+ private void acceptConnections() throws Exception {
+ while (true) {
+ Socket socket;
+ try {
+ socket = serverSocket.accept();
+ } catch (SocketException e) {
+ return;
+ }
+ SocketPolicy socketPolicy = dispatcher.peekSocketPolicy();
+ if (socketPolicy == DISCONNECT_AT_START) {
+ dispatchBookkeepingRequest(0, socket);
+ socket.close();
+ } else {
+ openClientSockets.put(socket, true);
+ serveConnection(socket);
+ }
+ }
+ }
+ }));
+ }
+
+ public void shutdown() throws IOException {
+ if (serverSocket != null) {
+ serverSocket.close(); // Should cause acceptConnections() to break out.
+ }
+ }
+
+ private void serveConnection(final Socket raw) {
+ String name = "MockWebServer-" + raw.getRemoteSocketAddress();
+ executor.execute(namedRunnable(name, new Runnable() {
+ int sequenceNumber = 0;
+
+ public void run() {
+ try {
+ processConnection();
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "MockWebServer connection failed", e);
+ }
+ }
+
+ public void processConnection() throws Exception {
+ Transport transport = Transport.HTTP_11;
+ Socket socket;
+ if (sslSocketFactory != null) {
+ if (tunnelProxy) {
+ createTunnel();
+ }
+ SocketPolicy socketPolicy = dispatcher.peekSocketPolicy();
+ if (socketPolicy == FAIL_HANDSHAKE) {
+ dispatchBookkeepingRequest(sequenceNumber, raw);
+ processHandshakeFailure(raw);
+ return;
+ }
+ socket = sslSocketFactory.createSocket(
+ raw, raw.getInetAddress().getHostAddress(), raw.getPort(), true);
+ SSLSocket sslSocket = (SSLSocket) socket;
+ sslSocket.setUseClientMode(false);
+ openClientSockets.put(socket, true);
+
+ if (npnEnabled) {
+ Platform.get().setNpnProtocols(sslSocket, NPN_PROTOCOLS);
+ }
+
+ sslSocket.startHandshake();
+
+ if (npnEnabled) {
+ byte[] selectedProtocol = Platform.get().getNpnSelectedProtocol(sslSocket);
+ if (selectedProtocol == null || Arrays.equals(selectedProtocol, HTTP_11)) {
+ transport = Transport.HTTP_11;
+ } else if (Arrays.equals(selectedProtocol, HTTP_20_DRAFT_04)) {
+ transport = Transport.HTTP_20_DRAFT_04;
+ } else if (Arrays.equals(selectedProtocol, SPDY3)) {
+ transport = Transport.SPDY_3;
+ } else {
+ throw new IllegalStateException(
+ "Unexpected transport: " + new String(selectedProtocol, Util.US_ASCII));
+ }
+ }
+ openClientSockets.remove(raw);
+ } else {
+ socket = raw;
+ }
+
+ if (transport == Transport.SPDY_3 || transport == Transport.HTTP_20_DRAFT_04) {
+ SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket, transport);
+ SpdyConnection.Builder builder = new SpdyConnection.Builder(false, socket)
+ .handler(spdySocketHandler);
+ if (transport == Transport.SPDY_3) {
+ builder.spdy3();
+ } else {
+ builder.http20Draft04();
+ }
+ SpdyConnection spdyConnection = builder.build();
+ openSpdyConnections.put(spdyConnection, Boolean.TRUE);
+ openClientSockets.remove(socket);
+ spdyConnection.readConnectionHeader();
+ return;
+ }
+
+ InputStream in = new BufferedInputStream(socket.getInputStream());
+ OutputStream out = new BufferedOutputStream(socket.getOutputStream());
+
+ while (processOneRequest(socket, in, out)) {
+ }
+
+ if (sequenceNumber == 0) {
+ logger.warning("MockWebServer connection didn't make a request");
+ }
+
+ in.close();
+ out.close();
+ socket.close();
+ openClientSockets.remove(socket);
+ }
+
+ /**
+ * Respond to CONNECT requests until a SWITCH_TO_SSL_AT_END response is
+ * dispatched.
+ */
+ private void createTunnel() throws IOException, InterruptedException {
+ while (true) {
+ SocketPolicy socketPolicy = dispatcher.peekSocketPolicy();
+ if (!processOneRequest(raw, raw.getInputStream(), raw.getOutputStream())) {
+ throw new IllegalStateException("Tunnel without any CONNECT!");
+ }
+ if (socketPolicy == SocketPolicy.UPGRADE_TO_SSL_AT_END) return;
+ }
+ }
+
+ /**
+ * Reads a request and writes its response. Returns true if a request was
+ * processed.
+ */
+ private boolean processOneRequest(Socket socket, InputStream in, OutputStream out)
+ throws IOException, InterruptedException {
+ RecordedRequest request = readRequest(socket, in, out, sequenceNumber);
+ if (request == null) return false;
+ requestCount.incrementAndGet();
+ requestQueue.add(request);
+ MockResponse response = dispatcher.dispatch(request);
+ writeResponse(out, response);
+ if (response.getSocketPolicy() == SocketPolicy.DISCONNECT_AT_END) {
+ in.close();
+ out.close();
+ } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_INPUT_AT_END) {
+ socket.shutdownInput();
+ } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_OUTPUT_AT_END) {
+ socket.shutdownOutput();
+ }
+ logger.info("Received request: " + request + " and responded: " + response);
+ sequenceNumber++;
+ return true;
+ }
+ }));
+ }
+
+ private void processHandshakeFailure(Socket raw) throws Exception {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, new TrustManager[] { UNTRUSTED_TRUST_MANAGER }, new SecureRandom());
+ SSLSocketFactory sslSocketFactory = context.getSocketFactory();
+ SSLSocket socket =
+ (SSLSocket) sslSocketFactory.createSocket(raw, raw.getInetAddress().getHostAddress(),
+ raw.getPort(), true);
+ try {
+ socket.startHandshake(); // we're testing a handshake failure
+ throw new AssertionError();
+ } catch (IOException expected) {
+ }
+ socket.close();
+ }
+
+ private void dispatchBookkeepingRequest(int sequenceNumber, Socket socket)
+ throws InterruptedException {
+ requestCount.incrementAndGet();
+ dispatcher.dispatch(new RecordedRequest(null, null, null, -1, null, sequenceNumber, socket));
+ }
+
+ /** @param sequenceNumber the index of this request on this connection. */
+ private RecordedRequest readRequest(Socket socket, InputStream in, OutputStream out,
+ int sequenceNumber) throws IOException {
+ String request;
+ try {
+ request = readAsciiUntilCrlf(in);
+ } catch (IOException streamIsClosed) {
+ return null; // no request because we closed the stream
+ }
+ if (request.length() == 0) {
+ return null; // no request because the stream is exhausted
+ }
+
+ List<String> headers = new ArrayList<String>();
+ long contentLength = -1;
+ boolean chunked = false;
+ boolean expectContinue = false;
+ String header;
+ while ((header = readAsciiUntilCrlf(in)).length() != 0) {
+ headers.add(header);
+ String lowercaseHeader = header.toLowerCase(Locale.US);
+ if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) {
+ contentLength = Long.parseLong(header.substring(15).trim());
+ }
+ if (lowercaseHeader.startsWith("transfer-encoding:")
+ && lowercaseHeader.substring(18).trim().equals("chunked")) {
+ chunked = true;
+ }
+ if (lowercaseHeader.startsWith("expect:")
+ && lowercaseHeader.substring(7).trim().equals("100-continue")) {
+ expectContinue = true;
+ }
+ }
+
+ if (expectContinue) {
+ out.write(("HTTP/1.1 100 Continue\r\n").getBytes(Util.US_ASCII));
+ out.write(("Content-Length: 0\r\n").getBytes(Util.US_ASCII));
+ out.write(("\r\n").getBytes(Util.US_ASCII));
+ out.flush();
+ }
+
+ boolean hasBody = false;
+ TruncatingOutputStream requestBody = new TruncatingOutputStream();
+ List<Integer> chunkSizes = new ArrayList<Integer>();
+ if (contentLength != -1) {
+ hasBody = true;
+ transfer(contentLength, in, requestBody);
+ } else if (chunked) {
+ hasBody = true;
+ while (true) {
+ int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16);
+ if (chunkSize == 0) {
+ readEmptyLine(in);
+ break;
+ }
+ chunkSizes.add(chunkSize);
+ transfer(chunkSize, in, requestBody);
+ readEmptyLine(in);
+ }
+ }
+
+ if (request.startsWith("OPTIONS ")
+ || request.startsWith("GET ")
+ || request.startsWith("HEAD ")
+ || request.startsWith("DELETE ")
+ || request.startsWith("TRACE ")
+ || request.startsWith("CONNECT ")) {
+ if (hasBody) {
+ throw new IllegalArgumentException("Request must not have a body: " + request);
+ }
+ } else if (!request.startsWith("POST ") && !request.startsWith("PUT ")) {
+ throw new UnsupportedOperationException("Unexpected method: " + request);
+ }
+
+ return new RecordedRequest(request, headers, chunkSizes, requestBody.numBytesReceived,
+ requestBody.toByteArray(), sequenceNumber, socket);
+ }
+
+ private void writeResponse(OutputStream out, MockResponse response) throws IOException {
+ out.write((response.getStatus() + "\r\n").getBytes(Util.US_ASCII));
+ for (String header : response.getHeaders()) {
+ out.write((header + "\r\n").getBytes(Util.US_ASCII));
+ }
+ out.write(("\r\n").getBytes(Util.US_ASCII));
+ out.flush();
+
+ InputStream in = response.getBodyStream();
+ if (in == null) return;
+ int bytesPerSecond = response.getBytesPerSecond();
+
+ // Stream data in MTU-sized increments, with a minimum of one packet per second.
+ byte[] buffer = bytesPerSecond >= 1452 ? new byte[1452] : new byte[bytesPerSecond];
+ long delayMs = bytesPerSecond == Integer.MAX_VALUE
+ ? 0
+ : (1000 * buffer.length) / bytesPerSecond;
+
+ int read;
+ long sinceDelay = 0;
+ while ((read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ out.flush();
+
+ sinceDelay += read;
+ if (sinceDelay >= buffer.length && delayMs > 0) {
+ sinceDelay %= buffer.length;
+ try {
+ Thread.sleep(delayMs);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ }
+ }
+
+ /**
+ * Transfer bytes from {@code in} to {@code out} until either {@code length}
+ * bytes have been transferred or {@code in} is exhausted.
+ */
+ private void transfer(long length, InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ while (length > 0) {
+ int count = in.read(buffer, 0, (int) Math.min(buffer.length, length));
+ if (count == -1) return;
+ out.write(buffer, 0, count);
+ length -= count;
+ }
+ }
+
+ /**
+ * Returns the text from {@code in} until the next "\r\n", or null if {@code
+ * in} is exhausted.
+ */
+ private String readAsciiUntilCrlf(InputStream in) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ while (true) {
+ int c = in.read();
+ if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') {
+ builder.deleteCharAt(builder.length() - 1);
+ return builder.toString();
+ } else if (c == -1) {
+ return builder.toString();
+ } else {
+ builder.append((char) c);
+ }
+ }
+ }
+
+ private void readEmptyLine(InputStream in) throws IOException {
+ String line = readAsciiUntilCrlf(in);
+ if (line.length() != 0) throw new IllegalStateException("Expected empty but was: " + line);
+ }
+
+ /**
+ * Sets the dispatcher used to match incoming requests to mock responses.
+ * The default dispatcher simply serves a fixed sequence of responses from
+ * a {@link #enqueue(MockResponse) queue}; custom dispatchers can vary the
+ * response based on timing or the content of the request.
+ */
+ public void setDispatcher(Dispatcher dispatcher) {
+ if (dispatcher == null) throw new NullPointerException();
+ this.dispatcher = dispatcher;
+ }
+
+ /** An output stream that drops data after bodyLimit bytes. */
+ private class TruncatingOutputStream extends ByteArrayOutputStream {
+ private long numBytesReceived = 0;
+
+ @Override public void write(byte[] buffer, int offset, int len) {
+ numBytesReceived += len;
+ super.write(buffer, offset, Math.min(len, bodyLimit - count));
+ }
+
+ @Override public void write(int oneByte) {
+ numBytesReceived++;
+ if (count < bodyLimit) {
+ super.write(oneByte);
+ }
+ }
+ }
+
+ private static Runnable namedRunnable(final String name, final Runnable runnable) {
+ return new Runnable() {
+ public void run() {
+ String originalName = Thread.currentThread().getName();
+ Thread.currentThread().setName(name);
+ try {
+ runnable.run();
+ } finally {
+ Thread.currentThread().setName(originalName);
+ }
+ }
+ };
+ }
+
+ /** Processes HTTP requests layered over SPDY/3. */
+ private class SpdySocketHandler implements IncomingStreamHandler {
+ private final Socket socket;
+ private final Transport transport;
+ private final AtomicInteger sequenceNumber = new AtomicInteger();
+
+ private SpdySocketHandler(Socket socket, Transport transport) {
+ this.socket = socket;
+ this.transport = transport;
+ }
+
+ @Override public void receive(SpdyStream stream) throws IOException {
+ RecordedRequest request = readRequest(stream);
+ requestQueue.add(request);
+ MockResponse response;
+ try {
+ response = dispatcher.dispatch(request);
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ writeResponse(stream, response);
+ logger.info("Received request: " + request + " and responded: " + response
+ + " transport is " + transport);
+ }
+
+ private RecordedRequest readRequest(SpdyStream stream) throws IOException {
+ List<String> spdyHeaders = stream.getRequestHeaders();
+ List<String> httpHeaders = new ArrayList<String>();
+ String method = "<:method omitted>";
+ String path = "<:path omitted>";
+ String version = "<:version omitted>";
+ for (Iterator<String> i = spdyHeaders.iterator(); i.hasNext(); ) {
+ String name = i.next();
+ String value = i.next();
+ if (":method".equals(name)) {
+ method = value;
+ } else if (":path".equals(name)) {
+ path = value;
+ } else if (":version".equals(name)) {
+ version = value;
+ } else {
+ httpHeaders.add(name + ": " + value);
+ }
+ }
+
+ InputStream bodyIn = stream.getInputStream();
+ ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = bodyIn.read(buffer)) != -1) {
+ bodyOut.write(buffer, 0, count);
+ }
+ bodyIn.close();
+ String requestLine = method + ' ' + path + ' ' + version;
+ List<Integer> chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY.
+ return new RecordedRequest(requestLine, httpHeaders, chunkSizes, bodyOut.size(),
+ bodyOut.toByteArray(), sequenceNumber.getAndIncrement(), socket);
+ }
+
+ private void writeResponse(SpdyStream stream, MockResponse response) throws IOException {
+ List<String> spdyHeaders = new ArrayList<String>();
+ String[] statusParts = response.getStatus().split(" ", 2);
+ if (statusParts.length != 2) {
+ throw new AssertionError("Unexpected status: " + response.getStatus());
+ }
+ spdyHeaders.add(":status");
+ spdyHeaders.add(statusParts[1]);
+ // TODO: no ":version" header for HTTP/2.0, only SPDY.
+ spdyHeaders.add(":version");
+ spdyHeaders.add(statusParts[0]);
+ for (String header : response.getHeaders()) {
+ String[] headerParts = header.split(":", 2);
+ if (headerParts.length != 2) {
+ throw new AssertionError("Unexpected header: " + header);
+ }
+ spdyHeaders.add(headerParts[0].toLowerCase(Locale.US).trim());
+ spdyHeaders.add(headerParts[1].trim());
+ }
+ byte[] body = response.getBody();
+ stream.reply(spdyHeaders, body.length > 0);
+ if (body.length > 0) {
+ stream.getOutputStream().write(body);
+ stream.getOutputStream().close();
+ }
+ }
+ }
+
+ enum Transport {
+ HTTP_11, SPDY_3, HTTP_20_DRAFT_04
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
new file mode 100644
index 0000000..0f0cb28
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+import java.net.HttpURLConnection;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Default dispatcher that processes a script of responses. Populate the script
+ * by calling {@link #enqueueResponse(MockResponse)}.
+ */
+public class QueueDispatcher extends Dispatcher {
+ protected final BlockingQueue<MockResponse> responseQueue
+ = new LinkedBlockingQueue<MockResponse>();
+ private MockResponse failFastResponse;
+
+ @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ // To permit interactive/browser testing, ignore requests for favicons.
+ final String requestLine = request.getRequestLine();
+ if (requestLine != null && requestLine.equals("GET /favicon.ico HTTP/1.1")) {
+ System.out.println("served " + requestLine);
+ return new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND);
+ }
+
+ if (failFastResponse != null && responseQueue.peek() == null) {
+ // Fail fast if there's no response queued up.
+ return failFastResponse;
+ }
+
+ return responseQueue.take();
+ }
+
+ @Override public SocketPolicy peekSocketPolicy() {
+ MockResponse peek = responseQueue.peek();
+ if (peek == null) {
+ return failFastResponse != null
+ ? failFastResponse.getSocketPolicy()
+ : SocketPolicy.KEEP_OPEN;
+ }
+ return peek.getSocketPolicy();
+ }
+
+ public void enqueueResponse(MockResponse response) {
+ responseQueue.add(response);
+ }
+
+ public void setFailFast(boolean failFast) {
+ MockResponse failFastResponse = failFast
+ ? new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
+ : null;
+ setFailFast(failFastResponse);
+ }
+
+ public void setFailFast(MockResponse failFastResponse) {
+ this.failFastResponse = failFastResponse;
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
new file mode 100644
index 0000000..aceacd1
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+import java.io.UnsupportedEncodingException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.SSLSocket;
+
+/** An HTTP request that came into the mock web server. */
+public final class RecordedRequest {
+ private final String requestLine;
+ private final String method;
+ private final String path;
+ private final List<String> headers;
+ private final List<Integer> chunkSizes;
+ private final long bodySize;
+ private final byte[] body;
+ private final int sequenceNumber;
+ private final String sslProtocol;
+
+ public RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes,
+ long bodySize, byte[] body, int sequenceNumber, Socket socket) {
+ this.requestLine = requestLine;
+ this.headers = headers;
+ this.chunkSizes = chunkSizes;
+ this.bodySize = bodySize;
+ this.body = body;
+ this.sequenceNumber = sequenceNumber;
+ this.sslProtocol = socket instanceof SSLSocket
+ ? ((SSLSocket) socket).getSession().getProtocol()
+ : null;
+
+ if (requestLine != null) {
+ int methodEnd = requestLine.indexOf(' ');
+ int pathEnd = requestLine.indexOf(' ', methodEnd + 1);
+ this.method = requestLine.substring(0, methodEnd);
+ this.path = requestLine.substring(methodEnd + 1, pathEnd);
+ } else {
+ this.method = null;
+ this.path = null;
+ }
+ }
+
+ public String getRequestLine() {
+ return requestLine;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ /** Returns all headers. */
+ public List<String> getHeaders() {
+ return headers;
+ }
+
+ /**
+ * Returns the first header named {@code name}, or null if no such header
+ * exists.
+ */
+ public String getHeader(String name) {
+ name += ":";
+ for (String header : headers) {
+ if (name.regionMatches(true, 0, header, 0, name.length())) {
+ return header.substring(name.length()).trim();
+ }
+ }
+ return null;
+ }
+
+ /** Returns the headers named {@code name}. */
+ public List<String> getHeaders(String name) {
+ List<String> result = new ArrayList<String>();
+ name += ":";
+ for (String header : headers) {
+ if (name.regionMatches(true, 0, header, 0, name.length())) {
+ result.add(header.substring(name.length()).trim());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the sizes of the chunks of this request's body, or an empty list
+ * if the request's body was empty or unchunked.
+ */
+ public List<Integer> getChunkSizes() {
+ return chunkSizes;
+ }
+
+ /**
+ * Returns the total size of the body of this POST request (before
+ * truncation).
+ */
+ public long getBodySize() {
+ return bodySize;
+ }
+
+ /** Returns the body of this POST request. This may be truncated. */
+ public byte[] getBody() {
+ return body;
+ }
+
+ /** Returns the body of this POST request decoded as a UTF-8 string. */
+ public String getUtf8Body() {
+ try {
+ return new String(body, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Returns the index of this request on its HTTP connection. Since a single
+ * HTTP connection may serve multiple requests, each request is assigned its
+ * own sequence number.
+ */
+ public int getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ /**
+ * Returns the connection's SSL protocol like {@code TLSv1}, {@code SSLv3},
+ * {@code NONE} or null if the connection doesn't use SSL.
+ */
+ public String getSslProtocol() {
+ return sslProtocol;
+ }
+
+ @Override public String toString() {
+ return requestLine;
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
new file mode 100644
index 0000000..7912f3a
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+/** What should be done with the incoming socket. */
+public enum SocketPolicy {
+
+ /**
+ * Keep the socket open after the response. This is the default HTTP/1.1
+ * behavior.
+ */
+ KEEP_OPEN,
+
+ /**
+ * Close the socket after the response. This is the default HTTP/1.0
+ * behavior.
+ */
+ DISCONNECT_AT_END,
+
+ /**
+ * Wrap the socket with SSL at the completion of this request/response pair.
+ * Used for CONNECT messages to tunnel SSL over an HTTP proxy.
+ */
+ UPGRADE_TO_SSL_AT_END,
+
+ /**
+ * Request immediate close of connection without even reading the request. Use
+ * to simulate buggy SSL servers closing connections in response to
+ * unrecognized TLS extensions.
+ */
+ DISCONNECT_AT_START,
+
+ /** Don't trust the client during the SSL handshake. */
+ FAIL_HANDSHAKE,
+
+ /**
+ * Shutdown the socket input after sending the response. For testing bad
+ * behavior.
+ */
+ SHUTDOWN_INPUT_AT_END,
+
+ /**
+ * Shutdown the socket output after sending the response. For testing bad
+ * behavior.
+ */
+ SHUTDOWN_OUTPUT_AT_END
+}
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
new file mode 100644
index 0000000..22e6a95
--- /dev/null
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CustomDispatcherTest extends TestCase {
+
+ private MockWebServer mockWebServer = new MockWebServer();
+
+ @Override
+ public void tearDown() throws Exception {
+ mockWebServer.shutdown();
+ }
+
+ public void testSimpleDispatch() throws Exception {
+ mockWebServer.play();
+ final List<RecordedRequest> requestsMade = new ArrayList<RecordedRequest>();
+ final Dispatcher dispatcher = new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ requestsMade.add(request);
+ return new MockResponse();
+ }
+ };
+ assertEquals(0, requestsMade.size());
+ mockWebServer.setDispatcher(dispatcher);
+ final URL url = mockWebServer.getUrl("/");
+ final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.getResponseCode(); // Force the connection to hit the "server".
+ // Make sure our dispatcher got the request.
+ assertEquals(1, requestsMade.size());
+ }
+
+ public void testOutOfOrderResponses() throws Exception {
+ AtomicInteger firstResponseCode = new AtomicInteger();
+ AtomicInteger secondResponseCode = new AtomicInteger();
+ mockWebServer.play();
+ final String secondRequest = "/bar";
+ final String firstRequest = "/foo";
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Dispatcher dispatcher = new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ if (request.getPath().equals(firstRequest)) {
+ latch.await();
+ }
+ return new MockResponse();
+ }
+ };
+ mockWebServer.setDispatcher(dispatcher);
+ final Thread startsFirst = buildRequestThread(firstRequest, firstResponseCode);
+ startsFirst.start();
+ final Thread endsFirst = buildRequestThread(secondRequest, secondResponseCode);
+ endsFirst.start();
+ endsFirst.join();
+ assertEquals(0, firstResponseCode.get()); // First response is still waiting.
+ assertEquals(200, secondResponseCode.get()); // Second response is done.
+ latch.countDown();
+ startsFirst.join();
+ assertEquals(200, firstResponseCode.get()); // And now it's done!
+ assertEquals(200, secondResponseCode.get()); // (Still done).
+ }
+
+ private Thread buildRequestThread(final String path, final AtomicInteger responseCode) {
+ return new Thread(new Runnable() {
+ @Override public void run() {
+ final URL url = mockWebServer.getUrl(path);
+ final HttpURLConnection conn;
+ try {
+ conn = (HttpURLConnection) url.openConnection();
+ responseCode.set(conn.getResponseCode()); // Force the connection to hit the "server".
+ } catch (IOException e) {
+ }
+ }
+ });
+ }
+
+}
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
new file mode 100644
index 0000000..98efc44
--- /dev/null
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2011 Google Inc.
+ *
+ * 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.squareup.okhttp.mockwebserver;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import junit.framework.TestCase;
+
+public final class MockWebServerTest extends TestCase {
+
+ private MockWebServer server = new MockWebServer();
+
+ @Override protected void tearDown() throws Exception {
+ server.shutdown();
+ super.tearDown();
+ }
+
+ public void testRecordedRequestAccessors() {
+ List<String> headers = Arrays.asList(
+ "User-Agent: okhttp",
+ "Cookie: s=square",
+ "Cookie: a=android",
+ "X-Whitespace: left",
+ "X-Whitespace:right ",
+ "X-Whitespace: both "
+ );
+ List<Integer> chunkSizes = Collections.emptyList();
+ byte[] body = {'A', 'B', 'C'};
+ String requestLine = "GET / HTTP/1.1";
+ RecordedRequest request = new RecordedRequest(
+ requestLine, headers, chunkSizes, body.length, body, 0, null);
+ assertEquals("s=square", request.getHeader("cookie"));
+ assertEquals(Arrays.asList("s=square", "a=android"), request.getHeaders("cookie"));
+ assertEquals("left", request.getHeader("x-whitespace"));
+ assertEquals(Arrays.asList("left", "right", "both"), request.getHeaders("x-whitespace"));
+ assertEquals("ABC", request.getUtf8Body());
+ }
+
+ public void testDefaultMockResponse() {
+ MockResponse response = new MockResponse();
+ assertEquals(Arrays.asList("Content-Length: 0"), response.getHeaders());
+ assertEquals("HTTP/1.1 200 OK", response.getStatus());
+ }
+
+ public void testSetBodyAdjustsHeaders() throws IOException {
+ MockResponse response = new MockResponse().setBody("ABC");
+ assertEquals(Arrays.asList("Content-Length: 3"), response.getHeaders());
+ InputStream in = response.getBodyStream();
+ assertEquals('A', in.read());
+ assertEquals('B', in.read());
+ assertEquals('C', in.read());
+ assertEquals(-1, in.read());
+ assertEquals("HTTP/1.1 200 OK", response.getStatus());
+ }
+
+ public void testMockResponseAddHeader() {
+ MockResponse response = new MockResponse()
+ .clearHeaders()
+ .addHeader("Cookie: s=square")
+ .addHeader("Cookie", "a=android");
+ assertEquals(Arrays.asList("Cookie: s=square", "Cookie: a=android"),
+ response.getHeaders());
+ }
+
+ public void testMockResponseSetHeader() {
+ MockResponse response = new MockResponse()
+ .clearHeaders()
+ .addHeader("Cookie: s=square")
+ .addHeader("Cookie: a=android")
+ .addHeader("Cookies: delicious");
+ response.setHeader("cookie", "r=robot");
+ assertEquals(Arrays.asList("Cookies: delicious", "cookie: r=robot"),
+ response.getHeaders());
+ }
+
+ /**
+ * Clients who adhere to <a
+ * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3">100
+ * Status</a> expect the server to send an interim response with status code
+ * 100 before they send their payload.
+ *
+ * <h4>Note</h4>
+ *
+ * JRE 6 only passes this test if
+ * {@code -Dsun.net.http.allowRestrictedHeaders=true} is set.
+ */
+ public void testExpect100ContinueWithBody() throws Exception {
+ server.enqueue(new MockResponse());
+ server.play();
+
+ URL url = server.getUrl("/");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("PUT");
+ connection.setAllowUserInteraction(false);
+ connection.setRequestProperty("Expect", "100-continue");
+ connection.setDoOutput(true);
+ connection.getOutputStream().write("hello".getBytes());
+ assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
+
+ assertEquals(server.getRequestCount(), 1);
+ RecordedRequest request = server.takeRequest();
+ assertEquals(request.getRequestLine(), "PUT / HTTP/1.1");
+ assertEquals("5", request.getHeader("Content-Length"));
+ assertEquals(5, request.getBodySize());
+ assertEquals("hello", new String(request.getBody()));
+ // below fails on JRE 6 unless -Dsun.net.http.allowRestrictedHeaders=true is set
+ assertEquals("100-continue", request.getHeader("Expect"));
+ }
+
+ public void testExpect100ContinueWithNoBody() throws Exception {
+ server.enqueue(new MockResponse());
+ server.play();
+
+ URL url = server.getUrl("/");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("PUT");
+ connection.setAllowUserInteraction(false);
+ connection.setRequestProperty("Expect", "100-continue");
+ connection.setRequestProperty("Content-Length", "0");
+ connection.setDoOutput(true);
+ connection.setFixedLengthStreamingMode(0);
+ assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
+
+ assertEquals(server.getRequestCount(), 1);
+ RecordedRequest request = server.takeRequest();
+ assertEquals(request.getRequestLine(), "PUT / HTTP/1.1");
+ assertEquals("0", request.getHeader("Content-Length"));
+ assertEquals(0, request.getBodySize());
+ // below fails on JRE 6 unless -Dsun.net.http.allowRestrictedHeaders=true is set
+ assertEquals("100-continue", request.getHeader("Expect"));
+ }
+
+ public void testRegularResponse() throws Exception {
+ server.enqueue(new MockResponse().setBody("hello world"));
+ server.play();
+
+ URL url = server.getUrl("/");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestProperty("Accept-Language", "en-US");
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
+ assertEquals("hello world", reader.readLine());
+
+ RecordedRequest request = server.takeRequest();
+ assertEquals("GET / HTTP/1.1", request.getRequestLine());
+ assertTrue(request.getHeaders().contains("Accept-Language: en-US"));
+ }
+
+ public void testRedirect() throws Exception {
+ server.play();
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ .addHeader("Location: " + server.getUrl("/new-path"))
+ .setBody("This page has moved!"));
+ server.enqueue(new MockResponse().setBody("This is the new location!"));
+
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals("This is the new location!", reader.readLine());
+
+ RecordedRequest first = server.takeRequest();
+ assertEquals("GET / HTTP/1.1", first.getRequestLine());
+ RecordedRequest redirect = server.takeRequest();
+ assertEquals("GET /new-path HTTP/1.1", redirect.getRequestLine());
+ }
+
+ /**
+ * Test that MockWebServer blocks for a call to enqueue() if a request
+ * is made before a mock response is ready.
+ */
+ public void testDispatchBlocksWaitingForEnqueue() throws Exception {
+ server.play();
+
+ new Thread() {
+ @Override public void run() {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignored) {
+ }
+ server.enqueue(new MockResponse().setBody("enqueued in the background"));
+ }
+ }.start();
+
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals("enqueued in the background", reader.readLine());
+ }
+
+ public void testNonHexadecimalChunkSize() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n")
+ .clearHeaders()
+ .addHeader("Transfer-encoding: chunked"));
+ server.play();
+
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ try {
+ in.read();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public void testResponseTimeout() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("ABC")
+ .clearHeaders()
+ .addHeader("Content-Length: 4"));
+ server.enqueue(new MockResponse()
+ .setBody("DEF"));
+ server.play();
+
+ URLConnection urlConnection = server.getUrl("/").openConnection();
+ urlConnection.setReadTimeout(1000);
+ InputStream in = urlConnection.getInputStream();
+ assertEquals('A', in.read());
+ assertEquals('B', in.read());
+ assertEquals('C', in.read());
+ try {
+ in.read(); // if Content-Length was accurate, this would return -1 immediately
+ fail();
+ } catch (SocketTimeoutException expected) {
+ }
+
+ URLConnection urlConnection2 = server.getUrl("/").openConnection();
+ InputStream in2 = urlConnection2.getInputStream();
+ assertEquals('D', in2.read());
+ assertEquals('E', in2.read());
+ assertEquals('F', in2.read());
+ assertEquals(-1, in2.read());
+
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ public void testDisconnectAtStart() throws Exception {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse()); // The jdk's HttpUrlConnection is a bastard.
+ server.enqueue(new MockResponse());
+ server.play();
+ try {
+ server.getUrl("/a").openConnection().getInputStream();
+ } catch (IOException e) {
+ // Expected.
+ }
+ server.getUrl("/b").openConnection().getInputStream(); // Should succeed.
+ }
+
+ public void testStreamingResponseBody() throws Exception {
+ InputStream responseBody = new ByteArrayInputStream("ABC".getBytes("UTF-8"));
+ server.enqueue(new MockResponse().setBody(responseBody, 3));
+ server.play();
+
+ InputStream in = server.getUrl("/").openConnection().getInputStream();
+ assertEquals('A', in.read());
+ assertEquals('B', in.read());
+ assertEquals('C', in.read());
+
+ assertEquals(-1, responseBody.read()); // The body is exhausted.
+ }
+}