View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.jcabi.http.wire;
6   
7   import com.jcabi.http.Request;
8   import com.jcabi.http.mock.MkAnswer;
9   import com.jcabi.http.mock.MkContainer;
10  import com.jcabi.http.mock.MkGrizzlyContainer;
11  import com.jcabi.http.mock.MkQuery;
12  import com.jcabi.http.request.JdkRequest;
13  import com.jcabi.http.response.RestResponse;
14  import jakarta.ws.rs.core.HttpHeaders;
15  import java.net.HttpURLConnection;
16  import java.util.Collections;
17  import java.util.Map;
18  import org.hamcrest.Description;
19  import org.hamcrest.Matcher;
20  import org.hamcrest.MatcherAssert;
21  import org.hamcrest.Matchers;
22  import org.hamcrest.TypeSafeMatcher;
23  import org.hamcrest.core.IsAnything;
24  import org.junit.jupiter.api.Test;
25  
26  /**
27   * Test case for {@link LastModifiedCachingWire}.
28   * @since 1.15
29   */
30  final class LastModifiedCachingWireTest {
31  
32      /**
33       * Test body.
34       * @todo #120:15min Clean tests shared fields and redundant variables
35       *  Move constants in this file to their tests because tests must share
36       *  nothing. Then also inline any redundant variables.
37       *  Please also configure pdd and est in.travis.yml as done e.g. in
38       *  https://github.com/jcabi/jcabi-xml/blob/master/.travis.yml
39       *  For first points explanation, read:
40       *  http://www.yegor256.com/2016/05/03/test-methods-must-share-nothing.html
41       *  http://www.yegor256.com/2015/09/01/redundant-variables-are-evil.html
42       * */
43      private static final String BODY = "Test body";
44  
45      /**
46       * Test body updated.
47       * */
48      private static final String BODY_UPDATED = "Test body updated";
49  
50      /**
51       * LastModifiedCachingWire can handle requests without headers.
52       * @throws Exception If fails
53       */
54      @Test
55      void requestWithoutHeaderPassed() throws Exception {
56          final MkContainer container = new MkGrizzlyContainer()
57              .next(
58                  new MkAnswer.Simple(
59                      HttpURLConnection.HTTP_OK, LastModifiedCachingWireTest.BODY
60                  )
61              ).start();
62          try {
63              final Request req = new JdkRequest(container.home())
64                  .through(LastModifiedCachingWire.class);
65              req.fetch().as(RestResponse.class)
66                  .assertStatus(HttpURLConnection.HTTP_OK)
67                  .assertBody(Matchers.equalTo(LastModifiedCachingWireTest.BODY));
68              MatcherAssert.assertThat("should be equal 1", container.queries(), Matchers.equalTo(1));
69          } finally {
70              container.stop();
71          }
72      }
73  
74      /**
75       * LastModifiedCachingWire can cache GET requests.
76       * @throws Exception If fails
77       */
78      @Test
79      void cachesGetRequest() throws Exception {
80          final Map<String, String> headers = Collections.singletonMap(
81              HttpHeaders.LAST_MODIFIED,
82              "Wed, 15 Nov 1995 04:58:08 GMT"
83          );
84          final int count = 10;
85          final MkContainer container = new MkGrizzlyContainer()
86              .next(
87                  new MkAnswer.Simple(
88                      HttpURLConnection.HTTP_OK,
89                      headers.entrySet(),
90                      LastModifiedCachingWireTest.BODY.getBytes()
91                  )
92              )
93              .next(
94                  new MkAnswer.Simple(HttpURLConnection.HTTP_NOT_MODIFIED),
95                  new IsAnything<MkQuery>(),
96                  count
97              ).start();
98          try {
99              final Request req = new JdkRequest(container.home())
100                 .through(LastModifiedCachingWire.class);
101             for (int idx = 0; idx < count; ++idx) {
102                 req
103                     .fetch()
104                     .as(RestResponse.class)
105                     .assertStatus(HttpURLConnection.HTTP_OK)
106                     .assertBody(
107                         Matchers.equalTo(LastModifiedCachingWireTest.BODY)
108                     );
109             }
110             MatcherAssert.assertThat(
111                 "should be equal to count", container.queries(), Matchers.equalTo(count)
112             );
113         } finally {
114             container.stop();
115         }
116     }
117 
118     /**
119      * LastModifiedCachingWire can evict any previous cached entry if a new
120      * response does not have a last modified header.
121      * We can observe this via the If-Modified-Since headers as when the cache
122      * does not contain an entry, this is not present on the request.
123      * @throws Exception If fails
124      */
125     @Test
126     void doesNotCacheGetRequestIfTheLastModifiedHeaderIsMissing()
127         throws Exception {
128         final String first = "Body 1";
129         final String second = "Body 2";
130         final String third = "Body 3";
131         final MkContainer container = new MkGrizzlyContainer()
132             .next(
133                 new MkAnswer.Simple(
134                     HttpURLConnection.HTTP_OK,
135                     Collections.singletonMap(
136                         HttpHeaders.LAST_MODIFIED,
137                         "Wed, 15 Nov 1995 05:58:08 GMT"
138                     ).entrySet(),
139                     first.getBytes()
140                 ),
141                 Matchers.not(queryContainsIfModifiedSinceHeader())
142             )
143             .next(
144                 new MkAnswer.Simple(
145                     HttpURLConnection.HTTP_OK,
146                     Collections.<Map.Entry<String, String>>emptySet(),
147                     second.getBytes()
148                 ),
149                 queryContainsIfModifiedSinceHeader()
150             )
151             .next(
152                 new MkAnswer.Simple(
153                     HttpURLConnection.HTTP_OK,
154                     Collections.<Map.Entry<String, String>>emptySet(),
155                     third.getBytes()
156                 ),
157                 Matchers.not(queryContainsIfModifiedSinceHeader())
158             ).start();
159         try {
160             final Request req = new JdkRequest(container.home())
161                 .through(LastModifiedCachingWire.class);
162             req.fetch().as(RestResponse.class)
163                 .assertStatus(HttpURLConnection.HTTP_OK)
164                 .assertBody(Matchers.equalTo(first));
165             req.fetch().as(RestResponse.class)
166                 .assertStatus(HttpURLConnection.HTTP_OK)
167                 .assertBody(Matchers.equalTo(second));
168             req.fetch().as(RestResponse.class)
169                 .assertStatus(HttpURLConnection.HTTP_OK)
170                 .assertBody(Matchers.equalTo(third));
171         } finally {
172             container.stop();
173         }
174     }
175 
176     /**
177      * LastModifiedCachingWire can resist cache eviction in the event of a non
178      * OK response without a last modified header.
179      * @throws Exception If fails
180      * @todo #120:30min Confirm cache clearing behaviour in all non-OK responses
181      *  Non-OK behaviour was not specified in #120, so for example, if the
182      *  response is 404 as below, does it make any sense to keep the item in
183      *  cache? Is it likely a server will respond 404, and then later the exact
184      *  unmodified content is available again. I think they all need to be
185      *  thought about, another dubious response might be 301 Moved Permanently,
186      *  or 410 Gone etc. Or, personally I think all non-OK and OK responses
187      *  should behave the same WRT to clearing the cache as the cache value is
188      *  so unlikely to be returned in future.
189      */
190     @Test
191     void doesNotEvictCacheOnNonOk()
192         throws Exception {
193         final String body = "Body";
194         final MkContainer container = new MkGrizzlyContainer()
195             .next(
196                 new MkAnswer.Simple(
197                     HttpURLConnection.HTTP_OK,
198                     Collections.singletonMap(
199                         HttpHeaders.LAST_MODIFIED,
200                         "Wed, 15 Nov 1995 06:58:08 GMT"
201                     ).entrySet(),
202                     body.getBytes()
203                 ),
204                 Matchers.not(queryContainsIfModifiedSinceHeader())
205             )
206             .next(
207                 new MkAnswer.Simple(HttpURLConnection.HTTP_NOT_FOUND),
208                 queryContainsIfModifiedSinceHeader()
209             )
210             .next(
211                 new MkAnswer.Simple(HttpURLConnection.HTTP_NOT_MODIFIED),
212                 queryContainsIfModifiedSinceHeader()
213             ).start();
214         try {
215             final Request req = new JdkRequest(container.home())
216                 .through(LastModifiedCachingWire.class);
217             req
218                 .fetch()
219                 .as(RestResponse.class)
220                 .assertStatus(HttpURLConnection.HTTP_OK)
221                 .assertBody(
222                     Matchers.equalTo(body)
223                 );
224             req
225                 .fetch()
226                 .as(RestResponse.class)
227                 .assertStatus(HttpURLConnection.HTTP_NOT_FOUND);
228             req
229                 .fetch()
230                 .as(RestResponse.class)
231                 .assertStatus(HttpURLConnection.HTTP_OK)
232                 .assertBody(
233                     Matchers.equalTo(body)
234                 );
235         } finally {
236             container.stop();
237         }
238     }
239 
240     /**
241      * LastModifiedCachingWire cache updates with newer response.
242      * @throws Exception If fails
243      */
244     @Test
245     void cacheUpdateNewerResponse() throws Exception {
246         final Map<String, String> headers = Collections.singletonMap(
247             HttpHeaders.LAST_MODIFIED,
248             "Wed, 16 Nov 1995 04:58:08 GMT"
249         );
250         final MkContainer container = new MkGrizzlyContainer()
251             .next(
252                 new MkAnswer.Simple(
253                     HttpURLConnection.HTTP_OK,
254                     headers.entrySet(),
255                     LastModifiedCachingWireTest.BODY.getBytes()
256                 )
257             )
258             .next(new MkAnswer.Simple(HttpURLConnection.HTTP_NOT_MODIFIED))
259             .next(
260                 new MkAnswer.Simple(
261                     HttpURLConnection.HTTP_OK,
262                     headers.entrySet(),
263                     LastModifiedCachingWireTest.BODY_UPDATED.getBytes()
264                 )
265             )
266             .next(new MkAnswer.Simple(HttpURLConnection.HTTP_NOT_MODIFIED))
267             .start();
268         try {
269             final Request req = new JdkRequest(container.home())
270                 .through(LastModifiedCachingWire.class);
271             for (int idx = 0; idx < 2; ++idx) {
272                 req
273                     .fetch()
274                     .as(RestResponse.class)
275                     .assertStatus(HttpURLConnection.HTTP_OK)
276                     .assertBody(
277                         Matchers.equalTo(LastModifiedCachingWireTest.BODY)
278                     );
279             }
280             for (int idx = 0; idx < 2; ++idx) {
281                 req
282                     .fetch()
283                     .as(RestResponse.class)
284                     .assertStatus(HttpURLConnection.HTTP_OK)
285                     .assertBody(
286                         Matchers.equalTo(
287                             LastModifiedCachingWireTest.BODY_UPDATED
288                         )
289                     );
290             }
291             MatcherAssert.assertThat(
292                 "should be equal 4", container.queries(), Matchers.equalTo(2 + 2)
293             );
294         } finally {
295             container.stop();
296         }
297     }
298 
299     /**
300      * LastModifiedCachingWire can send a request directly
301      * if it contains the "If-Modified-Since" header.
302      * @throws Exception - if the test fails
303      */
304     @Test
305     void sendsRequestDirectly() throws Exception {
306         final MkContainer container = new MkGrizzlyContainer()
307             .next(
308                 new MkAnswer.Simple(
309                     HttpURLConnection.HTTP_OK, LastModifiedCachingWireTest.BODY
310                 )
311             )
312             .next(
313                 new MkAnswer.Simple(
314                     HttpURLConnection.HTTP_OK, LastModifiedCachingWireTest.BODY
315                 )
316             )
317             .start();
318         try {
319             final Request req = new JdkRequest(container.home())
320                 .through(LastModifiedCachingWire.class).header(
321                     HttpHeaders.IF_MODIFIED_SINCE,
322                     "Fri, 01 Jan 2016 00:00:00 GMT"
323                 );
324             for (int idx = 0; idx < 2; ++idx) {
325                 req
326                     .fetch()
327                     .as(RestResponse.class)
328                     .assertStatus(HttpURLConnection.HTTP_OK)
329                     .assertBody(
330                         Matchers.equalTo(LastModifiedCachingWireTest.BODY)
331                     );
332             }
333             MatcherAssert.assertThat("should be eq 2", container.queries(), Matchers.equalTo(2));
334         } finally {
335             container.stop();
336         }
337     }
338 
339     /**
340      * A Matcher that tests for the presence of the If-Modified-Since header.
341      * @return The query matcher
342      */
343     private static Matcher<MkQuery> queryContainsIfModifiedSinceHeader() {
344         return LastModifiedCachingWireTest.queryContainingHeader(
345             "If-Modified-Since"
346         );
347     }
348 
349     /**
350      * Provides a MkQuery matcher that tests if the request contains the
351      * specified header.
352      * @param header The header to look for
353      * @return A matcher which tests for the supplied header
354      */
355     private static Matcher<MkQuery> queryContainingHeader(final String header) {
356         return new TypeSafeMatcher<MkQuery>() {
357             @Override
358             protected boolean matchesSafely(final MkQuery query) {
359                 return query.headers().containsKey(header);
360             }
361 
362             @Override
363             public void describeTo(final Description description) {
364                 description.appendText("contains ");
365                 description.appendText(header);
366             }
367         };
368     }
369 }