View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  
30  import static org.hamcrest.MatcherAssert.assertThat;
31  import static org.junit.jupiter.api.Assertions.assertEquals;
32  import static org.junit.jupiter.api.Assertions.assertNotSame;
33  import static org.junit.jupiter.api.Assertions.fail;
34  
35  import java.io.IOException;
36  import java.time.Instant;
37  import java.util.HashMap;
38  import java.util.Map;
39  
40  import org.apache.hc.client5.http.cache.HttpCacheEntry;
41  import org.apache.hc.client5.http.utils.DateUtils;
42  import org.apache.hc.core5.http.Header;
43  import org.apache.hc.core5.http.HttpResponse;
44  import org.apache.hc.core5.http.HttpStatus;
45  import org.apache.hc.core5.http.message.BasicHeader;
46  import org.apache.hc.core5.http.message.BasicHttpResponse;
47  import org.junit.jupiter.api.BeforeEach;
48  import org.junit.jupiter.api.Test;
49  
50  public class TestCacheUpdateHandler {
51  
52      private Instant requestDate;
53      private Instant responseDate;
54  
55      private CacheUpdateHandler impl;
56      private HttpCacheEntry entry;
57      private Instant now;
58      private Instant oneSecondAgo;
59      private Instant twoSecondsAgo;
60      private Instant eightSecondsAgo;
61      private Instant tenSecondsAgo;
62      private HttpResponse response;
63  
64      @BeforeEach
65      public void setUp() throws Exception {
66          requestDate = Instant.now().minusSeconds(1);
67          responseDate = Instant.now();
68  
69          now = Instant.now();
70          oneSecondAgo = now.minusSeconds(1);
71          twoSecondsAgo = now.minusSeconds(2);
72          eightSecondsAgo = now.minusSeconds(8);
73          tenSecondsAgo = now.minusSeconds(10);
74  
75          response = new BasicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
76  
77          impl = new CacheUpdateHandler();
78      }
79  
80      @Test
81      public void testUpdateCacheEntryReturnsDifferentEntryInstance()
82              throws IOException {
83          entry = HttpTestUtils.makeCacheEntry();
84          final HttpCacheEntry newEntry = impl.updateCacheEntry(null, entry,
85                  requestDate, responseDate, response);
86          assertNotSame(newEntry, entry);
87      }
88  
89      @Test
90      public void testHeadersAreMergedCorrectly() throws IOException {
91          final Header[] headers = {
92                  new BasicHeader("Date", DateUtils.formatStandardDate(responseDate)),
93                  new BasicHeader("ETag", "\"etag\"")};
94          entry = HttpTestUtils.makeCacheEntry(headers);
95          response.setHeaders();
96  
97          final HttpCacheEntry updatedEntry = impl.updateCacheEntry(null, entry,
98                  Instant.now(), Instant.now(), response);
99  
100          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(responseDate)));
101          assertThat(updatedEntry, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
102     }
103 
104     @Test
105     public void testNewerHeadersReplaceExistingHeaders() throws IOException {
106         final Header[] headers = {
107                 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
108                 new BasicHeader("Cache-Control", "private"),
109                 new BasicHeader("ETag", "\"etag\""),
110                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(requestDate)),
111                 new BasicHeader("Cache-Control", "max-age=0"),};
112         entry = HttpTestUtils.makeCacheEntry(headers);
113 
114         response.setHeaders(new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
115                 new BasicHeader("Cache-Control", "public"));
116 
117         final HttpCacheEntry updatedEntry = impl.updateCacheEntry(null, entry,
118                 Instant.now(), Instant.now(), response);
119 
120          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(requestDate)));
121          assertThat(updatedEntry, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
122          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Last-Modified", DateUtils.formatStandardDate(responseDate)));
123          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Cache-Control", "public"));
124     }
125 
126     @Test
127     public void testNewHeadersAreAddedByMerge() throws IOException {
128 
129         final Header[] headers = {
130                 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
131                 new BasicHeader("ETag", "\"etag\"")};
132 
133         entry = HttpTestUtils.makeCacheEntry(headers);
134         response.setHeaders(new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
135                 new BasicHeader("Cache-Control", "public"));
136 
137         final HttpCacheEntry updatedEntry = impl.updateCacheEntry(null, entry,
138                 Instant.now(), Instant.now(), response);
139 
140          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(requestDate)));
141          assertThat(updatedEntry, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
142          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Last-Modified", DateUtils.formatStandardDate(responseDate)));
143          assertThat(updatedEntry, ContainsHeaderMatcher.contains("Cache-Control", "public"));
144     }
145 
146     @Test
147     public void oldHeadersRetainedIfResponseOlderThanEntry()
148             throws Exception {
149         final Header[] headers = {
150                 new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
151                 new BasicHeader("ETag", "\"new-etag\"")
152         };
153         entry = HttpTestUtils.makeCacheEntry(twoSecondsAgo, now, headers);
154         response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
155         response.setHeader("ETag", "\"old-etag\"");
156         final HttpCacheEntry result = impl.updateCacheEntry("A", entry, Instant.now(),
157                 Instant.now(), response);
158          assertThat(result, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(oneSecondAgo)));
159          assertThat(result, ContainsHeaderMatcher.contains("ETag", "\"new-etag\""));
160     }
161 
162     @Test
163     public void testUpdatedEntryHasLatestRequestAndResponseDates()
164             throws IOException {
165         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo);
166         final HttpCacheEntry updated = impl.updateCacheEntry(null, entry,
167                 twoSecondsAgo, oneSecondAgo, response);
168 
169         assertEquals(twoSecondsAgo, updated.getRequestInstant());
170         assertEquals(oneSecondAgo, updated.getResponseInstant());
171     }
172 
173     @Test
174     public void entry1xxWarningsAreRemovedOnUpdate() throws Exception {
175         final Header[] headers = {
176                 new BasicHeader("Warning", "110 fred \"Response is stale\""),
177                 new BasicHeader("ETag", "\"old\""),
178                 new BasicHeader("Date", DateUtils.formatStandardDate(eightSecondsAgo))
179         };
180         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, headers);
181         response.setHeader("ETag", "\"new\"");
182         response.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
183         final HttpCacheEntry updated = impl.updateCacheEntry(null, entry,
184                 twoSecondsAgo, oneSecondAgo, response);
185 
186         assertEquals(0, updated.getHeaders("Warning").length);
187     }
188 
189     @Test
190     public void entryWithMalformedDateIsStillUpdated() throws Exception {
191         final Header[] headers = {
192                 new BasicHeader("ETag", "\"old\""),
193                 new BasicHeader("Date", "bad-date")
194         };
195         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, headers);
196         response.setHeader("ETag", "\"new\"");
197         response.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
198         final HttpCacheEntry updated = impl.updateCacheEntry(null, entry,
199                 twoSecondsAgo, oneSecondAgo, response);
200 
201         assertEquals("\"new\"", updated.getFirstHeader("ETag").getValue());
202     }
203 
204     @Test
205     public void entryIsStillUpdatedByResponseWithMalformedDate() throws Exception {
206         final Header[] headers = {
207                 new BasicHeader("ETag", "\"old\""),
208                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))
209         };
210         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo, headers);
211         response.setHeader("ETag", "\"new\"");
212         response.setHeader("Date", "bad-date");
213         final HttpCacheEntry updated = impl.updateCacheEntry(null, entry, twoSecondsAgo,
214                 oneSecondAgo, response);
215 
216         assertEquals("\"new\"", updated.getFirstHeader("ETag").getValue());
217     }
218 
219     @Test
220     public void cannotUpdateFromANon304OriginResponse() throws Exception {
221         entry = HttpTestUtils.makeCacheEntry();
222         response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
223         try {
224             impl.updateCacheEntry("A", entry, Instant.now(), Instant.now(),
225                     response);
226             fail("should have thrown exception");
227         } catch (final IllegalArgumentException expected) {
228         }
229     }
230 
231     @Test
232     public void testCacheUpdateAddsVariantURIToParentEntry() throws Exception {
233         final String parentCacheKey = "parentCacheKey";
234         final String variantCacheKey = "variantCacheKey";
235         final String existingVariantKey = "existingVariantKey";
236         final String newVariantCacheKey = "newVariantCacheKey";
237         final String newVariantKey = "newVariantKey";
238         final Map<String,String> existingVariants = new HashMap<>();
239         existingVariants.put(existingVariantKey, variantCacheKey);
240         final HttpCacheEntry parent = HttpTestUtils.makeCacheEntry(existingVariants);
241         final HttpCacheEntry variant = HttpTestUtils.makeCacheEntry();
242 
243         final HttpCacheEntry result = impl.updateParentCacheEntry(parentCacheKey, parent, variant, newVariantKey, newVariantCacheKey);
244         final Map<String,String> resultMap = result.getVariantMap();
245         assertEquals(2, resultMap.size());
246         assertEquals(variantCacheKey, resultMap.get(existingVariantKey));
247         assertEquals(newVariantCacheKey, resultMap.get(newVariantKey));
248     }
249 
250     @Test
251     public void testContentEncodingHeaderIsNotUpdatedByMerge() throws IOException {
252         final Header[] headers = {
253                 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
254                 new BasicHeader("ETag", "\"etag\""),
255                 new BasicHeader("Content-Encoding", "identity")};
256 
257         entry = HttpTestUtils.makeCacheEntry(headers);
258         response.setHeaders(new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
259                 new BasicHeader("Cache-Control", "public"),
260                 new BasicHeader("Content-Encoding", "gzip"));
261 
262         final HttpCacheEntry updatedEntry = impl.updateCacheEntry(null, entry,
263                 Instant.now(), Instant.now(), response);
264 
265         final Header[] updatedHeaders = updatedEntry.getHeaders();
266         headersContain(updatedHeaders, "Content-Encoding", "identity");
267         headersNotContain(updatedHeaders, "Content-Encoding", "gzip");
268     }
269 
270     @Test
271     public void testContentLengthIsNotAddedWhenTransferEncodingIsPresent() throws IOException {
272         final Header[] headers = {
273                 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
274                 new BasicHeader("ETag", "\"etag\""),
275                 new BasicHeader("Transfer-Encoding", "chunked")};
276 
277         entry = HttpTestUtils.makeCacheEntry(headers);
278         response.setHeaders(new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
279                 new BasicHeader("Cache-Control", "public"),
280                 new BasicHeader("Content-Length", "0"));
281 
282         final HttpCacheEntry updatedEntry = impl.updateCacheEntry(null, entry,
283                 Instant.now(), Instant.now(), response);
284 
285         final Header[] updatedHeaders = updatedEntry.getHeaders();
286         headersContain(updatedHeaders, "Transfer-Encoding", "chunked");
287         headersNotContain(updatedHeaders, "Content-Length", "0");
288     }
289 
290     private void headersContain(final Header[] headers, final String name, final String value) {
291         for (final Header header : headers) {
292             if (header.getName().equals(name)) {
293                 if (header.getValue().equals(value)) {
294                     return;
295                 }
296             }
297         }
298         fail("Header [" + name + ": " + value + "] not found in headers.");
299     }
300 
301     private void headersNotContain(final Header[] headers, final String name, final String value) {
302         for (final Header header : headers) {
303             if (header.getName().equals(name)) {
304                 if (header.getValue().equals(value)) {
305                     fail("Header [" + name + ": " + value + "] found in headers where it should not be");
306                 }
307             }
308         }
309     }
310 }