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.impl.cache;
28
29
30 import static org.junit.jupiter.api.Assertions.assertEquals;
31 import static org.junit.jupiter.api.Assertions.assertNotEquals;
32 import static org.junit.jupiter.api.Assertions.assertTrue;
33
34 import java.io.ByteArrayInputStream;
35 import java.io.IOException;
36 import java.time.Instant;
37 import java.util.concurrent.ScheduledExecutorService;
38 import java.util.concurrent.ScheduledThreadPoolExecutor;
39
40 import org.apache.hc.client5.http.HttpRoute;
41 import org.apache.hc.client5.http.cache.HttpCacheContext;
42 import org.apache.hc.client5.http.classic.ExecChain;
43 import org.apache.hc.client5.http.classic.ExecRuntime;
44 import org.apache.hc.core5.http.ClassicHttpRequest;
45 import org.apache.hc.core5.http.ClassicHttpResponse;
46 import org.apache.hc.core5.http.HttpEntity;
47 import org.apache.hc.core5.http.HttpException;
48 import org.apache.hc.core5.http.HttpHost;
49 import org.apache.hc.core5.http.HttpStatus;
50 import org.apache.hc.core5.http.io.entity.InputStreamEntity;
51 import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
52 import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
53 import org.junit.jupiter.api.AfterEach;
54 import org.junit.jupiter.api.BeforeEach;
55 import org.junit.jupiter.api.Test;
56 import org.mockito.Mock;
57 import org.mockito.Mockito;
58 import org.mockito.MockitoAnnotations;
59
60
61
62
63
64
65 class TestRFC5861Compliance {
66
67 static final int MAX_BYTES = 1024;
68 static final int MAX_ENTRIES = 100;
69 static final int ENTITY_LENGTH = 128;
70
71 HttpHost host;
72 HttpRoute route;
73 HttpEntity body;
74 HttpCacheContext context;
75 @Mock
76 ExecChain mockExecChain;
77 @Mock
78 ExecRuntime mockExecRuntime;
79 ClassicHttpRequest request;
80 ClassicHttpResponse originResponse;
81 CacheConfig config;
82 CachingExec impl;
83 HttpCache cache;
84 ScheduledExecutorService executorService;
85
86 @BeforeEach
87 void setUp() throws Exception {
88 MockitoAnnotations.openMocks(this);
89
90 host = new HttpHost("foo.example.com", 80);
91
92 route = new HttpRoute(host);
93
94 body = HttpTestUtils.makeBody(ENTITY_LENGTH);
95
96 request = new BasicClassicHttpRequest("GET", "/foo");
97
98 context = HttpCacheContext.create();
99
100 originResponse = HttpTestUtils.make200Response();
101
102 config = CacheConfig.custom()
103 .setMaxCacheEntries(MAX_ENTRIES)
104 .setMaxObjectSize(MAX_BYTES)
105 .build();
106
107 cache = new BasicHttpCache(config);
108 impl = new CachingExec(cache, null, config);
109
110 executorService = new ScheduledThreadPoolExecutor(1);
111
112 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(originResponse);
113 Mockito.when(mockExecRuntime.fork(null)).thenReturn(mockExecRuntime);
114 }
115
116 @AfterEach
117 void cleanup() {
118 executorService.shutdownNow();
119 }
120
121 public ClassicHttpResponse execute(final ClassicHttpRequest request) throws IOException, HttpException {
122 return impl.execute(
123 ClassicRequestBuilder.copy(request).build(),
124 new ExecChain.Scope("test", route, request, mockExecRuntime, context),
125 mockExecChain);
126 }
127
128 @Test
129 void testConsumesErrorResponseWhenServingStale() throws Exception {
130 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
131 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
132 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
133 "public, max-age=5, stale-if-error=60");
134
135 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
136 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
137 final byte[] body101 = HttpTestUtils.makeRandomBytes(101);
138 final ByteArrayInputStream cis = Mockito.spy(new ByteArrayInputStream(body101));
139 final HttpEntity entity = new InputStreamEntity(cis, 101, null);
140 resp2.setEntity(entity);
141
142 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
143
144 execute(req1);
145
146 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
147
148 execute(req2);
149
150 Mockito.verify(cis).close();
151 }
152
153 @Test
154 void testStaleIfErrorInResponseYieldsToMustRevalidate() throws Exception {
155 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
156 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
157 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
158 "public, max-age=5, stale-if-error=60, must-revalidate");
159
160 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
161 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
162
163 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
164
165 execute(req1);
166
167 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
168
169 final ClassicHttpResponse result = execute(req2);
170
171 assertNotEquals(HttpStatus.SC_OK, result.getCode());
172 }
173
174 @Test
175 void testStaleIfErrorInResponseYieldsToProxyRevalidateForSharedCache() throws Exception {
176 assertTrue(config.isSharedCache());
177 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
178 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
179 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
180 "public, max-age=5, stale-if-error=60, proxy-revalidate");
181
182 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
183 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
184
185 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
186
187 execute(req1);
188
189 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
190
191 final ClassicHttpResponse result = execute(req2);
192
193 assertNotEquals(HttpStatus.SC_OK, result.getCode());
194 }
195
196 @Test
197 void testStaleIfErrorInResponseYieldsToExplicitFreshnessRequest() throws Exception {
198 final Instant tenSecondsAgo = Instant.now().minusSeconds(10);
199 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
200 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
201 "public, max-age=5, stale-if-error=60");
202
203 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
204 req2.setHeader("Cache-Control","min-fresh=2");
205 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
206
207 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
208
209 execute(req1);
210
211 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
212
213 final ClassicHttpResponse result = execute(req2);
214
215 assertNotEquals(HttpStatus.SC_OK, result.getCode());
216 }
217
218 @Test
219 void testStaleIfErrorInResponseIsFalseReturnsError() throws Exception {
220 final Instant now = Instant.now();
221 final Instant tenSecondsAgo = now.minusSeconds(10);
222 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
223 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
224 "public, max-age=5, stale-if-error=2");
225
226 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
227 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
228
229 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
230
231 execute(req1);
232
233 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
234
235 final ClassicHttpResponse result = execute(req2);
236
237 assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR,
238 result.getCode());
239 }
240
241 @Test
242 void testStaleIfErrorInRequestIsFalseReturnsError() throws Exception {
243 final Instant now = Instant.now();
244 final Instant tenSecondsAgo = now.minusSeconds(10);
245 final ClassicHttpRequest req1 = HttpTestUtils.makeDefaultRequest();
246 final ClassicHttpResponse resp1 = HttpTestUtils.make200Response(tenSecondsAgo,
247 "public, max-age=5");
248
249 final ClassicHttpRequest req2 = HttpTestUtils.makeDefaultRequest();
250 req2.setHeader("Cache-Control","stale-if-error=2");
251 final ClassicHttpResponse resp2 = HttpTestUtils.make500Response();
252
253 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp1);
254
255 execute(req1);
256
257 Mockito.when(mockExecChain.proceed(Mockito.any(), Mockito.any())).thenReturn(resp2);
258
259 final ClassicHttpResponse result = execute(req2);
260
261 assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, result.getCode());
262 }
263
264 }