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  
28  package org.apache.hc.client5.http.impl.cache;
29  
30  import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.HttpCacheStorageEntryTestTemplate;
31  import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.httpCacheStorageEntryFromBytes;
32  import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.makeTestFileObject;
33  import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.readTestFileBytes;
34  import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.testWithCache;
35  import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.verifyHttpCacheEntryFromTestFile;
36  
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.util.HashMap;
41  import java.util.Map;
42  
43  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
44  import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
45  import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
46  import org.apache.hc.client5.http.cache.ResourceIOException;
47  import org.apache.hc.core5.http.ClassicHttpResponse;
48  import org.apache.hc.core5.http.Header;
49  import org.apache.hc.core5.http.HttpException;
50  import org.apache.hc.core5.http.impl.io.AbstractMessageParser;
51  import org.apache.hc.core5.http.impl.io.AbstractMessageWriter;
52  import org.apache.hc.core5.http.io.SessionInputBuffer;
53  import org.apache.hc.core5.http.io.SessionOutputBuffer;
54  import org.apache.hc.core5.http.message.BasicHeader;
55  import org.junit.jupiter.api.Assertions;
56  import org.junit.jupiter.api.BeforeEach;
57  import org.junit.jupiter.api.Test;
58  import org.mockito.Mockito;
59  
60  public class TestHttpByteArrayCacheEntrySerializer {
61      private static final String SERIALIAZED_EXTENSION = ".httpbytes.serialized";
62  
63      private static final String FILE_TEST_SERIALIZED_NAME = "ApacheLogo" + SERIALIAZED_EXTENSION;
64      private static final String SIMPLE_OBJECT_SERIALIZED_NAME = "simpleObject" + SERIALIAZED_EXTENSION;
65      private static final String VARIANTMAP_TEST_SERIALIZED_NAME = "variantMap" + SERIALIAZED_EXTENSION;
66      private static final String ESCAPED_HEADER_TEST_SERIALIZED_NAME = "escapedHeader" + SERIALIAZED_EXTENSION;
67      private static final String NO_BODY_TEST_SERIALIZED_NAME = "noBody" + SERIALIAZED_EXTENSION;
68      private static final String MISSING_HEADER_TEST_SERIALIZED_NAME = "missingHeader" + SERIALIAZED_EXTENSION;
69      private static final String INVALID_HEADER_TEST_SERIALIZED_NAME = "invalidHeader" + SERIALIAZED_EXTENSION;
70      private static final String VARIANTMAP_MISSING_KEY_TEST_SERIALIZED_NAME = "variantMapMissingKey" + SERIALIAZED_EXTENSION;
71      private static final String VARIANTMAP_MISSING_VALUE_TEST_SERIALIZED_NAME = "variantMapMissingValue" + SERIALIAZED_EXTENSION;
72  
73      private static final String TEST_CONTENT_FILE_NAME = "ApacheLogo.png";
74  
75      private HttpCacheEntrySerializer<byte[]> serializer;
76  
77      // Manually set this to true to re-generate all of the serialized files
78      private final boolean reserializeFiles = false;
79  
80      @BeforeEach
81      public void before() {
82          serializer = HttpByteArrayCacheEntrySerializer.INSTANCE;
83      }
84  
85      /**
86       * Deserialize a cache entry in a bad format, expecting an exception.
87       *
88       * @throws Exception is expected
89       */
90      @Test
91      public void testInvalidCacheEntry() throws Exception {
92          // This file is a JPEG not a cache entry, so should fail to deserialize
93          final byte[] bytes = readTestFileBytes(TEST_CONTENT_FILE_NAME);
94          Assertions.assertThrows(ResourceIOException.class, () ->
95                  httpCacheStorageEntryFromBytes(serializer, bytes));
96      }
97  
98      /**
99       * Deserialize a cache entry with a missing header, from a previously saved file.
100      *
101      * @throws Exception is expected
102      */
103     @Test
104     public void testMissingHeaderCacheEntry() throws Exception {
105         // This file hand-edited to be missing a necessary header
106         final byte[] bytes = readTestFileBytes(MISSING_HEADER_TEST_SERIALIZED_NAME);
107         Assertions.assertThrows(ResourceIOException.class, () ->
108                 httpCacheStorageEntryFromBytes(serializer, bytes));
109     }
110 
111     /**
112      * Deserialize a cache entry with an invalid header value, from a previously saved file.
113      *
114      * @throws Exception is expected
115      */
116     @Test
117     public void testInvalidHeaderCacheEntry() throws Exception {
118         // This file hand-edited to have an invalid header
119         final byte[] bytes = readTestFileBytes(INVALID_HEADER_TEST_SERIALIZED_NAME);
120         Assertions.assertThrows(ResourceIOException.class, () ->
121                 httpCacheStorageEntryFromBytes(serializer, bytes));
122     }
123 
124     /**
125      * Deserialize a cache entry with a missing variant map key, from a previously saved file.
126      *
127      * @throws Exception is expected
128      */
129     @Test
130     public void testVariantMapMissingKeyCacheEntry() throws Exception {
131         // This file hand-edited to be missing a VariantCache key
132         final byte[] bytes = readTestFileBytes(VARIANTMAP_MISSING_KEY_TEST_SERIALIZED_NAME);
133         Assertions.assertThrows(ResourceIOException.class, () ->
134                 httpCacheStorageEntryFromBytes(serializer, bytes));
135     }
136 
137     /**
138      * Deserialize a cache entry with a missing variant map value, from a previously saved file.
139      *
140      * @throws Exception is expected
141      */
142     @Test
143     public void testVariantMapMissingValueCacheEntry() throws Exception {
144         // This file hand-edited to be missing a VariantCache value
145         final byte[] bytes = readTestFileBytes(VARIANTMAP_MISSING_VALUE_TEST_SERIALIZED_NAME);
146         Assertions.assertThrows(ResourceIOException.class, () ->
147                 httpCacheStorageEntryFromBytes(serializer, bytes));
148     }
149 
150     /**
151      * Test an IOException being thrown while deserializing.
152      *
153      * @throws Exception is expected
154      */
155     @Test
156     public void testDeserializeWithIOException() throws Exception {
157         final AbstractMessageParser<ClassicHttpResponse> throwyParser = Mockito.mock(AbstractMessageParser.class);
158         Mockito.
159                 doThrow(new IOException("Test Exception")).
160                 when(throwyParser).
161                 parse(Mockito.any(SessionInputBuffer.class), Mockito.any(InputStream.class));
162 
163         final HttpByteArrayCacheEntrySerializer testSerializer = new HttpByteArrayCacheEntrySerializer() {
164             @Override
165             protected AbstractMessageParser<ClassicHttpResponse> makeHttpResponseParser() {
166                 return throwyParser;
167             }
168         };
169         Assertions.assertThrows(ResourceIOException.class, () ->
170                 testSerializer.deserialize(new byte[0]));
171     }
172 
173 
174     ////////////// Using new Constructor with Instant  //////////////
175 
176 
177     /**
178      * Serialize and deserialize a simple object with a tiny body.
179      *
180      * @throws Exception if anything goes wrong
181      */
182     @Test
183     public void simpleObjectTest() throws Exception {
184         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
185         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
186 
187         testWithCache(serializer, testEntry);
188     }
189 
190     /**
191      * Serialize and deserialize a larger object with a binary file for a body.
192      *
193      * @throws Exception if anything goes wrong
194      */
195     @Test
196     public void fileObjectTest() throws Exception {
197         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
198         cacheObjectValues.resource = new FileResource(makeTestFileObject(TEST_CONTENT_FILE_NAME));
199         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
200 
201         testWithCache(serializer, testEntry);
202     }
203 
204     /**
205      * Serialize and deserialize a cache entry with no headers.
206      *
207      * @throws Exception if anything goes wrong
208      */
209     @Test
210     public void noHeadersTest() throws Exception {
211         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
212         cacheObjectValues.responseHeaders = new Header[0];
213         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
214 
215         testWithCache(serializer, testEntry);
216     }
217 
218     /**
219      * Serialize and deserialize a cache entry with an empty body.
220      *
221      * @throws Exception if anything goes wrong
222      */
223     @Test
224     public void emptyBodyTest() throws Exception {
225         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
226         cacheObjectValues.resource = new HeapResource(new byte[0]);
227         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
228 
229         testWithCache(serializer, testEntry);
230     }
231 
232     /**
233      * Serialize and deserialize a cache entry with no body.
234      *
235      * @throws Exception if anything goes wrong
236      */
237     @Test
238     public void noBodyTest() throws Exception {
239         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
240         cacheObjectValues.resource = null;
241         cacheObjectValues.responseCode = 204;
242 
243         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
244 
245         testWithCache(serializer, testEntry);
246     }
247 
248     /**
249      * Serialize and deserialize a cache entry with a variant map.
250      *
251      * @throws Exception if anything goes wrong
252      */
253     @Test
254     public void testSimpleVariantMap() throws Exception {
255         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
256         final Map<String, String> variantMap = new HashMap<>();
257         variantMap.put("{Accept-Encoding=gzip}","{Accept-Encoding=gzip}https://example.com:1234/foo");
258         variantMap.put("{Accept-Encoding=compress}","{Accept-Encoding=compress}https://example.com:1234/foo");
259         cacheObjectValues.variantMap = variantMap;
260         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
261 
262         testWithCache(serializer, testEntry);
263     }
264 
265     /**
266      * Ensures that if the server uses our reserved header names we don't mix them up with our own pseudo-headers.
267      *
268      * @throws Exception if anything goes wrong
269      */
270     @Test
271     public void testEscapedHeaders() throws Exception {
272         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
273         cacheObjectValues.responseHeaders = new Header[] {
274                 new BasicHeader("hc-test-1", "hc-test-1-value"),
275                 new BasicHeader("hc-sk", "hc-sk-value"),
276                 new BasicHeader("hc-resp-date", "hc-resp-date-value"),
277                 new BasicHeader("hc-req-date-date", "hc-req-date-value"),
278                 new BasicHeader("hc-varmap-key", "hc-varmap-key-value"),
279                 new BasicHeader("hc-varmap-val", "hc-varmap-val-value"),
280         };
281         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
282 
283         testWithCache(serializer, testEntry);
284     }
285 
286     /**
287      * Attempt to store a cache entry with a null storage key.
288      *
289      */
290     @Test
291     public void testNullStorageKey() {
292         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
293         cacheObjectValues.storageKey = null;
294 
295         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
296         Assertions.assertThrows(IllegalStateException.class, () ->
297                 serializer.serialize(testEntry));
298     }
299 
300     /**
301      * Deserialize a simple object, from a previously saved file.
302      *
303      * Ensures that if the serialization format changes in an incompatible way, we'll find out in a test.
304      *
305      * @throws Exception if anything goes wrong
306      */
307     @Test
308     public void simpleTestFromPreviouslySerialized() throws Exception {
309         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
310         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
311 
312         verifyHttpCacheEntryFromTestFile(serializer, testEntry, SIMPLE_OBJECT_SERIALIZED_NAME, reserializeFiles);
313     }
314 
315     /**
316      * Deserialize a larger object with a binary body, from a previously saved file.
317      *
318      * Ensures that if the serialization format changes in an incompatible way, we'll find out in a test.
319      *
320      * @throws Exception if anything goes wrong
321      */
322     @Test
323     public void fileTestFromPreviouslySerialized() throws Exception {
324         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
325         cacheObjectValues.resource = new FileResource(makeTestFileObject(TEST_CONTENT_FILE_NAME));
326         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
327 
328         verifyHttpCacheEntryFromTestFile(serializer, testEntry, FILE_TEST_SERIALIZED_NAME, reserializeFiles);
329     }
330 
331     /**
332      * Deserialize a cache entry with a variant map, from a previously saved file.
333      *
334      * @throws Exception if anything goes wrong
335      */
336     @Test
337     public void variantMapTestFromPreviouslySerialized() throws Exception {
338         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
339         final Map<String, String> variantMap = new HashMap<>();
340         variantMap.put("{Accept-Encoding=gzip}","{Accept-Encoding=gzip}https://example.com:1234/foo");
341         variantMap.put("{Accept-Encoding=compress}","{Accept-Encoding=compress}https://example.com:1234/foo");
342         cacheObjectValues.variantMap = variantMap;
343         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
344 
345         verifyHttpCacheEntryFromTestFile(serializer, testEntry, VARIANTMAP_TEST_SERIALIZED_NAME, reserializeFiles);
346     }
347 
348     /**
349      * Deserialize a cache entry with headers that use our pseudo-header prefix and need escaping.
350      *
351      * @throws Exception if anything goes wrong
352      */
353     @Test
354     public void escapedHeaderTestFromPreviouslySerialized() throws Exception {
355         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
356         cacheObjectValues.responseHeaders = new Header[] {
357                 new BasicHeader("hc-test-1", "hc-test-1-value"),
358                 new BasicHeader("hc-sk", "hc-sk-value"),
359                 new BasicHeader("hc-resp-date", "hc-resp-date-value"),
360                 new BasicHeader("hc-req-date-date", "hc-req-date-value"),
361                 new BasicHeader("hc-varmap-key", "hc-varmap-key-value"),
362                 new BasicHeader("hc-varmap-val", "hc-varmap-val-value"),
363         };
364         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
365 
366         verifyHttpCacheEntryFromTestFile(serializer, testEntry, ESCAPED_HEADER_TEST_SERIALIZED_NAME, reserializeFiles);
367     }
368 
369     /**
370      * Deserialize a cache entry with no body, from a previously saved file.
371      *
372      * @throws Exception if anything goes wrong
373      */
374     @Test
375     public void noBodyTestFromPreviouslySerialized() throws Exception {
376         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
377         cacheObjectValues.resource = null;
378         cacheObjectValues.responseCode = 204;
379 
380         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
381 
382         verifyHttpCacheEntryFromTestFile(serializer, testEntry, NO_BODY_TEST_SERIALIZED_NAME, reserializeFiles);
383     }
384 
385     /**
386      * Test an HttpException being thrown while serializing.
387      *
388      * @throws Exception is expected
389      */
390     @Test
391     public void testSerializeWithHTTPException() throws Exception {
392         final AbstractMessageWriter<SimpleHttpResponse> throwyHttpWriter = Mockito.mock(AbstractMessageWriter.class);
393         Mockito.
394                 doThrow(new HttpException("Test Exception")).
395                 when(throwyHttpWriter).
396                 write(Mockito.any(SimpleHttpResponse.class), Mockito.any(SessionOutputBuffer.class), Mockito.any(OutputStream.class));
397 
398         final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
399         final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
400 
401         final HttpByteArrayCacheEntrySerializer testSerializer = new HttpByteArrayCacheEntrySerializer() {
402             @Override
403             protected AbstractMessageWriter<SimpleHttpResponse> makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) {
404                 return throwyHttpWriter;
405             }
406         };
407         Assertions.assertThrows(ResourceIOException.class, () ->
408                 testSerializer.serialize(testEntry));
409     }
410 
411 
412 }