1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package org.apache.hc.client5.http.cache;
28
29 import java.time.Instant;
30 import java.util.HashSet;
31 import java.util.Set;
32
33 import org.apache.hc.client5.http.HeadersMatcher;
34 import org.apache.hc.client5.http.impl.cache.ContainsHeaderMatcher;
35 import org.apache.hc.client5.http.impl.cache.HttpCacheEntryMatcher;
36 import org.apache.hc.client5.http.impl.cache.HttpTestUtils;
37 import org.apache.hc.client5.http.utils.DateUtils;
38 import org.apache.hc.core5.http.ContentType;
39 import org.apache.hc.core5.http.Header;
40 import org.apache.hc.core5.http.HttpHeaders;
41 import org.apache.hc.core5.http.HttpHost;
42 import org.apache.hc.core5.http.HttpRequest;
43 import org.apache.hc.core5.http.HttpResponse;
44 import org.apache.hc.core5.http.HttpStatus;
45 import org.apache.hc.core5.http.Method;
46 import org.apache.hc.core5.http.message.BasicHeader;
47 import org.apache.hc.core5.http.message.BasicHttpRequest;
48 import org.apache.hc.core5.http.message.BasicHttpResponse;
49 import org.apache.hc.core5.http.message.HeaderGroup;
50 import org.hamcrest.MatcherAssert;
51 import org.junit.jupiter.api.Assertions;
52 import org.junit.jupiter.api.BeforeEach;
53 import org.junit.jupiter.api.Test;
54
55 class TestHttpCacheEntryFactory {
56
57 private Instant requestDate;
58 private Instant responseDate;
59
60 private HttpCacheEntry entry;
61 private Instant now;
62 private Instant oneSecondAgo;
63 private Instant twoSecondsAgo;
64 private Instant eightSecondsAgo;
65 private Instant tenSecondsAgo;
66 private HttpHost host;
67 private HttpRequest request;
68 private HttpResponse response;
69 private HttpCacheEntryFactory impl;
70
71 @BeforeEach
72 void setUp() {
73 requestDate = Instant.now().minusSeconds(1);
74 responseDate = Instant.now();
75
76 now = Instant.now();
77 oneSecondAgo = now.minusSeconds(1);
78 twoSecondsAgo = now.minusSeconds(2);
79 eightSecondsAgo = now.minusSeconds(8);
80 tenSecondsAgo = now.minusSeconds(10);
81
82 host = new HttpHost("foo.example.com");
83 request = new BasicHttpRequest("GET", "/stuff");
84 response = new BasicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
85
86 impl = new HttpCacheEntryFactory();
87 }
88
89 @Test
90 void testFilterHopByHopAndConnectionSpecificHeaders() {
91 response.setHeaders(
92 new BasicHeader(HttpHeaders.CONNECTION, "blah, blah, this, that"),
93 new BasicHeader("Blah", "huh?"),
94 new BasicHeader("BLAH", "huh?"),
95 new BasicHeader("this", "huh?"),
96 new BasicHeader("That", "huh?"),
97 new BasicHeader("Keep-Alive", "timeout, max=20"),
98 new BasicHeader("X-custom", "my stuff"),
99 new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.TEXT_PLAIN.toString()),
100 new BasicHeader(HttpHeaders.CONTENT_LENGTH, "111"));
101 final HeaderGroup filteredHeaders = HttpCacheEntryFactory.filterHopByHopHeaders(response);
102 MatcherAssert.assertThat(filteredHeaders.getHeaders(), HeadersMatcher.same(
103 new BasicHeader("X-custom", "my stuff"),
104 new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.TEXT_PLAIN.toString())
105 ));
106 }
107
108 @Test
109 void testHeadersAreMergedCorrectly() {
110 entry = HttpTestUtils.makeCacheEntry(
111 new BasicHeader("Date", DateUtils.formatStandardDate(responseDate)),
112 new BasicHeader("ETag", "\"etag\""));
113
114 final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
115
116 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(responseDate)));
117 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
118 }
119
120 @Test
121 void testNewerHeadersReplaceExistingHeaders() {
122 entry = HttpTestUtils.makeCacheEntry(
123 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
124 new BasicHeader("Cache-Control", "private"),
125 new BasicHeader("ETag", "\"etag\""),
126 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(requestDate)),
127 new BasicHeader("Cache-Control", "max-age=0"));
128
129 response.setHeaders(
130 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
131 new BasicHeader("Cache-Control", "public"));
132
133 final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
134
135 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(requestDate)));
136 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
137 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Last-Modified", DateUtils.formatStandardDate(responseDate)));
138 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Cache-Control", "public"));
139 }
140
141 @Test
142 void testNewHeadersAreAddedByMerge() {
143 entry = HttpTestUtils.makeCacheEntry(
144 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
145 new BasicHeader("ETag", "\"etag\""));
146 response.setHeaders(
147 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
148 new BasicHeader("Cache-Control", "public"));
149
150 final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
151
152 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(requestDate)));
153 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
154 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Last-Modified", DateUtils.formatStandardDate(responseDate)));
155 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Cache-Control", "public"));
156 }
157
158 @Test
159 void entryWithMalformedDateIsStillUpdated() {
160 entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
161 new BasicHeader("ETag", "\"old\""),
162 new BasicHeader("Date", "bad-date"));
163 response.setHeader("ETag", "\"new\"");
164 response.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
165
166 final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
167
168 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"new\""));
169 }
170
171 @Test
172 void entryIsStillUpdatedByResponseWithMalformedDate() {
173 entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
174 new BasicHeader("ETag", "\"old\""),
175 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
176 response.setHeader("ETag", "\"new\"");
177 response.setHeader("Date", "bad-date");
178
179 final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
180
181 MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"new\""));
182 }
183
184 @Test
185 void testUpdateCacheEntryReturnsDifferentEntryInstance() {
186 entry = HttpTestUtils.makeCacheEntry();
187 final HttpCacheEntry newEntry = impl.createUpdated(requestDate, responseDate, host, request, response, entry);
188 Assertions.assertNotSame(newEntry, entry);
189 }
190
191 @Test
192 void testCreateRootVariantEntry() {
193 request.setHeaders(
194 new BasicHeader("Keep-Alive", "timeout, max=20"),
195 new BasicHeader("X-custom", "my stuff"),
196 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
197 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
198 );
199 response.setHeaders(
200 new BasicHeader(HttpHeaders.TRANSFER_ENCODING, "identity"),
201 new BasicHeader(HttpHeaders.CONNECTION, "Keep-Alive, Blah"),
202 new BasicHeader("Blah", "huh?"),
203 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
204 new BasicHeader(HttpHeaders.VARY, "Stuff"),
205 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
206 new BasicHeader("X-custom", "my stuff")
207 );
208
209 final HttpCacheEntry newEntry = impl.create(tenSecondsAgo, oneSecondAgo, host, request, response, HttpTestUtils.makeRandomResource(1024));
210
211 final Set<String> variants = new HashSet<>();
212 variants.add("variant1");
213 variants.add("variant2");
214 variants.add("variant3");
215
216 final HttpCacheEntry newRoot = impl.createRoot(newEntry, variants);
217
218 MatcherAssert.assertThat(newRoot, HttpCacheEntryMatcher.equivalent(
219 HttpTestUtils.makeCacheEntry(
220 tenSecondsAgo,
221 oneSecondAgo,
222 Method.GET,
223 "http://foo.example.com:80/stuff",
224 new Header[]{
225 new BasicHeader("X-custom", "my stuff"),
226 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
227 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
228 },
229 HttpStatus.SC_NOT_MODIFIED,
230 new Header[]{
231 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
232 new BasicHeader(HttpHeaders.VARY, "Stuff"),
233 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
234 new BasicHeader("X-custom", "my stuff")
235 },
236 variants
237 )));
238
239 Assertions.assertTrue(newRoot.hasVariants());
240 Assertions.assertNull(newRoot.getResource());
241 }
242
243 @Test
244 void testCreateResourceEntry() {
245 request.setHeaders(
246 new BasicHeader("Keep-Alive", "timeout, max=20"),
247 new BasicHeader("X-custom", "my stuff"),
248 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
249 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de"),
250 new BasicHeader(HttpHeaders.AUTHORIZATION, "Super secret")
251 );
252 response.setHeaders(
253 new BasicHeader(HttpHeaders.TRANSFER_ENCODING, "identity"),
254 new BasicHeader(HttpHeaders.CONNECTION, "Keep-Alive, Blah"),
255 new BasicHeader("Blah", "huh?"),
256 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
257 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
258 new BasicHeader("X-custom", "my stuff")
259 );
260
261 final Resource resource = HttpTestUtils.makeRandomResource(128);
262 final HttpCacheEntry newEntry = impl.create(tenSecondsAgo, oneSecondAgo, host, request, response, resource);
263
264 MatcherAssert.assertThat(newEntry, HttpCacheEntryMatcher.equivalent(
265 HttpTestUtils.makeCacheEntry(
266 tenSecondsAgo,
267 oneSecondAgo,
268 Method.GET,
269 "http://foo.example.com:80/stuff",
270 new Header[]{
271 new BasicHeader("X-custom", "my stuff"),
272 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
273 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
274 },
275 HttpStatus.SC_NOT_MODIFIED,
276 new Header[]{
277 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
278 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
279 new BasicHeader("X-custom", "my stuff")
280 },
281 resource
282 )));
283
284 Assertions.assertFalse(newEntry.hasVariants());
285 }
286
287 @Test
288 void testCreateUpdatedResourceEntry() {
289 final Resource resource = HttpTestUtils.makeRandomResource(128);
290 final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
291 tenSecondsAgo,
292 twoSecondsAgo,
293 Method.GET,
294 "/stuff",
295 new Header[]{
296 new BasicHeader("X-custom", "my stuff"),
297 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
298 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
299 },
300 HttpStatus.SC_NOT_MODIFIED,
301 new Header[]{
302 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
303 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
304 new BasicHeader("X-custom", "my stuff"),
305 new BasicHeader("Cache-Control", "max-age=0")
306 },
307 resource
308 );
309
310 response.setHeaders(
311 new BasicHeader(HttpHeaders.ETAG, "\"some-new-etag\""),
312 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(oneSecondAgo)),
313 new BasicHeader("Cache-Control", "public")
314 );
315
316 request.setHeaders(
317 new BasicHeader("X-custom", "my other stuff"),
318 new BasicHeader(HttpHeaders.ACCEPT, "stuff, other-stuff"),
319 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
320 );
321
322 final HttpCacheEntry updatedEntry = impl.createUpdated(tenSecondsAgo, oneSecondAgo, host, request, response, entry);
323
324 MatcherAssert.assertThat(updatedEntry, HttpCacheEntryMatcher.equivalent(
325 HttpTestUtils.makeCacheEntry(
326 tenSecondsAgo,
327 oneSecondAgo,
328 Method.GET,
329 "http://foo.example.com:80/stuff",
330 new Header[]{
331 new BasicHeader("X-custom", "my other stuff"),
332 new BasicHeader(HttpHeaders.ACCEPT, "stuff, other-stuff"),
333 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
334 },
335 HttpStatus.SC_NOT_MODIFIED,
336 new Header[]{
337 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
338 new BasicHeader("X-custom", "my stuff"),
339 new BasicHeader(HttpHeaders.ETAG, "\"some-new-etag\""),
340 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(oneSecondAgo)),
341 new BasicHeader("Cache-Control", "public")
342 },
343 resource
344 )));
345
346 Assertions.assertFalse(updatedEntry.hasVariants());
347 }
348
349 @Test
350 void testUpdateNotModifiedIfResponseOlder() {
351 entry = HttpTestUtils.makeCacheEntry(twoSecondsAgo, now,
352 new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
353 new BasicHeader("ETag", "\"new-etag\""));
354 response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
355 response.setHeader("ETag", "\"old-etag\"");
356
357 final HttpCacheEntry newEntry = impl.createUpdated(Instant.now(), Instant.now(), host, request, response, entry);
358
359 Assertions.assertSame(newEntry, entry);
360 }
361
362 @Test
363 void testUpdateHasLatestRequestAndResponseDates() {
364 entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo);
365 final HttpCacheEntry updated = impl.createUpdated(twoSecondsAgo, oneSecondAgo, host, request, response, entry);
366
367 Assertions.assertEquals(twoSecondsAgo, updated.getRequestInstant());
368 Assertions.assertEquals(oneSecondAgo, updated.getResponseInstant());
369 }
370
371 @Test
372 void cannotUpdateFromANon304OriginResponse() {
373 entry = HttpTestUtils.makeCacheEntry();
374 response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
375 Assertions.assertThrows(IllegalArgumentException.class, () ->
376 impl.createUpdated(Instant.now(), Instant.now(), host, request, response, entry));
377 }
378
379 }