1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.volley.toolbox; 18 19 import com.android.volley.Cache; 20 import com.android.volley.NetworkResponse; 21 22 import org.apache.http.Header; 23 import org.apache.http.message.BasicHeader; 24 import org.junit.Before; 25 import org.junit.Test; 26 import org.junit.runner.RunWith; 27 import org.robolectric.RobolectricTestRunner; 28 29 import java.text.DateFormat; 30 import java.text.SimpleDateFormat; 31 import java.util.Date; 32 import java.util.HashMap; 33 import java.util.Locale; 34 import java.util.Map; 35 36 import static org.junit.Assert.*; 37 38 @RunWith(RobolectricTestRunner.class) 39 public class HttpHeaderParserTest { 40 41 private static long ONE_MINUTE_MILLIS = 1000L * 60; 42 private static long ONE_HOUR_MILLIS = 1000L * 60 * 60; 43 private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24; 44 private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7; 45 46 private NetworkResponse response; 47 private Map<String, String> headers; 48 setUp()49 @Before public void setUp() throws Exception { 50 headers = new HashMap<String, String>(); 51 response = new NetworkResponse(0, null, headers, false); 52 } 53 parseCacheHeaders_noHeaders()54 @Test public void parseCacheHeaders_noHeaders() { 55 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 56 57 assertNotNull(entry); 58 assertNull(entry.etag); 59 assertEquals(0, entry.serverDate); 60 assertEquals(0, entry.lastModified); 61 assertEquals(0, entry.ttl); 62 assertEquals(0, entry.softTtl); 63 } 64 parseCacheHeaders_headersSet()65 @Test public void parseCacheHeaders_headersSet() { 66 headers.put("MyCustomHeader", "42"); 67 68 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 69 70 assertNotNull(entry); 71 assertNotNull(entry.responseHeaders); 72 assertEquals(1, entry.responseHeaders.size()); 73 assertEquals("42", entry.responseHeaders.get("MyCustomHeader")); 74 } 75 parseCacheHeaders_etag()76 @Test public void parseCacheHeaders_etag() { 77 headers.put("ETag", "Yow!"); 78 79 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 80 81 assertNotNull(entry); 82 assertEquals("Yow!", entry.etag); 83 } 84 parseCacheHeaders_normalExpire()85 @Test public void parseCacheHeaders_normalExpire() { 86 long now = System.currentTimeMillis(); 87 headers.put("Date", rfc1123Date(now)); 88 headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS)); 89 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 90 91 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 92 93 assertNotNull(entry); 94 assertNull(entry.etag); 95 assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); 96 assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS); 97 assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS)); 98 assertTrue(entry.ttl == entry.softTtl); 99 } 100 parseCacheHeaders_expiresInPast()101 @Test public void parseCacheHeaders_expiresInPast() { 102 long now = System.currentTimeMillis(); 103 headers.put("Date", rfc1123Date(now)); 104 headers.put("Expires", rfc1123Date(now - ONE_HOUR_MILLIS)); 105 106 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 107 108 assertNotNull(entry); 109 assertNull(entry.etag); 110 assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); 111 assertEquals(0, entry.ttl); 112 assertEquals(0, entry.softTtl); 113 } 114 parseCacheHeaders_serverRelative()115 @Test public void parseCacheHeaders_serverRelative() { 116 117 long now = System.currentTimeMillis(); 118 // Set "current" date as one hour in the future 119 headers.put("Date", rfc1123Date(now + ONE_HOUR_MILLIS)); 120 // TTL four hours in the future, so should be three hours from now 121 headers.put("Expires", rfc1123Date(now + 4 * ONE_HOUR_MILLIS)); 122 123 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 124 125 assertEqualsWithin(now + 3 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); 126 assertEquals(entry.softTtl, entry.ttl); 127 } 128 parseCacheHeaders_cacheControlOverridesExpires()129 @Test public void parseCacheHeaders_cacheControlOverridesExpires() { 130 long now = System.currentTimeMillis(); 131 headers.put("Date", rfc1123Date(now)); 132 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 133 headers.put("Cache-Control", "public, max-age=86400"); 134 135 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 136 137 assertNotNull(entry); 138 assertNull(entry.etag); 139 assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); 140 assertEquals(entry.softTtl, entry.ttl); 141 } 142 testParseCacheHeaders_staleWhileRevalidate()143 @Test public void testParseCacheHeaders_staleWhileRevalidate() { 144 long now = System.currentTimeMillis(); 145 headers.put("Date", rfc1123Date(now)); 146 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 147 148 // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day 149 // - stale-while-revalidate (entry.ttl) indicates that the asset may 150 // continue to be served stale for up to additional 7 days 151 headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800"); 152 153 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 154 155 assertNotNull(entry); 156 assertNull(entry.etag); 157 assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); 158 assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); 159 } 160 parseCacheHeaders_cacheControlNoCache()161 @Test public void parseCacheHeaders_cacheControlNoCache() { 162 long now = System.currentTimeMillis(); 163 headers.put("Date", rfc1123Date(now)); 164 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 165 headers.put("Cache-Control", "no-cache"); 166 167 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 168 169 assertNull(entry); 170 } 171 parseCacheHeaders_cacheControlMustRevalidateNoMaxAge()172 @Test public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() { 173 long now = System.currentTimeMillis(); 174 headers.put("Date", rfc1123Date(now)); 175 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 176 headers.put("Cache-Control", "must-revalidate"); 177 178 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 179 assertNotNull(entry); 180 assertNull(entry.etag); 181 assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS); 182 assertEquals(entry.softTtl, entry.ttl); 183 } 184 parseCacheHeaders_cacheControlMustRevalidateWithMaxAge()185 @Test public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() { 186 long now = System.currentTimeMillis(); 187 headers.put("Date", rfc1123Date(now)); 188 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 189 headers.put("Cache-Control", "must-revalidate, max-age=3600"); 190 191 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 192 assertNotNull(entry); 193 assertNull(entry.etag); 194 assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); 195 assertEquals(entry.softTtl, entry.ttl); 196 } 197 parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale()198 @Test public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() { 199 long now = System.currentTimeMillis(); 200 headers.put("Date", rfc1123Date(now)); 201 headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 202 203 // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day 204 // - stale-while-revalidate (entry.ttl) indicates that the asset may 205 // continue to be served stale for up to additional 7 days, but this is 206 // ignored in this case because of the must-revalidate header. 207 headers.put("Cache-Control", 208 "must-revalidate, max-age=86400, stale-while-revalidate=604800"); 209 210 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 211 assertNotNull(entry); 212 assertNull(entry.etag); 213 assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); 214 assertEquals(entry.softTtl, entry.ttl); 215 } 216 assertEqualsWithin(long expected, long value, long fudgeFactor)217 private void assertEqualsWithin(long expected, long value, long fudgeFactor) { 218 long diff = Math.abs(expected - value); 219 assertTrue(diff < fudgeFactor); 220 } 221 222 private static String rfc1123Date(long millis) { 223 DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); 224 return df.format(new Date(millis)); 225 } 226 227 // -------------------------- 228 229 @Test public void parseCharset() { 230 // Like the ones we usually see 231 headers.put("Content-Type", "text/plain; charset=utf-8"); 232 assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); 233 234 // Charset specified, ignore default charset 235 headers.put("Content-Type", "text/plain; charset=utf-8"); 236 assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "ISO-8859-1")); 237 238 // Extra whitespace 239 headers.put("Content-Type", "text/plain; charset=utf-8 "); 240 assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); 241 242 // Extra parameters 243 headers.put("Content-Type", "text/plain; charset=utf-8; frozzle=bar"); 244 assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); 245 246 // No Content-Type header 247 headers.clear(); 248 assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); 249 250 // No Content-Type header, use default charset 251 headers.clear(); 252 assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); 253 254 // Empty value 255 headers.put("Content-Type", "text/plain; charset="); 256 assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); 257 258 // None specified 259 headers.put("Content-Type", "text/plain"); 260 assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); 261 262 // None charset specified, use default charset 263 headers.put("Content-Type", "application/json"); 264 assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); 265 266 // None specified, extra semicolon 267 headers.put("Content-Type", "text/plain;"); 268 assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); 269 } 270 271 @Test public void parseCaseInsensitive() { 272 273 long now = System.currentTimeMillis(); 274 275 Header[] headersArray = new Header[5]; 276 headersArray[0] = new BasicHeader("eTAG", "Yow!"); 277 headersArray[1] = new BasicHeader("DATE", rfc1123Date(now)); 278 headersArray[2] = new BasicHeader("expires", rfc1123Date(now + ONE_HOUR_MILLIS)); 279 headersArray[3] = new BasicHeader("cache-control", "public, max-age=86400"); 280 headersArray[4] = new BasicHeader("content-type", "text/plain"); 281 282 Map<String, String> headers = BasicNetwork.convertHeaders(headersArray); 283 NetworkResponse response = new NetworkResponse(0, null, headers, false); 284 Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); 285 286 assertNotNull(entry); 287 assertEquals("Yow!", entry.etag); 288 assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); 289 assertEquals(entry.softTtl, entry.ttl); 290 assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); 291 } 292 } 293