/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.shindig.gadgets.http; import org.apache.shindig.common.util.DateUtil; import org.apache.shindig.common.util.FakeTimeSource; import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Arrays; public class HttpResponseTest extends Assert { private static final byte[] UTF8_DATA = { (byte)0xEF, (byte)0xBB, (byte)0xBF, 'h', 'e', 'l', 'l', 'o' }; private static final String UTF8_STRING = "hello"; // A large string is needed for accurate charset detection. private static final byte[] LATIN1_DATA = { 'G', 'a', 'm', 'e', 's', ',', ' ', 'H', 'Q', ',', ' ', 'M', 'a', 'n', 'g', (byte)0xE1, ',', ' ', 'A', 'n', 'i', 'm', 'e', ' ', 'e', ' ', 't', 'u', 'd', 'o', ' ', 'q', 'u', 'e', ' ', 'u', 'm', ' ', 'b', 'o', 'm', ' ', 'n', 'e', 'r', 'd', ' ', 'a', 'm', 'a' }; private static final String LATIN1_STRING = "Games, HQ, Mang\u00E1, Anime e tudo que um bom nerd ama"; private static final byte[] BIG5_DATA = { (byte)0xa7, (byte)0x41, (byte)0xa6, (byte)0x6e }; private static final String BIG5_STRING = "\u4F60\u597D"; private static int roundToSeconds(long ts) { return (int)(ts / 1000); } private static FakeTimeSource timeSource = new FakeTimeSource(System.currentTimeMillis()); @Before public void setUp() { HttpResponse.setTimeSource(timeSource); } @Test public void testEncodingDetectionUtf8WithBom() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; charset=UTF-8") .setResponse(UTF8_DATA) .create(); assertEquals(UTF8_STRING, response.getResponseAsString()); assertEquals("UTF-8", response.getEncoding()); } @Test public void testEncodingDetectionUtf8WithBomCaseInsensitiveKey() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; Charset=utf-8") // Legitimate data, should be ignored in favor of explicit charset. .setResponse(LATIN1_DATA) .create(); assertEquals("UTF-8", response.getEncoding()); } @Test public void testEncodingDetectionLatin1() throws Exception { // Input is a basic latin-1 string with 1 non-UTF8 compatible char. HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; charset=iso-8859-1") .setResponse(LATIN1_DATA) .create(); assertEquals(LATIN1_STRING, response.getResponseAsString()); } @Test public void testEncodingDetectionLatin1withIncorrectCharset() throws Exception { // Input is a basic latin-1 string with 1 non-UTF8 compatible char. HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; charset=iso-88859-1") .setResponse(LATIN1_DATA) .create(); assertEquals(LATIN1_STRING, response.getResponseAsString()); assertEquals("ISO-8859-1", response.getEncoding()); } @Test public void testEncodingDetectionUtf8WithBomAndIncorrectCharset() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; charset=UTTFF-88") .setResponse(UTF8_DATA) .create(); assertEquals(UTF8_STRING, response.getResponseAsString()); assertEquals("UTF-8", response.getEncoding()); } @Test public void testEncodingDetectionUtf8WithBomAndInvalidCharset() throws Exception { HttpResponse response = new HttpResponseBuilder() // Use a charset that will generate an IllegalCharsetNameException .addHeader("Content-Type", "text/plain; charset=.UTF-8") .setResponse(UTF8_DATA) .create(); assertEquals(UTF8_STRING, response.getResponseAsString()); assertEquals("UTF-8", response.getEncoding()); } @Test public void testEncodingDetectionBig5() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; charset=BIG5") .setResponse(BIG5_DATA) .create(); assertEquals(BIG5_STRING, response.getResponseAsString()); assertEquals("text/plain; charset=BIG5", response.getHeader("Content-Type")); HttpResponseBuilder subResponseBuilder = new HttpResponseBuilder(response); subResponseBuilder.setContent(response.getResponseAsString()); HttpResponse subResponse = subResponseBuilder.create(); // Same string. assertEquals("text/plain; charset=UTF-8", subResponse.getHeader("Content-Type")); assertEquals(BIG5_STRING, subResponse.getResponseAsString()); // New encoding. } @Test public void testEncodingDetectionBig5WithQuotes() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain; charset=\"BIG5\"") .setResponse(BIG5_DATA) .create(); assertEquals(BIG5_STRING, response.getResponseAsString()); } @Test public void testEncodingDetectionUtf8WithBomNoCharsetSpecified() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain") .setResponse(UTF8_DATA) .create(); assertEquals("UTF-8", response.getEncoding().toUpperCase()); assertEquals(UTF8_STRING, response.getResponseAsString()); } @Test public void testEncodingDetectionLatin1NoCharsetSpecified() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "text/plain;") .setResponse(LATIN1_DATA) .create(); assertEquals("ISO-8859-1", response.getEncoding().toUpperCase()); assertEquals(LATIN1_STRING, response.getResponseAsString()); } @Test public void testEncodingDetectionWithEmptyContentType() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "") .setResponseString("something") .create(); assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding()); } @Test public void testEncodingDetectionUtf8WithBomNoContentHeader() throws Exception { HttpResponse response = new HttpResponseBuilder() .setResponse(UTF8_DATA) .create(); assertEquals("UTF-8", response.getEncoding().toUpperCase()); assertEquals(UTF8_STRING, response.getResponseAsString()); } @Test public void testEncodingDetectionLatin1NoContentHeader() throws Exception { HttpResponse response = new HttpResponseBuilder() .setResponse(LATIN1_DATA) .create(); assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding()); } @Test public void testGetEncodingForImageContentType() throws Exception { HttpResponse response = new HttpResponseBuilder() .setResponse(LATIN1_DATA) .addHeader("Content-Type", "image/png; charset=iso-8859-1") .create(); assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding().toUpperCase()); } @Test public void testGetEncodingForFlashContentType() throws Exception { HttpResponse response = new HttpResponseBuilder() .setResponse(LATIN1_DATA) .addHeader("Content-Type", "application/x-shockwave-flash; charset=iso-8859-1") .create(); assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding().toUpperCase()); } @Test public void testPreserveBinaryData() throws Exception { byte[] data = { (byte)0x00, (byte)0xDE, (byte)0xEA, (byte)0xDB, (byte)0xEE, (byte)0xF0 }; HttpResponse response = new HttpResponseBuilder() .addHeader("Content-Type", "application/octet-stream") .setResponse(data) .create(); byte[] out = IOUtils.toByteArray(response.getResponse()); assertEquals(data.length, response.getContentLength()); assertTrue(Arrays.equals(data, out)); out = IOUtils.toByteArray(response.getResponse()); assertTrue(Arrays.equals(data, out)); } @Test public void testStrictCacheControlNoCache() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Cache-Control", "no-cache") .create(); assertTrue(response.isStrictNoCache()); assertEquals(-1, response.getCacheExpiration()); assertEquals(-1, response.getCacheTtl()); } @Test public void testStrictPragmaNoCache() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Pragma", "no-cache") .create(); assertTrue(response.isStrictNoCache()); assertEquals(-1, response.getCacheExpiration()); assertEquals(-1, response.getCacheTtl()); } @Test public void testStrictPragmaJunk() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Pragma", "junk") .create(); assertFalse(response.isStrictNoCache()); int expected = roundToSeconds(timeSource.currentTimeMillis() + response.getDefaultTtl()); int expires = roundToSeconds(response.getCacheExpiration()); assertEquals(expected, expires); assertTrue(response.getCacheTtl() <= response.getDefaultTtl() && response.getCacheTtl() > 0); } @Test public void testCachingHeadersIgnoredOnError() throws Exception { HttpResponse response = new HttpResponseBuilder() .addHeader("Cache-Control", "no-cache") .setHttpStatusCode(404) .create(); assertFalse(response.isStrictNoCache()); response = new HttpResponseBuilder() .addHeader("Cache-Control", "no-cache") .setHttpStatusCode(403) .create(); assertTrue(response.isStrictNoCache()); response = new HttpResponseBuilder() .addHeader("Cache-Control", "no-cache") .setHttpStatusCode(401) .create(); assertTrue(response.isStrictNoCache()); } /** * Verifies that the cache TTL is within acceptable ranges. * This always rounds down due to timing, so actual verification will be against maxAge - 1. */ private static void assertTtlOk(int maxAge, HttpResponse response) { assertEquals(maxAge - 1, roundToSeconds(response.getCacheTtl() - 1)); } @Test public void testExpires() throws Exception { int maxAge = 10; int time = roundToSeconds(timeSource.currentTimeMillis()) + maxAge; HttpResponse response = new HttpResponseBuilder() .addHeader("Expires", DateUtil.formatRfc1123Date(1000L * time)) .create(); assertEquals(time, roundToSeconds(response.getCacheExpiration())); // Second rounding makes this n-1. assertTtlOk(maxAge, response); } @Test public void testExpiresZeroValue() throws Exception { HttpResponse response = new HttpResponseBuilder().addHeader("Expires", "0").create(); assertEquals(0, roundToSeconds(response.getCacheExpiration())); } @Test public void testExpiresUnknownValue() throws Exception { HttpResponse response = new HttpResponseBuilder().addHeader("Expires", "howdy").create(); assertEquals(0, roundToSeconds(response.getCacheExpiration())); } @Test public void testMaxAgeNoDate() throws Exception { int maxAge = 10; // Guess time. int expected = roundToSeconds(timeSource.currentTimeMillis()) + maxAge; HttpResponse response = new HttpResponseBuilder() .addHeader("Cache-Control", "public, max-age=" + maxAge) .create(); int expiration = roundToSeconds(response.getCacheExpiration()); assertEquals(expected, expiration); assertTtlOk(maxAge, response); } @Test public void testMaxAgeInvalidDate() throws Exception { int maxAge = 10; // Guess time. int expected = roundToSeconds(timeSource.currentTimeMillis()) + maxAge; HttpResponse response = new HttpResponseBuilder() .addHeader("Date", "Wed, 09 Jul 2008 19:18:33 EDT") .addHeader("Cache-Control", "public, max-age=" + maxAge) .create(); int expiration = roundToSeconds(response.getCacheExpiration()); assertEquals(expected, expiration); assertTtlOk(maxAge, response); } @Test public void testMaxAgeWithDate() throws Exception { int maxAge = 10; int now = roundToSeconds(timeSource.currentTimeMillis()); HttpResponse response = new HttpResponseBuilder() .addHeader("Date", DateUtil.formatRfc1123Date(1000L * now)) .addHeader("Cache-Control", "public, max-age=" + maxAge) .create(); assertEquals(now + maxAge, roundToSeconds(response.getCacheExpiration())); assertTtlOk(maxAge, response); } @Test public void testFixedDate() throws Exception { int time = roundToSeconds(timeSource.currentTimeMillis()); HttpResponse response = new HttpResponseBuilder() .addHeader("Date", DateUtil.formatRfc1123Date(1000L * time)) .create(); assertEquals(time + roundToSeconds(response.getDefaultTtl()), roundToSeconds(response.getCacheExpiration())); assertEquals(DateUtil.formatRfc1123Date(timeSource.currentTimeMillis()), response.getHeader("Date")); assertTtlOk(roundToSeconds(response.getDefaultTtl()), response); } @Test public void testFixedDateOld() throws Exception { int time = roundToSeconds(timeSource.currentTimeMillis()); HttpResponse response = new HttpResponseBuilder() .addHeader("Date", DateUtil.formatRfc1123Date(1000L * time - 1000 - HttpResponse.DEFAULT_DRIFT_LIMIT_MS)) .create(); // Verify that the old time is ignored: assertEquals(time + roundToSeconds(response.getDefaultTtl()), roundToSeconds(response.getCacheExpiration())); assertEquals(DateUtil.formatRfc1123Date(timeSource.currentTimeMillis()), response.getHeader("Date")); assertTtlOk(roundToSeconds(response.getDefaultTtl()), response); } @Test public void testFixedDateNew() throws Exception { int time = roundToSeconds(timeSource.currentTimeMillis()); HttpResponse response = new HttpResponseBuilder() .addHeader("Date", DateUtil.formatRfc1123Date(1000L * time + 1000 + HttpResponse.DEFAULT_DRIFT_LIMIT_MS)) .create(); // Verify that the old time is ignored: assertEquals(time + roundToSeconds(response.getDefaultTtl()), roundToSeconds(response.getCacheExpiration())); assertTtlOk(roundToSeconds(response.getDefaultTtl()), response); } @Test public void testNegativeCaching() { assertTrue("Bad HTTP responses must be cacheable!", HttpResponse.error().getCacheExpiration() > timeSource.currentTimeMillis()); assertTrue("Bad HTTP responses must be cacheable!", HttpResponse.notFound().getCacheExpiration() > timeSource.currentTimeMillis()); assertTrue("Bad HTTP responses must be cacheable!", HttpResponse.timeout().getCacheExpiration() > timeSource.currentTimeMillis()); long ttl = HttpResponse.error().getCacheTtl(); assertTrue(ttl <= HttpResponse.DEFAULT_TTL && ttl > 0); } private static void assertDoesNotAllowNegativeCaching(int status) { HttpResponse response = new HttpResponseBuilder() .setHttpStatusCode(status) .setResponse(UTF8_DATA) .setStrictNoCache() .create(); assertEquals(-1, response.getCacheTtl()); } private static void assertAllowsNegativeCaching(int status) { HttpResponse response = new HttpResponseBuilder() .setHttpStatusCode(status) .setResponse(UTF8_DATA) .setStrictNoCache() .create(); long ttl = response.getCacheTtl(); assertTrue(ttl <= response.getDefaultTtl() && ttl > 0); } @Test public void testStrictNoCacheAndNegativeCaching() { assertDoesNotAllowNegativeCaching(HttpResponse.SC_UNAUTHORIZED); assertDoesNotAllowNegativeCaching(HttpResponse.SC_FORBIDDEN); assertDoesNotAllowNegativeCaching(HttpResponse.SC_OK); assertAllowsNegativeCaching(HttpResponse.SC_NOT_FOUND); assertAllowsNegativeCaching(HttpResponse.SC_INTERNAL_SERVER_ERROR); assertAllowsNegativeCaching(HttpResponse.SC_GATEWAY_TIMEOUT); } @Test public void testRetryAfter() { HttpResponse response; for (int rc : Arrays.asList(HttpResponse.SC_INTERNAL_SERVER_ERROR, HttpResponse.SC_GATEWAY_TIMEOUT, HttpResponse.SC_BAD_REQUEST)) { response = new HttpResponseBuilder() .setHttpStatusCode(rc) .setHeader("Retry-After","60") .create(); long ttl = response.getCacheTtl(); assertTrue(ttl <= 60 * 1000L && ttl > 0); } } @Test public void testSetNoCache() { int time = roundToSeconds(timeSource.currentTimeMillis()); HttpResponse response = new HttpResponseBuilder() .addHeader("Expires", DateUtil.formatRfc1123Date(1000L * time)) .setStrictNoCache() .create(); assertNull(response.getHeader("Expires")); assertEquals("no-cache", response.getHeader("Pragma")); assertEquals("no-cache", response.getHeader("Cache-Control")); } @Test public void testNullHeaderNamesStripped() { HttpResponse response = new HttpResponseBuilder() .addHeader(null, "dummy") .create(); for (String key : response.getHeaders().keySet()) { assertNotNull("Null header not removed.", key); } } @Test public void testIsError() { // These aren't all valid status codes, but they're reserved in these blocks. Changes // would be required to the HTTP standard anyway before this test would be invalid. for (int i = 100; i < 400; i += 100) { for (int j = 0; j < 10; ++j) { HttpResponse response = new HttpResponseBuilder().setHttpStatusCode(i).create(); assertFalse("Status below 400 considered to be an error", response.isError()); } } for (int i = 400; i < 600; i += 100) { for (int j = 0; j < 10; ++j) { HttpResponse response = new HttpResponseBuilder().setHttpStatusCode(i).create(); assertTrue("Status above 400 considered to be an error", response.isError()); } } } @Test public void testSerialization() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(baos); HttpResponse response = new HttpResponseBuilder() .addHeader("Foo", "bar") .addHeader("Foo", "baz") .addHeader("Blah", "blah") .setHttpStatusCode(204) .setResponseString("This is the response string") .create(); out.writeObject(response); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream in = new ObjectInputStream(bais); HttpResponse deserialized = (HttpResponse)in.readObject(); assertEquals(response, deserialized); } @Test public void testSerializationWithTransientFields() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(baos); long now = timeSource.currentTimeMillis(); HttpResponse response = new HttpResponseBuilder() .addHeader("Foo", "bar") .addHeader("Foo", "baz") .addHeader("Blah", "blah") .addHeader("Date", DateUtil.formatRfc1123Date(now)) .setHttpStatusCode(204) .setResponseString("This is the response string") .setMetadata("foo", "bar") .create(); out.writeObject(response); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream in = new ObjectInputStream(bais); HttpResponse deserialized = (HttpResponse)in.readObject(); HttpResponse expectedResponse = new HttpResponseBuilder() .addHeader("Foo", "bar") .addHeader("Foo", "baz") .addHeader("Blah", "blah") .addHeader("Date", DateUtil.formatRfc1123Date(now)) .setHttpStatusCode(204) .setResponseString("This is the response string") .create(); assertEquals(expectedResponse, deserialized); } }