| // Copyright (C) 2015 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.google.gerrit.httpd.raw; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; |
| import static javax.servlet.http.HttpServletResponse.SC_OK; |
| |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Strings; |
| import com.google.common.cache.Cache; |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.collect.Lists; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.jimfs.Configuration; |
| import com.google.common.jimfs.Jimfs; |
| import com.google.gerrit.httpd.raw.ResourceServlet.Resource; |
| import com.google.gerrit.util.http.testutil.FakeHttpServletRequest; |
| import com.google.gerrit.util.http.testutil.FakeHttpServletResponse; |
| import java.io.ByteArrayInputStream; |
| import java.io.InputStream; |
| import java.nio.file.FileSystem; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.attribute.FileTime; |
| import java.time.LocalDateTime; |
| import java.time.Month; |
| import java.time.ZoneOffset; |
| import java.util.Locale; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.zip.GZIPInputStream; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| public class ResourceServletTest { |
| private static Cache<Path, Resource> newCache(int size) { |
| return CacheBuilder.newBuilder().maximumSize(size).recordStats().build(); |
| } |
| |
| private static class Servlet extends ResourceServlet { |
| private static final long serialVersionUID = 1L; |
| |
| private final FileSystem fs; |
| |
| private Servlet(FileSystem fs, Cache<Path, Resource> cache, boolean refresh) { |
| super(cache, refresh); |
| this.fs = fs; |
| } |
| |
| private Servlet( |
| FileSystem fs, Cache<Path, Resource> cache, boolean refresh, boolean cacheOnClient) { |
| super(cache, refresh, cacheOnClient); |
| this.fs = fs; |
| } |
| |
| private Servlet( |
| FileSystem fs, Cache<Path, Resource> cache, boolean refresh, int cacheFileSizeLimitBytes) { |
| super(cache, refresh, true, cacheFileSizeLimitBytes); |
| this.fs = fs; |
| } |
| |
| @Override |
| protected Path getResourcePath(String pathInfo) { |
| return fs.getPath("/" + CharMatcher.is('/').trimLeadingFrom(pathInfo)); |
| } |
| } |
| |
| private FileSystem fs; |
| private AtomicLong ts; |
| |
| @Before |
| public void setUp() { |
| fs = Jimfs.newFileSystem(Configuration.unix()); |
| ts = |
| new AtomicLong( |
| LocalDateTime.of(2010, Month.JANUARY, 30, 12, 0, 0) |
| .atOffset(ZoneOffset.ofHours(-8)) |
| .toInstant() |
| .toEpochMilli()); |
| } |
| |
| @Test |
| public void notFoundWithoutRefresh() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, false); |
| |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/notfound"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND); |
| assertNotCacheable(res); |
| assertCacheHits(cache, 0, 1); |
| |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/notfound"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND); |
| assertNotCacheable(res); |
| assertCacheHits(cache, 1, 1); |
| } |
| |
| @Test |
| public void notFoundWithRefresh() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, true); |
| |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/notfound"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND); |
| assertNotCacheable(res); |
| assertCacheHits(cache, 0, 1); |
| |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/notfound"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_NOT_FOUND); |
| assertNotCacheable(res); |
| assertCacheHits(cache, 1, 1); |
| } |
| |
| @Test |
| public void smallFileWithRefresh() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, true); |
| |
| writeFile("/foo", "foo1"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertCacheable(res, true); |
| assertHasETag(res); |
| // Miss on getIfPresent, miss on get. |
| assertCacheHits(cache, 0, 2); |
| |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertCacheable(res, true); |
| assertHasETag(res); |
| assertCacheHits(cache, 1, 2); |
| |
| writeFile("/foo", "foo2"); |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo2"); |
| assertCacheable(res, true); |
| assertHasETag(res); |
| // Hit, invalidate, miss. |
| assertCacheHits(cache, 2, 3); |
| } |
| |
| @Test |
| public void smallFileWithoutClientCache() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, false, false); |
| |
| writeFile("/foo", "foo1"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertNotCacheable(res); |
| |
| // Miss on getIfPresent, miss on get. |
| assertCacheHits(cache, 0, 2); |
| |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertNotCacheable(res); |
| assertCacheHits(cache, 1, 2); |
| |
| writeFile("/foo", "foo2"); |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertNotCacheable(res); |
| assertCacheHits(cache, 2, 2); |
| } |
| |
| @Test |
| public void smallFileWithoutRefresh() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, false); |
| |
| writeFile("/foo", "foo1"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertCacheable(res, false); |
| assertHasETag(res); |
| // Miss on getIfPresent, miss on get. |
| assertCacheHits(cache, 0, 2); |
| |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertCacheable(res, false); |
| assertHasETag(res); |
| assertCacheHits(cache, 1, 2); |
| |
| writeFile("/foo", "foo2"); |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertCacheable(res, false); |
| assertHasETag(res); |
| assertCacheHits(cache, 2, 2); |
| } |
| |
| @Test |
| public void verySmallFileDoesntBotherWithGzip() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, true); |
| writeFile("/foo", "foo1"); |
| |
| FakeHttpServletRequest req = request("/foo").addHeader("Accept-Encoding", "gzip"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(req, res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getHeader("Content-Encoding")).isNull(); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertHasETag(res); |
| assertCacheable(res, true); |
| } |
| |
| @Test |
| public void smallFileWithGzip() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, true); |
| String content = Strings.repeat("a", 100); |
| writeFile("/foo", content); |
| |
| FakeHttpServletRequest req = request("/foo").addHeader("Accept-Encoding", "gzip"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(req, res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getHeader("Content-Encoding")).isEqualTo("gzip"); |
| assertThat(gunzip(res.getActualBody())).isEqualTo(content); |
| assertHasETag(res); |
| assertCacheable(res, true); |
| } |
| |
| @Test |
| public void largeFileBypassesCacheRegardlessOfRefreshParamter() throws Exception { |
| for (boolean refresh : Lists.newArrayList(true, false)) { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, refresh, 3); |
| |
| writeFile("/foo", "foo1"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertThat(res.getHeader("Last-Modified")).isNotNull(); |
| assertCacheable(res, refresh); |
| assertHasLastModified(res); |
| assertCacheHits(cache, 0, 1); |
| |
| writeFile("/foo", "foo1"); |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo1"); |
| assertThat(res.getHeader("Last-Modified")).isNotNull(); |
| assertCacheable(res, refresh); |
| assertHasLastModified(res); |
| assertCacheHits(cache, 0, 2); |
| |
| writeFile("/foo", "foo2"); |
| res = new FakeHttpServletResponse(); |
| servlet.doGet(request("/foo"), res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getActualBodyString()).isEqualTo("foo2"); |
| assertThat(res.getHeader("Last-Modified")).isNotNull(); |
| assertCacheable(res, refresh); |
| assertHasLastModified(res); |
| assertCacheHits(cache, 0, 3); |
| } |
| } |
| |
| @Test |
| public void largeFileWithGzip() throws Exception { |
| Cache<Path, Resource> cache = newCache(1); |
| Servlet servlet = new Servlet(fs, cache, true, 3); |
| String content = Strings.repeat("a", 100); |
| writeFile("/foo", content); |
| |
| FakeHttpServletRequest req = request("/foo").addHeader("Accept-Encoding", "gzip"); |
| FakeHttpServletResponse res = new FakeHttpServletResponse(); |
| servlet.doGet(req, res); |
| assertThat(res.getStatus()).isEqualTo(SC_OK); |
| assertThat(res.getHeader("Content-Encoding")).isEqualTo("gzip"); |
| assertThat(gunzip(res.getActualBody())).isEqualTo(content); |
| assertHasLastModified(res); |
| assertCacheable(res, true); |
| } |
| |
| // TODO(dborowitz): Check MIME type. |
| // TODO(dborowitz): Test that JS is not gzipped. |
| // TODO(dborowitz): Test ?e parameter. |
| // TODO(dborowitz): Test If-None-Match behavior. |
| // TODO(dborowitz): Test If-Modified-Since behavior. |
| |
| private void writeFile(String path, String content) throws Exception { |
| Files.write(fs.getPath(path), content.getBytes(UTF_8)); |
| Files.setLastModifiedTime(fs.getPath(path), FileTime.fromMillis(ts.getAndIncrement())); |
| } |
| |
| private static void assertCacheHits(Cache<?, ?> cache, int hits, int misses) { |
| assertWithMessage("hits").that(cache.stats().hitCount()).isEqualTo(hits); |
| assertWithMessage("misses").that(cache.stats().missCount()).isEqualTo(misses); |
| } |
| |
| private static void assertCacheable(FakeHttpServletResponse res, boolean revalidate) { |
| String header = res.getHeader("Cache-Control").toLowerCase(Locale.US); |
| assertThat(header).contains("public"); |
| if (revalidate) { |
| assertThat(header).contains("must-revalidate"); |
| } else { |
| assertThat(header).doesNotContain("must-revalidate"); |
| } |
| } |
| |
| private static void assertHasLastModified(FakeHttpServletResponse res) { |
| assertThat(res.getHeader("Last-Modified")).isNotNull(); |
| assertThat(res.getHeader("ETag")).isNull(); |
| } |
| |
| private static void assertHasETag(FakeHttpServletResponse res) { |
| assertThat(res.getHeader("ETag")).isNotNull(); |
| assertThat(res.getHeader("Last-Modified")).isNull(); |
| } |
| |
| private static void assertNotCacheable(FakeHttpServletResponse res) { |
| assertThat(res.getHeader("Cache-Control")).contains("no-cache"); |
| assertThat(res.getHeader("ETag")).isNull(); |
| assertThat(res.getHeader("Last-Modified")).isNull(); |
| } |
| |
| private static FakeHttpServletRequest request(String path) { |
| return new FakeHttpServletRequest().setPathInfo(path); |
| } |
| |
| private static String gunzip(byte[] data) throws Exception { |
| try (InputStream in = new GZIPInputStream(new ByteArrayInputStream(data))) { |
| return new String(ByteStreams.toByteArray(in), UTF_8); |
| } |
| } |
| } |