/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* 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.
* #L%
*/
package org.wisdom.framework.vertx;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.wisdom.api.Controller;
import org.wisdom.api.DefaultController;
import org.wisdom.api.configuration.ApplicationConfiguration;
import org.wisdom.api.content.ContentEngine;
import org.wisdom.api.exceptions.ExceptionMapper;
import org.wisdom.api.http.*;
import org.wisdom.api.router.Route;
import org.wisdom.api.router.RouteBuilder;
import org.wisdom.api.router.Router;
import org.wisdom.framework.vertx.file.DiskFileUpload;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Check that we file upload works.
*/
public class FileUploadTest extends VertxBaseTest {
private WisdomVertxServer server;
@After
public void tearDown() {
if (server != null) {
server.stop();
server = null;
}
}
@Test
public void testFileUploadOfSmallFiles() throws InterruptedException, IOException {
// Prepare the configuration
ApplicationConfiguration configuration = mock(ApplicationConfiguration.class);
when(configuration.getIntegerWithDefault(eq("vertx.http.port"), anyInt())).thenReturn(0);
when(configuration.getIntegerWithDefault(eq("vertx.https.port"), anyInt())).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.acceptBacklog", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.receiveBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.sendBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("request.body.max.size", 100 * 1024)).thenReturn(100 * 1024);
when(configuration.getLongWithDefault("http.upload.disk.threshold", DiskFileUpload.MINSIZE)).thenReturn
(DiskFileUpload.MINSIZE);
when(configuration.getLongWithDefault("http.upload.max", -1l)).thenReturn(-1l);
when(configuration.getStringArray("wisdom.websocket.subprotocols")).thenReturn(new String[0]);
when(configuration.getStringArray("vertx.websocket-subprotocols")).thenReturn(new String[0]);
// Prepare the router with a controller
Controller controller = new DefaultController() {
@SuppressWarnings("unused")
public Result index() throws IOException {
FileItem item = context().file("upload");
if (!item.isInMemory()) {
return badRequest("In memory expected");
}
if (!item.name().equals("my-file.dat")) {
return badRequest("broken name");
}
if (item.size() != 2048) {
return badRequest("broken file");
}
if (!context().form().get("comment").get(0).equals("my description")) {
return badRequest("broken form");
}
final File file = item.toFile();
if (! file.exists() && file.length() != 2048) {
return badRequest("broken in memory to file handling");
}
return ok(item.stream()).as(MimeTypes.BINARY);
}
};
Router router = mock(Router.class);
Route route = new RouteBuilder().route(HttpMethod.POST)
.on("/")
.to(controller, "index");
when(router.getRouteFor(anyString(), anyString(), any(Request.class))).thenReturn(route);
ContentEngine contentEngine = getMockContentEngine();
// Configure the server.
server = new WisdomVertxServer();
server.accessor = new ServiceAccessor(
null,
configuration,
router,
contentEngine,
executor,
null,
Collections.<ExceptionMapper>emptyList()
);
server.configuration = configuration;
server.vertx = vertx;
server.start();
VertxHttpServerTest.waitForStart(server);
// Now start bunch of clients
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(NUMBER_OF_CLIENTS);
int port = server.httpPort();
for (int i = 1; i < NUMBER_OF_CLIENTS + 1; ++i) {
// create and start threads
clients.execute(new Client(startSignal, doneSignal, port, i, 2048));
}
startSignal.countDown(); // let all threads proceed
if (!doneSignal.await(60, TimeUnit.SECONDS)) { // wait for all to finish
Assert.fail("testFileUploadOfSmallFiles - Client not served in time");
}
assertThat(failure).isEmpty();
assertThat(success).hasSize(NUMBER_OF_CLIENTS);
}
@Test
public void testFileUploadOfSmallFilesOnDisk() throws InterruptedException, IOException {
// Prepare the configuration
ApplicationConfiguration configuration = mock(ApplicationConfiguration.class);
when(configuration.getIntegerWithDefault(eq("vertx.http.port"), anyInt())).thenReturn(0);
when(configuration.getIntegerWithDefault(eq("vertx.https.port"), anyInt())).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.acceptBacklog", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.receiveBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.sendBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("request.body.max.size", 100 * 1024)).thenReturn(100 * 1024);
when(configuration.getStringArray("wisdom.websocket.subprotocols")).thenReturn(new String[0]);
when(configuration.getStringArray("vertx.websocket-subprotocols")).thenReturn(new String[0]);
// Reduce it to force disk storage
when(configuration.getLongWithDefault("http.upload.disk.threshold", DiskFileUpload.MINSIZE)).thenReturn
(1024l);
when(configuration.getLongWithDefault("http.upload.max", -1l)).thenReturn(-1l);
// Prepare the router with a controller
Controller controller = new DefaultController() {
@SuppressWarnings("unused")
public Result index() throws IOException {
FileItem item = context().file("upload");
if (item.isInMemory()) {
return badRequest("on disk expected");
}
if (!item.name().equals("my-file.dat")) {
return badRequest("broken name");
}
if (item.size() != 2048) {
return badRequest("broken file");
}
if (!context().form().get("comment").get(0).equals("my description")) {
return badRequest("broken form");
}
final File file = item.toFile();
if (! file.exists() && file.length() != 2048) {
return badRequest("broken file to file handling");
}
return ok(item.stream()).as(MimeTypes.BINARY);
}
};
Router router = mock(Router.class);
Route route = new RouteBuilder().route(HttpMethod.POST)
.on("/")
.to(controller, "index");
when(router.getRouteFor(anyString(), anyString(), any(Request.class))).thenReturn(route);
ContentEngine contentEngine = getMockContentEngine();
// Configure the server.
server = new WisdomVertxServer();
server.accessor = new ServiceAccessor(
null,
configuration,
router,
contentEngine,
executor,
null,
Collections.<ExceptionMapper>emptyList()
);
server.vertx = vertx;
server.configuration = configuration;
server.start();
VertxHttpServerTest.waitForStart(server);
// Now start bunch of clients
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(NUMBER_OF_CLIENTS);
int port = server.httpPort();
for (int i = 1; i < NUMBER_OF_CLIENTS + 1; ++i) {
clients.submit(new Client(startSignal, doneSignal, port, i, 2048));
}
startSignal.countDown(); // let all threads proceed
if (!doneSignal.await(120, TimeUnit.SECONDS)) { // wait for all to finish
Assert.fail("testFileUploadOfSmallFilesOnDisk - Did not server all requests in time");
}
assertThat(failure).isEmpty();
assertThat(success).hasSize(NUMBER_OF_CLIENTS);
}
@Test
public void testFileUploadOfSmallFilesWithAsyncDownload() throws InterruptedException, IOException {
// Prepare the configuration
ApplicationConfiguration configuration = mock(ApplicationConfiguration.class);
when(configuration.getIntegerWithDefault(eq("vertx.http.port"), anyInt())).thenReturn(0);
when(configuration.getIntegerWithDefault(eq("vertx.https.port"), anyInt())).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.acceptBacklog", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.receiveBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.sendBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("request.body.max.size", 100 * 1024)).thenReturn(100 * 1024);
when(configuration.getLongWithDefault("http.upload.disk.threshold", DiskFileUpload.MINSIZE)).thenReturn
(DiskFileUpload.MINSIZE);
when(configuration.getLongWithDefault("http.upload.max", -1l)).thenReturn(-1l);
when(configuration.getStringArray("wisdom.websocket.subprotocols")).thenReturn(new String[0]);
when(configuration.getStringArray("vertx.websocket-subprotocols")).thenReturn(new String[0]);
// Prepare the router with a controller
Controller controller = new DefaultController() {
@SuppressWarnings("unused")
public Result index() {
final FileItem item = context().file("upload");
if (!item.isInMemory()) {
return badRequest("In memory expected");
}
if (!item.name().equals("my-file.dat")) {
return badRequest("broken name");
}
if (item.size() != 2048) {
return badRequest("broken file");
}
if (!context().form().get("comment").get(0).equals("my description")) {
return badRequest("broken form");
}
return async(new Callable<Result>() {
@Override
public Result call() throws Exception {
return ok(item.stream()).as(MimeTypes.BINARY);
}
});
}
};
Router router = mock(Router.class);
Route route = new RouteBuilder().route(HttpMethod.POST)
.on("/")
.to(controller, "index");
when(router.getRouteFor(anyString(), anyString(), any(Request.class))).thenReturn(route);
ContentEngine contentEngine = getMockContentEngine();
// Configure the server.
server = new WisdomVertxServer();
server.accessor = new ServiceAccessor(
null,
configuration,
router,
contentEngine,
executor,
null,
Collections.<ExceptionMapper>emptyList()
);
server.configuration = configuration;
server.vertx = vertx;
server.start();
VertxHttpServerTest.waitForStart(server);
// Now start bunch of clients
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(NUMBER_OF_CLIENTS);
int port = server.httpPort();
for (int i = 1; i < NUMBER_OF_CLIENTS + 1; ++i) // create and start threads
clients.submit(new Client(startSignal, doneSignal, port, i, 2048));
startSignal.countDown(); // let all threads proceed
if (!doneSignal.await(60, TimeUnit.SECONDS)) { // wait for all to finish
Assert.fail("testFileUploadOfSmallFilesWithAsyncDownload - Client not served in time");
}
assertThat(failure).isEmpty();
assertThat(success).hasSize(NUMBER_OF_CLIENTS);
}
@Test
public void testThatFileUpdateFailedWhenTheFileExceedTheConfiguredSize() throws InterruptedException, IOException {
// Prepare the configuration
ApplicationConfiguration configuration = mock(ApplicationConfiguration.class);
when(configuration.getIntegerWithDefault(eq("vertx.http.port"), anyInt())).thenReturn(0);
when(configuration.getIntegerWithDefault(eq("vertx.https.port"), anyInt())).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.acceptBacklog", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.receiveBufferSize", -1)).thenReturn(-1);
when(configuration.getIntegerWithDefault("vertx.sendBufferSize", -1)).thenReturn(-1);
when(configuration.getLongWithDefault("http.upload.disk.threshold", DiskFileUpload.MINSIZE)).thenReturn
(DiskFileUpload.MINSIZE);
when(configuration.getLongWithDefault("http.upload.max", -1l)).thenReturn(10l); // 10 bytes max
when(configuration.getStringArray("wisdom.websocket.subprotocols")).thenReturn(new String[0]);
when(configuration.getStringArray("vertx.websocket-subprotocols")).thenReturn(new String[0]);
// Prepare the router with a controller
Controller controller = new DefaultController() {
@SuppressWarnings("unused")
public Result index() {
return ok();
}
};
Router router = mock(Router.class);
Route route = new RouteBuilder().route(HttpMethod.POST)
.on("/")
.to(controller, "index");
when(router.getRouteFor(anyString(), anyString(), any(Request.class))).thenReturn(route);
ContentEngine contentEngine = getMockContentEngine();
// Configure the server.
server = new WisdomVertxServer();
server.accessor = new ServiceAccessor(
null,
configuration,
router,
contentEngine,
executor,
null,
Collections.<ExceptionMapper>emptyList()
);
server.configuration = configuration;
server.vertx = vertx;
server.start();
VertxHttpServerTest.waitForStart(server);
int port = server.httpPort();
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpPost post = new HttpPost("http://localhost:" + port + "/?id=" + 1);
ByteArrayBody body = new ByteArrayBody("this is too much...".getBytes(), "my-file.dat");
StringBody description = new StringBody("my description", ContentType.TEXT_PLAIN);
HttpEntity entity = MultipartEntityBuilder.create()
.addPart("upload", body)
.addPart("comment", description)
.build();
post.setEntity(entity);
CloseableHttpResponse response = httpclient.execute(post);
// We should receive a Payload too large response (413)
assertThat(response.getStatusLine().getStatusCode()).isEqualTo(413);
}
private class Client implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
private final int port;
private final int id;
private final int size;
Client(CountDownLatch startSignal, CountDownLatch doneSignal, int port, int id, int size) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
this.port = port;
this.id = id;
this.size = size;
}
public void run() {
try {
startSignal.await();
doWork();
} catch (Throwable ex) {
fail(id);
} finally {
doneSignal.countDown();
}
}
void doWork() throws IOException {
final byte[] data = new byte[size];
RANDOM.nextBytes(data);
CloseableHttpClient httpclient = null;
CloseableHttpResponse response = null;
try {
httpclient = HttpClients.createDefault();
HttpPost post = new HttpPost("http://localhost:" + port + "/?id=" + id);
ByteArrayBody body = new ByteArrayBody(data, "my-file.dat");
StringBody description = new StringBody("my description", ContentType.TEXT_PLAIN);
HttpEntity entity = MultipartEntityBuilder.create()
.addPart("upload", body)
.addPart("comment", description)
.build();
post.setEntity(entity);
response = httpclient.execute(post);
byte[] content = EntityUtils.toByteArray(response.getEntity());
if (!isOk(response)) {
System.err.println("Invalid response code for " + id + " got " + response.getStatusLine()
.getStatusCode() + " " + new String(content));
fail(id);
return;
}
if (!containsExactly(content, data)) {
System.err.println("Invalid content for " + id);
fail(id);
return;
}
success(id);
} finally {
IOUtils.closeQuietly(httpclient);
IOUtils.closeQuietly(response);
}
}
}
}