package com.vtence.molecule.servers;
import com.vtence.molecule.BodyPart;
import com.vtence.molecule.Server;
import com.vtence.molecule.http.HttpStatus;
import com.vtence.molecule.support.StackTrace;
import com.vtence.molecule.testing.ResourceLocator;
import com.vtence.molecule.testing.http.Form;
import com.vtence.molecule.testing.http.HttpRequest;
import com.vtence.molecule.testing.http.HttpResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.vtence.molecule.http.HttpStatus.CREATED;
import static com.vtence.molecule.ssl.KeyStoreType.DEFAULT;
import static com.vtence.molecule.ssl.SecureProtocol.TLS;
import static com.vtence.molecule.testing.ResourceLocator.locateOnClasspath;
import static com.vtence.molecule.testing.http.HttpResponseAssert.assertThat;
import static java.lang.String.valueOf;
import static java.util.concurrent.CompletableFuture.runAsync;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.fail;
public abstract class ServerCompatibilityTests {
int port = 9999;
Server server;
ResourceLocator resources = ResourceLocator.onClasspath();
HttpRequest request = new HttpRequest(port);
HttpResponse response;
Throwable error;
@Before public void
configureServer() {
server = createServer("localhost", port);
server.reportErrorsTo(error -> ServerCompatibilityTests.this.error = error);
}
protected abstract Server createServer(String host, int port);
@After public void
stopServer() throws Exception {
server.shutdown();
}
@Test public void
knowsItsHost() throws IOException {
assertThat("host", server.host(), equalTo("localhost"));
}
@Test public void
knowsItsPort() throws IOException {
assertThat("port", server.port(), equalTo(9999));
}
@Test public void
notifiesReportersOfFailures() throws IOException {
server.run((request, response) -> {
throw new Exception("Crash!");
});
request.send();
assertThat("error", error, notNullValue());
assertThat("error message", error.getMessage(), equalTo("Crash!"));
}
@Test public void
notifiesReportersOfErrorsOccurringAsync() throws IOException {
server.run((request, response) -> response.done(new Exception("Crash!")));
request.send();
assertThat("error", error, notNullValue());
assertThat("error message", error.getMessage(), equalTo("Crash!"));
}
@Test public void
respondsToRequests() throws IOException {
server.run((request, response) -> response.status(CREATED).done());
response = request.send();
assertNoError();
assertThat(response).hasStatusCode(201)
.hasStatusMessage("Created");
}
@Test public void
respondsToRequestsAsynchronously() throws IOException {
server.run((request, response) -> runAsync(() -> response.status(CREATED).done()));
response = request.send();
assertNoError();
assertThat(response).hasStatusCode(201)
.hasStatusMessage("Created");
}
@Test public void
doesNotChunkResponsesWithContentLengthHeader() throws IOException {
server.run((request, response) -> {
response.contentLength(16);
response.body("<html>...</html>");
response.done();
});
response = request.send();
assertNoError();
assertThat(response).hasBodyText("<html>...</html>")
.hasHeader("Content-Length", "16")
.isNotChunked();
}
@Test public void
encodesResponsesAccordingToContentType() throws IOException {
server.run((request, response) -> {
response.contentType("text/plain; charset=utf-16");
response.body("This content requires encoding &âçüè!");
response.status(HttpStatus.OK);
response.done();
});
response = request.send();
assertNoError();
assertThat(response).isOK()
.hasContentEncodedAs(containsString("UTF-16"));
}
@SuppressWarnings("unchecked")
@Test public void
providesGeneralRequestInformation() throws IOException {
final Map<String, String> info = new HashMap<>();
server.run((request, response) -> {
info.put("uri", request.uri());
info.put("path", request.path());
info.put("query", request.query());
info.put("scheme", request.scheme());
info.put("server-host", request.serverHost());
info.put("server-port", valueOf(request.serverPort()));
info.put("remote-ip", request.remoteIp());
info.put("remote-host", request.remoteHost());
info.put("remote-port", valueOf(request.remotePort()));
info.put("protocol", request.protocol());
info.put("secure", valueOf(request.secure()));
info.put("timestamp", valueOf(request.timestamp()));
response.done();
});
request.get("/over/there?name=ferret");
assertNoError();
assertThat("request information", info, allOf(
hasEntry("uri", "/over/there?name=ferret"),
hasEntry("path", "/over/there"),
hasEntry("query", "name=ferret"),
hasEntry("scheme", "http"),
hasEntry("server-host", "localhost"),
hasEntry("server-port", "9999"),
hasEntry("remote-ip", "127.0.0.1"),
hasEntry("remote-host", "localhost"),
hasEntry(equalTo("remote-port"), not(equalTo("0"))),
hasEntry(equalTo("timestamp"), not(equalTo("0"))),
hasEntry("protocol", "HTTP/1.1"),
hasEntry("secure", "false")));
}
@SuppressWarnings("unchecked")
@Test public void
readsRequestHeaders() throws IOException {
final Map<String, Iterable<String>> headers = new HashMap<>();
server.run((request, response) -> {
headers.put("names", request.headerNames());
headers.put("encoding", request.headers("Accept-Encoding"));
response.done();
});
request.header("Accept", "text/html")
.header("Accept-Encoding", "gzip", "identity; q=0.5", "deflate;q=1.0", "*;q=0")
.send();
assertNoError();
assertThat("header names", headers.get("names"), hasItems("Accept", "Accept-Encoding"));
assertThat("accept-encoding", headers.get("encoding"),
contains("gzip", "identity; q=0.5", "deflate;q=1.0", "*;q=0"));
}
@Test public void
writesHeadersWithMultipleValues() throws IOException {
server.run((request, response) -> {
response.addHeader("Cache-Control", "no-cache");
response.addHeader("Cache-Control", "no-store");
response.done();
});
response = request.send();
assertNoError();
assertThat("response headers", response.headers("Cache-Control"), hasItems("no-cache", "no-store"));
}
@SuppressWarnings("unchecked")
@Test public void
readsRequestContent() throws IOException {
final Map<String, String> content = new HashMap<>();
server.run((request, response) -> {
content.put("contentType", valueOf(request.contentType()));
content.put("contentLength", valueOf(request.contentLength()));
content.put("body", request.body());
response.done();
});
request.contentType("application/json")
.body("{\"name\": \"value\"}")
.post("/uri");
assertNoError();
assertThat("request content", content, allOf(hasEntry("contentType", "application/json"),
hasEntry("contentLength", "17"),
hasEntry("body", "{\"name\": \"value\"}")));
}
@Test public void
readsQueryParameters() throws IOException {
final Map<String, String> parameters = new HashMap<>();
server.run((request, response) -> {
for (String name : request.parameterNames()) {
parameters.put(name, request.parameter(name));
}
response.done();
});
request.get("/?param1=value1¶m2=value2");
assertNoError();
assertThat("query parameters", parameters, allOf(hasEntry("param1", "value1"),
hasEntry("param2", "value2")));
}
@Test public void
supportsMultipleQueryParametersWithSameName() throws IOException {
server.run((request, response) -> response.body(request.parameters("names").toString()).done());
response = request.get("/?names=Alice&names=Bob&names=Charles");
assertNoError();
assertThat(response).hasBodyText("[Alice, Bob, Charles]");
}
@SuppressWarnings("unchecked")
@Test public void
readsFormEncodedParameters() throws IOException {
final Map<String, String> parameters = new HashMap<>();
server.run((request, response) -> {
for (String name : request.parameterNames()) {
parameters.put(name, request.parameter(name));
}
response.done();
});
response = request.content(Form.urlEncoded()
.addField("param1", "value1")
.addField("param2", "value2"))
.post("/");
assertNoError();
assertThat("form parameters", parameters, allOf(hasEntry("param1", "value1"),
hasEntry("param2", "value2")));
}
@Test public void
supportsMultipleFormEncodedParametersWithSameName() throws IOException {
server.run((request, response) -> response.body(request.parameters("name").toString()).done());
response = request.content(Form.urlEncoded()
.addField("name", "Alice")
.addField("name", "Bob")
.addField("name", "Charles"))
.post("/");
assertNoError();
assertThat(response).hasBodyText("[Alice, Bob, Charles]");
}
@Test public void
readsMultiPartFormParameters() throws IOException {
final Map<String, String> parameters = new HashMap<>();
server.run((request, response) -> {
List<BodyPart> parts = request.parts();
for (BodyPart part : parts) {
parameters.put(part.name(), part.value());
}
response.done();
});
response = request.content(Form.multipart()
.addField("param1", "value1")
.addField("param2", "value2"))
.post("/");
assertNoError();
assertThat("form data parameters", parameters, allOf(hasEntry("param1", "value1"),
hasEntry("param2", "value2")));
}
@Test public void
downloadsUploadedFiles() throws IOException {
final Map<String, Integer> files = new HashMap<>();
final Map<String, String> mimeTypes = new HashMap<>();
server.run((request, response) -> {
List<BodyPart> parts = request.parts();
for (BodyPart part : parts) {
files.put(part.filename(), part.content().length);
mimeTypes.put(part.filename(), part.contentType());
}
response.done();
});
response = request.content(Form.multipart()
.addBinaryFile("file", resources.locate("assets/images/minion.png")))
.post("/");
assertNoError();
assertThat("filenames", files, hasEntry("minion.png", 21134));
assertThat("mime types", mimeTypes, hasEntry("minion.png", "image/png"));
}
@Test public void
supportsHttps() throws Exception {
SSLContext sslContext =
TLS.initialize(DEFAULT.loadKeys(locateOnClasspath("ssl/keystore"), "password", "password"));
final Map<String, String> info = new HashMap<>();
server.run((request, response) -> {
info.put("scheme", request.scheme());
info.put("secure", valueOf(request.secure()));
response.done();
}, sslContext);
response = request.secure(true).get("/");
assertNoError();
assertThat("request information", info, allOf(
hasEntry("scheme", "https"),
hasEntry("secure", "true")));
}
protected void assertNoError() {
if (error != null) fail(StackTrace.of(error));
}
}