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