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
17package com.android.volley.toolbox;
18
19import com.android.volley.Cache;
20import com.android.volley.NetworkResponse;
21
22import org.apache.http.Header;
23import org.apache.http.message.BasicHeader;
24import org.junit.Before;
25import org.junit.Test;
26import org.junit.runner.RunWith;
27import org.robolectric.RobolectricTestRunner;
28
29import java.text.DateFormat;
30import java.text.SimpleDateFormat;
31import java.util.Date;
32import java.util.HashMap;
33import java.util.Locale;
34import java.util.Map;
35
36import static org.junit.Assert.*;
37
38@RunWith(RobolectricTestRunner.class)
39public 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
49    @Before public void setUp() throws Exception {
50        headers = new HashMap<String, String>();
51        response = new NetworkResponse(0, null, headers, false);
52    }
53
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
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
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
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
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
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
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
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
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
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
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
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
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