blob: 7b94bdf2cfe50d9d9fbaa934e9cc72e77964ac34 [file] [log] [blame]
/*
* Copyright (c) 2016 Manumitting Technologies Inc and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Manumitting Technologies Inc - initial API and implementation
*/
package org.eclipse.userstorage.oauth;
import static org.hamcrest.CoreMatchers.startsWith;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
import org.eclipse.userstorage.internal.util.StringUtil;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
/**
* A little HTTP server that is programmed with a sequence of responses. The
* requests are matched using Hamcrest {@link Matcher}s.
*/
public class MockServer {
private static boolean DEBUG = Boolean.getBoolean("org.eclipse.userstorage.session.debug");
private static final SimpleDateFormat rfc1123Format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
private String host;
private int port;
private ServerSocket serverSocket;
private Thread serverThread;
private List<Matcher<? extends String>> expectedRequests = new ArrayList<Matcher<? extends String>>();
private List<String[]> expectedResponses = new ArrayList<String[]>();
private volatile boolean success = true;
private volatile int handledRequests = 0;
/**
* Create with a random port.
*/
public MockServer() {
this(null, -1);
}
/**
* Create with a fixed port. If {@code port ≤ 0} then allocated with a
* random port.
*/
public MockServer(int port) {
this(null, port);
}
/**
* Create a server that accepts on a particular interface. If
* {@code port ≤ 0} then allocated with a random port.
*/
public MockServer(String host, int port) {
this.host = host;
this.port = port;
}
/**
* Providing the next request matches {@code requestMatcher}, send the
* provided response.
*/
public MockServer expect(Matcher<? extends String> requestMatcher, String header) {
return expect(requestMatcher, new String[] { header });
}
/**
* Providing the next request matches {@code requestMatcher}, send the
* provided response.
*/
public MockServer expect(Matcher<? extends String> requestMatcher, String[] headerAndResponse) {
if (headerAndResponse.length == 0 || headerAndResponse.length > 2) {
throw new IllegalArgumentException("response is misshaped");
}
try {
if (headerAndResponse.length > 1 && headerAndResponse[1] != null
&& headerAndResponse[1].getBytes(StringUtil.UTF8).length != headerAndResponse[1].length()) {
throw new IllegalArgumentException("response is multibyte");
}
} catch (UnsupportedEncodingException ex) {
throw new IllegalArgumentException(ex);
}
expectedRequests.add(requestMatcher);
expectedResponses.add(headerAndResponse);
return this;
}
public MockServer start() throws IOException {
SocketAddress endpoint = null;
if (port > 0) {
endpoint = new InetSocketAddress(host, port);
}
serverSocket = new ServerSocket();
serverSocket.bind(endpoint);
// serverSocket.setSoTimeout(5000 /* ms */);
serverThread = new Thread(new Runnable() {
@Override
public void run() {
listen();
}
});
serverThread.start();
return this;
}
public void stop() {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
warn("MockServer: exception closing server socket: " + e);
}
}
}
public boolean isOK() {
return success;
}
public URI getURI() {
try {
InetAddress address = serverSocket.getInetAddress();
return new URI("http", null, address.isAnyLocalAddress() ? "localhost" : address.getHostAddress(),
serverSocket.getLocalPort(), null, null, null);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
private void listen() {
for (handledRequests = 0; expectedRequests.isEmpty()
|| handledRequests < expectedRequests.size(); handledRequests++) {
Socket socket = null;
InputStreamReader reader = null;
OutputStreamWriter writer = null;
try {
socket = serverSocket.accept();
reader = new InputStreamReader(socket.getInputStream(), StringUtil.UTF8);
writer = new OutputStreamWriter(socket.getOutputStream(), StringUtil.UTF8);
handleIncomingRequest(handledRequests, reader, writer);
} catch (IOException e) {
if (!serverSocket.isClosed()) {
warn("MockServer: exception occurred: " + e);
e.printStackTrace();
}
} finally {
close(writer);
close(reader);
close(socket);
}
}
try {
serverSocket.close();
} catch (IOException e) {
warn(e.toString());
}
}
private void handleIncomingRequest(int requestIndex, InputStreamReader reader, OutputStreamWriter writer) {
String request = readRequest(reader);
Matcher<? extends String> matcher = expectedRequests.get(requestIndex);
success &= matcher.matches(request);
try {
if (success) {
String[] response = expectedResponses.get(requestIndex);
debug("MockServer: " + request.split("\n")[0] + " >>> " + response[0].split("\n")[0]);
writer.append(response[0].trim());
writer.append("\nDate: " + rfc1123Format.format(new Date()));
if (response.length == 2 && response[1] != null && response[1].length() > 0) {
// this is ok as response should not be multi-byte
writer.append("\nContent-Length: " + response[1].length());
writer.append("\n\n");
writer.append(response[1]);
} else {
writer.append("\n\n");
}
} else {
debug("MockServer: REQUEST MISMATCH: " + request.split("\n")[0]);
StringDescription description = new StringDescription();
description.appendText("Looking for: ");
matcher.describeTo(description);
description.appendText("\nMismatch:\n");
matcher.describeMismatch(request, description);
description.appendText("\n");
writer.write("HTTP/1.1 500 Server Error\n");
writer.write("Content-Type: text/plain\n");
writer.write("Content-Length: ");
writer.write(Integer.toString(description.toString().length()));
writer.write("\n\n");
writer.write(description.toString());
writer.flush();
}
} catch (IOException e) {
warn("MockServer: exception writing response: " + e);
}
}
public int getHandledRequests() {
return handledRequests;
}
/** Assumes we don't have binary content */
private String readRequest(InputStreamReader reader) {
StringBuilder sb = new StringBuilder();
try {
BufferedReader br = new BufferedReader(reader);
String line;
int contentLength = -1;
while ((line = br.readLine()) != null && !line.isEmpty()) {
if (line.startsWith("Content-Length:")) {
try {
contentLength = Integer.parseInt(line.split(": *")[1]);
} catch (NumberFormatException e) {
warn("MockServer: invalid Content-Length: " + line);
}
}
sb.append(line).append('\n');
}
if (contentLength > 0) {
sb.append('\n');
char[] buffer = new char[1024];
int len;
while (contentLength > 0 && (len = br.read(buffer, 0, Math.min(contentLength, buffer.length))) > 0) {
sb.append(buffer, 0, len);
contentLength -= len;
}
}
} catch (IOException e) {
warn("MockServer: exception reading request: " + e);
}
return sb.toString();
}
private void close(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
warn("MockServer: Error closing object: " + e);
}
}
private void close(Socket closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
warn("MockServer: Error closing object: " + e);
}
}
private void debug(String text) {
if (DEBUG) {
System.out.println(text);
}
}
private void warn(String text) {
if (DEBUG) {
System.err.println(text);
}
}
public static Matcher<String> isGet() {
return startsWith("GET ");
}
public static Matcher<String> isGet(String path) {
return startsWith("GET " + path);
}
public static Matcher<String> isPost() {
return startsWith("POST ");
}
public static Matcher<String> isPost(String path) {
return startsWith("POST " + path);
}
public static Matcher<String> isPut() {
return startsWith("PUT ");
}
public static Matcher<String> isPut(String path) {
return startsWith("PUT " + path);
}
public static Matcher<String> hasQueryParameter(String paramName, String paramValue) {
return RegexMatcher.matches("^\\p{Alpha}+ \\S*[?&]" + paramName + "=" + paramValue + ".*",
Pattern.MULTILINE | Pattern.DOTALL);
}
public static Matcher<? super String> hasHeader(String headerName, String headerValue) {
return RegexMatcher.matches(".*\n" + headerName + ": " + headerValue + ".*",
Pattern.MULTILINE | Pattern.DOTALL);
}
public static Matcher<String> hasBodyParameter(String paramName, String paramValue) {
return RegexMatcher.matches(".*\n\n(\\S*[?&])?" + paramName + "=" + paramValue + ".*",
Pattern.MULTILINE | Pattern.DOTALL);
}
}