// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.net.http.handlers; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.GZIPInputStream; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.google.common.io.ByteStreams; import com.google.common.io.InputSupplier; import org.junit.Before; import org.junit.Test; import com.twitter.common.net.http.handlers.AssetHandler.StaticAsset; import com.twitter.common.testing.EasyMockTest; import static com.twitter.common.net.http.handlers.AssetHandler.CACHE_CONTROL_MAX_AGE_SECS; import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.easymock.EasyMock.expect; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; /** * @author William Farner */ public class AssetHandlerTest extends EasyMockTest { private static final String TEST_DATA = "here is my great test data"; // Checksum of the gzipped TEST_DATA. private static final String TEST_DATA_CHECKSUM = "ePvVhtAeVRu85KSOLKL0oQ=="; private static final String CONTENT_TYPE = "text/plain"; private InputSupplier<InputStream> inputSupplier; @Before public void setUp() { inputSupplier = createMock(new Clazz<InputSupplier<InputStream>>() {}); } private static class Request { private final HttpServletRequest req; private final HttpServletResponse resp; private final ByteArrayOutputStream responseBody; Request(HttpServletRequest req, HttpServletResponse resp, ByteArrayOutputStream responseBody) { this.req = req; this.resp = resp; this.responseBody = responseBody; } } private Request doGet(String suppliedChecksum, String supportedEncodings, int expectedResponseCode, boolean expectRead) throws Exception { HttpServletRequest req = createMock(HttpServletRequest.class); HttpServletResponse resp = createMock(HttpServletResponse.class); if (expectRead) { expect(inputSupplier.getInput()).andReturn(new ByteArrayInputStream(TEST_DATA.getBytes())); } expect(req.getHeader("If-None-Match")).andReturn(suppliedChecksum); resp.setStatus(expectedResponseCode); if (expectedResponseCode == SC_OK) { expect(req.getHeader("Accept-Encoding")).andReturn(supportedEncodings); resp.setHeader("Cache-Control", "public,max-age=" + CACHE_CONTROL_MAX_AGE_SECS); resp.setHeader("ETag", TEST_DATA_CHECKSUM); resp.setContentType(CONTENT_TYPE); if (supportedEncodings != null && supportedEncodings.contains("gzip")) { resp.setHeader("Content-Encoding", "gzip"); } } return new Request(req, resp, expectPayload(resp)); } @Test public void testCached() throws Exception { // First request - no cached value Request test1 = doGet( null, // No local checksum. null, // No encodings supported. SC_OK, true // Triggers a data read. ); // Second request - client performs conditional GET with wrong checksum. Request test2 = doGet( "foo", // Wrong checksum. null, // No encodings supported. SC_OK, false // No read. ); // Third request - client performs conditional GET with correct checksum. Request test3 = doGet( TEST_DATA_CHECKSUM, // Correct checksum. null, // No encodings supported. SC_NOT_MODIFIED, false // No read. ); control.replay(); AssetHandler handler = new AssetHandler(new StaticAsset(inputSupplier, CONTENT_TYPE, true)); handler.doGet(test1.req, test1.resp); assertThat(new String(test1.responseBody.toByteArray()), is(TEST_DATA)); handler.doGet(test2.req, test2.resp); assertThat(new String(test2.responseBody.toByteArray()), is(TEST_DATA)); handler.doGet(test3.req, test3.resp); assertThat(new String(test3.responseBody.toByteArray()), is("")); } @Test public void testCachedGzipped() throws Exception { // First request - no cached value Request test1 = doGet( null, // No local checksum. "gzip", // Supported encodings. SC_OK, true // Triggers a data read. ); // Second request - client performs conditional GET with wrong checksum. Request test2 = doGet( "foo", // Wrong checksum. "gzip,fakeencoding", // Supported encodings. SC_OK, false // No read. ); // Third request - client performs conditional GET with correct checksum. Request test3 = doGet( TEST_DATA_CHECKSUM, // Correct checksum. "gzip,deflate", // Supported encodings. SC_NOT_MODIFIED, false // No read. ); control.replay(); AssetHandler handler = new AssetHandler(new StaticAsset(inputSupplier, CONTENT_TYPE, true)); handler.doGet(test1.req, test1.resp); assertThat(unzip(test1.responseBody), is(TEST_DATA)); handler.doGet(test2.req, test2.resp); assertThat(unzip(test2.responseBody), is(TEST_DATA)); handler.doGet(test3.req, test3.resp); assertThat(new String(test3.responseBody.toByteArray()), is("")); } @Test public void testUncached() throws Exception { // First request - no cached value Request test1 = doGet( null, // No local checksum. null, // No encodings supported. SC_OK, true // Triggers a data read. ); // Second request - client performs conditional GET with wrong checksum. Request test2 = doGet( "foo", // Wrong checksum. null, // No encodings supported. SC_OK, true // Triggers a data read. ); // Third request - client performs conditional GET with correct checksum. Request test3 = doGet( TEST_DATA_CHECKSUM, // Correct checksum. null, // No encodings supported. SC_NOT_MODIFIED, true // Triggers a data read. ); control.replay(); AssetHandler handler = new AssetHandler(new StaticAsset(inputSupplier, CONTENT_TYPE, false)); handler.doGet(test1.req, test1.resp); assertThat(new String(test1.responseBody.toByteArray()), is(TEST_DATA)); handler.doGet(test2.req, test2.resp); assertThat(new String(test2.responseBody.toByteArray()), is(TEST_DATA)); handler.doGet(test3.req, test3.resp); assertThat(new String(test3.responseBody.toByteArray()), is("")); } @Test public void testUncachedGzipped() throws Exception { // First request - no cached value Request test1 = doGet( null, // No local checksum. "gzip", // Supported encodings. SC_OK, true // Triggers a data read. ); // Second request - client performs conditional GET with wrong checksum. Request test2 = doGet( "foo", // Wrong checksum. "gzip,fakeencoding", // Supported encodings. SC_OK, true // Triggers a data read. ); // Third request - client performs conditional GET with correct checksum. Request test3 = doGet( TEST_DATA_CHECKSUM, // Correct checksum. "gzip,deflate", // Supported encodings. SC_NOT_MODIFIED, true // Triggers a data read. ); control.replay(); AssetHandler handler = new AssetHandler(new StaticAsset(inputSupplier, CONTENT_TYPE, false)); handler.doGet(test1.req, test1.resp); assertThat(unzip(test1.responseBody), is(TEST_DATA)); handler.doGet(test2.req, test2.resp); assertThat(unzip(test2.responseBody), is(TEST_DATA)); handler.doGet(test3.req, test3.resp); assertThat(new String(test3.responseBody.toByteArray()), is("")); } private static ByteArrayOutputStream expectPayload(HttpServletResponse resp) throws Exception { ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); expect(resp.getOutputStream()).andReturn(new FakeServletOutputStream(responseBody)); return responseBody; } private static String unzip(ByteArrayOutputStream streamData) throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(streamData.toByteArray()); GZIPInputStream unzip = new GZIPInputStream(in); return new String(ByteStreams.toByteArray(unzip)); } private static class FakeServletOutputStream extends ServletOutputStream { private final OutputStream realStream; FakeServletOutputStream(OutputStream realStream) { this.realStream = realStream; } @Override public void write(int b) throws IOException { realStream.write(b); } @Override public void write(byte[] b) throws IOException { realStream.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { realStream.write(b, off, len); } @Override public void flush() throws IOException { realStream.flush(); } @Override public void close() throws IOException { realStream.close(); } @Override public void print(String s) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void print(boolean b) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void print(char c) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void print(int i) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void print(long l) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void print(float f) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void print(double d) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println() throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(String s) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(boolean b) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(char c) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(int i) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(long l) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(float f) throws IOException { throw new UnsupportedOperationException("Not implemented"); } @Override public void println(double d) throws IOException { throw new UnsupportedOperationException("Not implemented"); } } }