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