dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/AssetServletTest.java
package io.dropwizard.servlets.assets;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.servlet.ServletTester;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import static org.assertj.core.api.Assertions.assertThat;
public class AssetServletTest {
private static final String DUMMY_SERVLET = "/dummy_servlet/";
private static final String NOINDEX_SERVLET = "/noindex_servlet/";
private static final String NOCHARSET_SERVLET = "/nocharset_servlet/";
private static final String NOMEDIATYPE_SERVLET = "/nomediatype_servlet/";
private static final String MEDIATYPE_SERVLET = "/mediatype_servlet/";
private static final String ROOT_SERVLET = "/";
private static final String RESOURCE_PATH = "/assets";
// ServletTester expects to be able to instantiate the servlet with zero arguments
public static class DummyAssetServlet extends AssetServlet {
private static final long serialVersionUID = -1L;
public DummyAssetServlet() {
super(RESOURCE_PATH, DUMMY_SERVLET, "index.htm", StandardCharsets.UTF_8);
}
}
public static class NoIndexAssetServlet extends AssetServlet {
private static final long serialVersionUID = -1L;
public NoIndexAssetServlet() {
super(RESOURCE_PATH, DUMMY_SERVLET, null, StandardCharsets.UTF_8);
}
}
public static class RootAssetServlet extends AssetServlet {
private static final long serialVersionUID = 1L;
public RootAssetServlet() {
super("/", ROOT_SERVLET, null, StandardCharsets.UTF_8);
}
}
public static class NoCharsetAssetServlet extends AssetServlet {
private static final long serialVersionUID = 1L;
public NoCharsetAssetServlet() {
super(RESOURCE_PATH, NOCHARSET_SERVLET, null, null);
}
}
public static class NoDefaultMediaTypeAssetServlet extends AssetServlet {
private static final long serialVersionUID = 1L;
public NoDefaultMediaTypeAssetServlet() {
super(RESOURCE_PATH, NOMEDIATYPE_SERVLET, null, null, StandardCharsets.UTF_8);
}
}
public static class DefaultMediaTypeAssetServlet extends AssetServlet {
private static final long serialVersionUID = 1L;
public DefaultMediaTypeAssetServlet() {
super(RESOURCE_PATH, MEDIATYPE_SERVLET, null, "text/plain", StandardCharsets.UTF_8);
}
}
private static final ServletTester SERVLET_TESTER = new ServletTester();
private final HttpTester.Request request = HttpTester.newRequest();
@Nullable
private HttpTester.Response response;
@BeforeAll
public static void startServletTester() throws Exception {
SERVLET_TESTER.addServlet(DummyAssetServlet.class, DUMMY_SERVLET + '*');
SERVLET_TESTER.addServlet(NoIndexAssetServlet.class, NOINDEX_SERVLET + '*');
SERVLET_TESTER.addServlet(NoCharsetAssetServlet.class, NOCHARSET_SERVLET + '*');
SERVLET_TESTER.addServlet(NoDefaultMediaTypeAssetServlet.class, NOMEDIATYPE_SERVLET + '*');
SERVLET_TESTER.addServlet(DefaultMediaTypeAssetServlet.class, MEDIATYPE_SERVLET + '*');
SERVLET_TESTER.addServlet(RootAssetServlet.class, ROOT_SERVLET + '*');
SERVLET_TESTER.start();
SERVLET_TESTER.getContext().getMimeTypes().addMimeMapping("mp4", "video/mp4");
SERVLET_TESTER.getContext().getMimeTypes().addMimeMapping("m4a", "audio/mp4");
}
@AfterAll
public static void stopServletTester() throws Exception {
SERVLET_TESTER.stop();
}
@BeforeEach
void setUp() throws Exception {
request.setMethod("GET");
request.setURI(DUMMY_SERVLET + "example.txt");
request.setVersion(HttpVersion.HTTP_1_0);
}
@Test
void servesFilesMappedToRoot() throws Exception {
request.setURI(ROOT_SERVLET + "assets/example.txt");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContent())
.isEqualTo("HELLO THERE");
}
@Test
void servesCharset() throws Exception {
request.setURI(DUMMY_SERVLET + "example.txt");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
.isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8);
request.setURI(NOCHARSET_SERVLET + "example.txt");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.get(HttpHeader.CONTENT_TYPE))
.isEqualTo(MimeTypes.Type.TEXT_PLAIN.toString());
}
@Test
void servesFilesFromRootsWithSameName() throws Exception {
request.setURI(DUMMY_SERVLET + "example2.txt");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContent())
.isEqualTo("HELLO THERE 2");
}
@Test
void cacheIfModifiedSinceOverwrittenByIfNoneMatch() throws Exception{
request.setHeader(HttpHeader.IF_MODIFIED_SINCE.toString(), "Sat, 05 Nov 1955 22:57:05 GMT");
request.setURI(DUMMY_SERVLET + "index.htm");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
String eTag = response.get(HttpHeader.ETAG);
// If-None-Match should override If-Modified-Since
request.setHeader(HttpHeader.IF_NONE_MATCH.toString(), eTag);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(304);
}
@Test
void servesFilesWithA200() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContent())
.isEqualTo("HELLO THERE");
}
@Test
void throws404IfTheAssetIsMissing() throws Exception {
request.setURI(DUMMY_SERVLET + "doesnotexist.txt");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(404);
}
@Test
void consistentlyAssignsETags() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final String firstEtag = response.get(HttpHeader.ETAG);
final int numRequests = 1000;
final List<Future<String>> futures = new ArrayList<>(numRequests);
final CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < numRequests; i++) {
futures.add(ForkJoinPool.commonPool().submit(() -> {
final ByteBuffer req = request.generate();
latch.await(); // Attempt to start multiple requests at the same time
final HttpTester.Response resp = HttpTester.parseResponse(SERVLET_TESTER.getResponses(req));
return resp.get(HttpHeader.ETAG);
}));
}
latch.countDown();
final Set<String> eTags = new HashSet<>();
for (Future<String> future : futures) {
eTags.add(future.get());
}
assertThat(eTags)
.describedAs("eTag generation should be consistent with concurrent requests")
.hasSize(1);
assertThat(firstEtag)
.isEqualTo("\"e7bd7e8e\"")
.isEqualTo(eTags.iterator().next());
}
@Test
void assignsDifferentETagsForDifferentFiles() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final String firstEtag = response.get(HttpHeader.ETAG);
request.setURI(DUMMY_SERVLET + "foo.bar");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final String secondEtag = response.get(HttpHeader.ETAG);
assertThat(firstEtag)
.isEqualTo("\"e7bd7e8e\"");
assertThat(secondEtag)
.isEqualTo("\"2684fb5a\"");
}
@Test
void supportsIfNoneMatchRequests() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final String correctEtag = response.get(HttpHeader.ETAG);
request.setHeader(HttpHeader.IF_NONE_MATCH.asString(), correctEtag);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final int statusWithMatchingEtag = response.getStatus();
request.setHeader(HttpHeader.IF_NONE_MATCH.asString(), correctEtag + "FOO");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final int statusWithNonMatchingEtag = response.getStatus();
assertThat(statusWithMatchingEtag)
.isEqualTo(304);
assertThat(statusWithNonMatchingEtag)
.isEqualTo(200);
}
@Test
void consistentlyAssignsLastModifiedTimes() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final long firstLastModifiedTime = response.getDateField(HttpHeader.LAST_MODIFIED.asString());
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final long secondLastModifiedTime = response.getDateField(HttpHeader.LAST_MODIFIED.asString());
assertThat(firstLastModifiedTime)
.isEqualTo(secondLastModifiedTime);
}
@Test
void supportsByteRangeForMedia() throws Exception {
request.setURI(ROOT_SERVLET + "assets/foo.mp4");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
request.setURI(ROOT_SERVLET + "assets/foo.m4a");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
}
@Test
void supportsFullByteRange() throws Exception {
request.setURI(ROOT_SERVLET + "assets/example.txt");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=0-");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(206);
assertThat(response.getContent()).isEqualTo("HELLO THERE");
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
assertThat(response.get(HttpHeader.CONTENT_RANGE)).isEqualTo(
"bytes 0-10/11");
}
@Test
void supportsCentralByteRange() throws Exception {
request.setURI(ROOT_SERVLET + "assets/example.txt");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=4-8");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(206);
assertThat(response.getContent()).isEqualTo("O THE");
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
assertThat(response.get(HttpHeader.CONTENT_RANGE)).isEqualTo(
"bytes 4-8/11");
assertThat(response.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("5");
}
@Test
void supportsFinalByteRange() throws Exception {
request.setURI(ROOT_SERVLET + "assets/example.txt");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=10-10");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(206);
assertThat(response.getContent()).isEqualTo("E");
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
assertThat(response.get(HttpHeader.CONTENT_RANGE)).isEqualTo(
"bytes 10-10/11");
assertThat(response.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("1");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=-1");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(206);
assertThat(response.getContent()).isEqualTo("E");
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
assertThat(response.get(HttpHeader.CONTENT_RANGE)).isEqualTo(
"bytes 10-10/11");
assertThat(response.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("1");
}
@Test
void rejectsInvalidByteRanges() throws Exception {
request.setURI(ROOT_SERVLET + "assets/example.txt");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=test");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(416);
request.setHeader(HttpHeader.RANGE.asString(), "bytes=");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(416);
request.setHeader(HttpHeader.RANGE.asString(), "bytes=1-infinity");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(416);
request.setHeader(HttpHeader.RANGE.asString(), "test");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(416);
}
@Test
void supportsMultipleByteRanges() throws Exception {
request.setURI(ROOT_SERVLET + "assets/example.txt");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=0-0,-1");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(206);
assertThat(response.getContent()).isEqualTo("HE");
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
assertThat(response.get(HttpHeader.CONTENT_RANGE)).isEqualTo(
"bytes 0-0,10-10/11");
assertThat(response.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("2");
request.setHeader(HttpHeader.RANGE.asString(), "bytes=5-6,7-10");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
assertThat(response.getStatus()).isEqualTo(206);
assertThat(response.getContent()).isEqualTo(" THERE");
assertThat(response.get(HttpHeader.ACCEPT_RANGES)).isEqualTo("bytes");
assertThat(response.get(HttpHeader.CONTENT_RANGE)).isEqualTo(
"bytes 5-6,7-10/11");
assertThat(response.get(HttpHeader.CONTENT_LENGTH)).isEqualTo("6");
}
@Test
void supportsIfRangeMatchRequests() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
final String correctEtag = response.get(HttpHeader.ETAG);
request.setHeader(HttpHeader.RANGE.asString(), "bytes=10-10");
request.setHeader(HttpHeader.IF_RANGE.asString(), correctEtag);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
final int statusWithMatchingEtag = response.getStatus();
request.setHeader(HttpHeader.IF_RANGE.asString(), correctEtag + "FOO");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request
.generate()));
final int statusWithNonMatchingEtag = response.getStatus();
assertThat(statusWithMatchingEtag).isEqualTo(206);
assertThat(statusWithNonMatchingEtag).isEqualTo(200);
}
@Test
void supportsIfModifiedSinceRequests() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final long lastModifiedTime = response.getDateField(HttpHeader.LAST_MODIFIED.asString());
request.putDateField(HttpHeader.IF_MODIFIED_SINCE, lastModifiedTime);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final int statusWithMatchingLastModifiedTime = response.getStatus();
request.putDateField(HttpHeader.IF_MODIFIED_SINCE,
lastModifiedTime - 100);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final int statusWithStaleLastModifiedTime = response.getStatus();
request.putDateField(HttpHeader.IF_MODIFIED_SINCE,
lastModifiedTime + 100);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
final int statusWithRecentLastModifiedTime = response.getStatus();
assertThat(statusWithMatchingLastModifiedTime)
.isEqualTo(304);
assertThat(statusWithStaleLastModifiedTime)
.isEqualTo(200);
assertThat(statusWithRecentLastModifiedTime)
.isEqualTo(304);
}
@Test
void guessesMimeTypes() throws Exception {
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
.isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8);
}
@Test
void defaultsToHtml() throws Exception {
request.setURI(DUMMY_SERVLET + "foo.bar");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
.isEqualTo(MimeTypes.Type.TEXT_HTML_UTF_8);
}
@Test
void defaultsToHtmlIfNotOverridden() throws Exception {
request.setURI(NOMEDIATYPE_SERVLET + "foo.bar");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
.isEqualTo(MimeTypes.Type.TEXT_HTML_UTF_8);
}
@Test
void servesWithDefaultMediaType() throws Exception {
request.setURI(MEDIATYPE_SERVLET + "foo.bar");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
.isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8);
}
@Test
void servesIndexFilesByDefault() throws Exception {
// Root directory listing:
request.setURI(DUMMY_SERVLET);
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContent())
.contains("/assets Index File");
// Subdirectory listing:
request.setURI(DUMMY_SERVLET + "some_directory");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContent())
.contains("/assets/some_directory Index File");
// Subdirectory listing with slash:
request.setURI(DUMMY_SERVLET + "some_directory/");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
assertThat(response.getContent())
.contains("/assets/some_directory Index File");
}
@Test
void throwsA404IfNoIndexFileIsDefined() throws Exception {
// Root directory listing:
request.setURI(NOINDEX_SERVLET + '/');
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(404);
// Subdirectory listing:
request.setURI(NOINDEX_SERVLET + "some_directory");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(404);
// Subdirectory listing with slash:
request.setURI(NOINDEX_SERVLET + "some_directory/");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(404);
}
@Test
void doesNotAllowOverridingUrls() throws Exception {
request.setURI(DUMMY_SERVLET + "file:/etc/passwd");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(404);
}
@Test
void doesNotAllowOverridingPaths() throws Exception {
request.setURI(DUMMY_SERVLET + "/etc/passwd");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(404);
}
@Test
void allowsEncodedAssetNames() throws Exception {
request.setURI(DUMMY_SERVLET + "encoded%20example.txt");
response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate()));
assertThat(response.getStatus())
.isEqualTo(200);
}
}