View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   
6   package com.jcabi.http;
7   
8   import com.jcabi.http.request.BaseRequest;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.net.URI;
12  import java.util.Collection;
13  import java.util.Map;
14  import java.util.concurrent.Callable;
15  import java.util.function.Supplier;
16  import org.hamcrest.MatcherAssert;
17  import org.hamcrest.Matchers;
18  import org.junit.jupiter.api.Test;
19  import org.junit.jupiter.params.ParameterizedTest;
20  import org.mockito.ArgumentCaptor;
21  import org.mockito.ArgumentMatchers;
22  import org.mockito.Mockito;
23  
24  /**
25   * Test case for loss of timeout parameters.
26   * @since 1.17.3
27   */
28  @SuppressWarnings("PMD.TooManyMethods")
29  final class RequestTimeoutLossTest extends RequestTestTemplate {
30      /**
31       * Placeholder URL used for testing purposes only.
32       */
33      private static final String LOCALHOST_URL = "http://localhost";
34  
35      /**
36       * Content type header name for testing purposes only.
37       */
38      private static final String CONTENT_TYPE = "Content-Type";
39  
40      /**
41       * Magic number for connection timeout.
42       */
43      private static final int CONNECT_TIMEOUT = 1234;
44  
45      /**
46       * Magic number for read timeout.
47       */
48      private static final int READ_TIMEOUT = 2345;
49  
50      /**
51       * The connect and read timeouts are properly set no matter in which order
52       * {@code Request.timeout} is called.
53       *
54       * @param type Type of Request
55       * @throws Exception If something goes wrong inside
56       */
57      @ParameterizedTest
58      @Values
59      void testTimeoutOrderDoesntMatterBeforeBody(
60          final Class<? extends Request> type
61      )
62          throws Exception {
63          final Callable<Response> execution = () -> RequestTimeoutLossTest.request(type)
64              .through(MockWire.class)
65              .method(Request.GET)
66              .timeout(
67                  RequestTimeoutLossTest.CONNECT_TIMEOUT,
68                  RequestTimeoutLossTest.READ_TIMEOUT
69              )
70              .body()
71              .back()
72              .fetch();
73          this.testTimeoutOrderDoesntMatter(execution);
74      }
75  
76      /**
77       * The connect and read timeouts are properly set no matter in which order
78       * {@code Request.timeout} is called.
79       *
80       * @param type Type of Request
81       * @throws Exception If something goes wrong inside
82       */
83      @ParameterizedTest
84      @Values
85      void testTimeoutOrderDoesntMatterBeforeFetch(
86          final Class<? extends Request> type
87      )
88          throws Exception {
89          final Callable<Response> execution = () -> RequestTimeoutLossTest.request(type)
90              .through(MockWire.class)
91              .method(Request.GET)
92              .timeout(
93                  RequestTimeoutLossTest.CONNECT_TIMEOUT,
94                  RequestTimeoutLossTest.READ_TIMEOUT
95              )
96              .fetch();
97          this.testTimeoutOrderDoesntMatter(execution);
98      }
99  
100     /**
101      * The connect and read timeouts are properly set no matter in which order
102      * {@code Request.timeout} is called.
103      *
104      * @param type Type of Request
105      * @throws Exception If something goes wrong inside
106      */
107     @ParameterizedTest
108     @Values
109     void testTimeoutOrderDoesntMatterBeforeHeader(
110         final Class<? extends Request> type
111     )
112         throws Exception {
113         final Callable<Response> execution = () -> RequestTimeoutLossTest.request(type)
114             .through(MockWire.class)
115             .method(Request.GET)
116             .timeout(
117                 RequestTimeoutLossTest.CONNECT_TIMEOUT,
118                 RequestTimeoutLossTest.READ_TIMEOUT
119             )
120             .header(
121                 RequestTimeoutLossTest.CONTENT_TYPE,
122                 "text/plain"
123             )
124             .fetch();
125         this.testTimeoutOrderDoesntMatter(execution);
126     }
127 
128     /**
129      * The connect and read timeouts are properly set no matter in which order
130      * {@code Request.timeout} is called.
131      *
132      * @param type Type of Request
133      * @throws Exception If something goes wrong inside
134      */
135     @ParameterizedTest
136     @Values
137     void testTimeoutOrderDoesntMatterBeforeMethod(
138         final Class<? extends Request> type
139     )
140         throws Exception {
141         final Callable<Response> execution = () -> RequestTimeoutLossTest.request(type)
142             .through(MockWire.class)
143             .timeout(
144                 RequestTimeoutLossTest.CONNECT_TIMEOUT,
145                 RequestTimeoutLossTest.READ_TIMEOUT
146             )
147             .method(Request.GET)
148             .fetch();
149         this.testTimeoutOrderDoesntMatter(execution);
150     }
151 
152     /**
153      * The connect and read timeouts are properly set no matter in which order
154      * {@code Request.timeout} is called.
155      *
156      * @param type Type of Request
157      * @throws Exception If something goes wrong inside
158      */
159     @ParameterizedTest
160     @Values
161     void testTimeoutOrderDoesntMatterBeforeMultipartBody(
162         final Class<? extends Request> type
163     )
164         throws Exception {
165         final Callable<Response> execution = () -> RequestTimeoutLossTest.request(type)
166             .through(MockWire.class)
167             .method(Request.GET)
168             .timeout(
169                 RequestTimeoutLossTest.CONNECT_TIMEOUT,
170                 RequestTimeoutLossTest.READ_TIMEOUT
171             )
172             .multipartBody()
173             .back()
174             .fetch();
175         this.testTimeoutOrderDoesntMatter(execution);
176     }
177 
178     /**
179      * The connect and read timeouts are properly set no matter in which order
180      * {@code Request.timeout} is called.
181      *
182      * @param type Type of Request
183      * @throws Exception If something goes wrong inside
184      */
185     @ParameterizedTest
186     @Values
187     void testTimeoutOrderDoesntMatterBeforeReset(
188         final Class<? extends Request> type
189     )
190         throws Exception {
191         final Callable<Response> execution = () -> RequestTimeoutLossTest.request(type)
192             .through(MockWire.class)
193             .method(Request.GET)
194             .timeout(
195                 RequestTimeoutLossTest.CONNECT_TIMEOUT,
196                 RequestTimeoutLossTest.READ_TIMEOUT
197             )
198             .reset(RequestTimeoutLossTest.CONTENT_TYPE)
199             .fetch();
200         this.testTimeoutOrderDoesntMatter(execution);
201     }
202 
203     /**
204      * The connect and read timeouts are properly set no matter in which order
205      * {@code Request.timeout} is called.
206      *
207      * @param type Type of Request
208      * @throws Exception If something goes wrong inside
209      */
210     @ParameterizedTest
211     @Values
212     void testTimeoutOrderDoesntMatterBeforeUriBack(
213         final Class<? extends Request> type
214     )
215         throws Exception {
216         this.testTimeoutOrderDoesntMatter(
217             () -> RequestTimeoutLossTest.request(type)
218                 .through(MockWire.class)
219                 .method(Request.GET)
220                 .timeout(
221                     RequestTimeoutLossTest.CONNECT_TIMEOUT,
222                     RequestTimeoutLossTest.READ_TIMEOUT
223                 )
224                 .uri()
225                 .path("/api")
226                 .back()
227                 .fetch()
228         );
229     }
230 
231     /**
232      * The wire passed to method "through" is used.
233      *
234      * @throws IOException On error
235      */
236     @Test
237     void passesThroughWire() throws IOException {
238         final Wire original = Mockito.mock(Wire.class);
239         final Wire wire = Mockito.mock(Wire.class);
240         final Response response = Mockito.mock(Response.class);
241         final Supplier<Collection<Map.Entry<String, String>>> hdrs =
242             () -> ArgumentMatchers.anyCollection();
243         final String url = "fake-url";
244         Mockito.when(
245             wire.send(
246                 ArgumentMatchers.any(Request.class),
247                 ArgumentMatchers.eq(url),
248                 ArgumentMatchers.anyString(),
249                 hdrs.get(),
250                 ArgumentMatchers.any(InputStream.class),
251                 ArgumentMatchers.anyInt(),
252                 ArgumentMatchers.anyInt()
253             )
254         ).thenReturn(response);
255         new BaseRequest(original, url).through(wire).fetch();
256         Mockito.verify(original, Mockito.never()).send(
257             ArgumentMatchers.any(Request.class),
258             ArgumentMatchers.anyString(),
259             ArgumentMatchers.anyString(),
260             hdrs.get(),
261             ArgumentMatchers.any(InputStream.class),
262             ArgumentMatchers.anyInt(),
263             ArgumentMatchers.anyInt()
264         );
265         Mockito.verify(wire).send(
266             ArgumentMatchers.any(Request.class),
267             ArgumentMatchers.anyString(),
268             ArgumentMatchers.anyString(),
269             hdrs.get(),
270             ArgumentMatchers.any(InputStream.class),
271             ArgumentMatchers.anyInt(),
272             ArgumentMatchers.anyInt()
273         );
274     }
275 
276     /**
277      * The connect and read timeouts are properly set no matter in which order
278      * {@code Request.timeout} is called.
279      *
280      * @param exec The callable that contains the actual request
281      * @throws Exception If something goes wrong inside
282      */
283     @SuppressWarnings("unchecked")
284     private void testTimeoutOrderDoesntMatter(final Callable<Response> exec)
285         throws Exception {
286         synchronized (MockWire.class) {
287             final Wire wire = Mockito.mock(Wire.class);
288             final ArgumentCaptor<Integer> cnc = ArgumentCaptor
289                 .forClass(Integer.class);
290             final ArgumentCaptor<Integer> rdc = ArgumentCaptor
291                 .forClass(Integer.class);
292             MockWire.setMockDelegate(wire);
293             final Response response = Mockito.mock(Response.class);
294             Mockito.when(
295                 wire.send(
296                     Mockito.any(Request.class),
297                     Mockito.anyString(),
298                     Mockito.anyString(),
299                     Mockito.<Map.Entry<String, String>>anyCollection(),
300                     Mockito.any(InputStream.class),
301                     Mockito.anyInt(),
302                     Mockito.anyInt()
303                 )
304             ).thenReturn(response);
305             exec.call();
306             Mockito.verify(wire).send(
307                 Mockito.any(Request.class),
308                 Mockito.anyString(),
309                 Mockito.anyString(),
310                 Mockito.<Map.Entry<String, String>>anyCollection(),
311                 Mockito.any(InputStream.class),
312                 cnc.capture(),
313                 rdc.capture()
314             );
315             MatcherAssert.assertThat(
316                 "should be connect timeout",
317                 cnc.getValue().intValue(),
318                 Matchers.is(RequestTimeoutLossTest.CONNECT_TIMEOUT)
319             );
320             MatcherAssert.assertThat(
321                 "should be read timeout",
322                 rdc.getValue().intValue(),
323                 Matchers.is(RequestTimeoutLossTest.READ_TIMEOUT)
324             );
325         }
326     }
327 
328     /**
329      * Make a request with default url.
330      * @param type Type of Request
331      * @return Request
332      * @throws Exception If fails
333      */
334     private static Request request(final Class<? extends Request> type)
335         throws Exception {
336         return RequestTestTemplate.request(
337             new URI(RequestTimeoutLossTest.LOCALHOST_URL),
338             type
339         );
340     }
341 
342 }