]> git.basschouten.com Git - openhab-addons.git/blob
d104c7458532956739d024563da89592c1eb71af
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.http.internal.http;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
17
18 import java.io.UnsupportedEncodingException;
19 import java.nio.ByteBuffer;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.CompletionException;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.api.Request;
26 import org.eclipse.jetty.client.api.Response;
27 import org.eclipse.jetty.client.api.Result;
28 import org.eclipse.jetty.http.HttpFields;
29 import org.eclipse.jetty.http.HttpHeader;
30 import org.eclipse.jetty.http.HttpStatus;
31 import org.junit.jupiter.api.BeforeEach;
32 import org.junit.jupiter.api.Test;
33 import org.junit.jupiter.api.extension.ExtendWith;
34 import org.mockito.junit.jupiter.MockitoExtension;
35
36 /**
37  * Unit tests for {@link HttpResponseListenerTest}.
38  *
39  * @author Corubba Smith - Initial contribution
40  */
41 @NonNullByDefault
42 @ExtendWith(MockitoExtension.class)
43 public class HttpResponseListenerTest {
44
45     private Request request = mock(Request.class);
46     private Response response = mock(Response.class);
47
48     // ******** Common methods ******** //
49
50     /**
51      * Run the given listener with the given result.
52      */
53     private void run(HttpResponseListener listener, Result result) {
54         listener.onComplete(result);
55     }
56
57     /**
58      * Return a default Result using the request- and response-mocks and no failure.
59      */
60     private Result createResult() {
61         return new Result(request, response);
62     }
63
64     /**
65      * Run the given listener with a default result.
66      */
67     private void run(HttpResponseListener listener) {
68         run(listener, createResult());
69     }
70
71     /**
72      * Set the given payload as body of the response in the buffer of the given listener.
73      */
74     private void setPayload(HttpResponseListener listener, byte[] payload) {
75         listener.onContent(null, ByteBuffer.wrap(payload));
76     }
77
78     /**
79      * Run a default listener with the given result and the given payload.
80      */
81     private CompletableFuture<@Nullable Content> run(Result result, byte @Nullable [] payload) {
82         CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
83         HttpResponseListener listener = new HttpResponseListener(future, null, 1024 * 1024);
84         if (null != payload) {
85             setPayload(listener, payload);
86         }
87         run(listener, result);
88         return future;
89     }
90
91     /**
92      * Run a default listener with the given result.
93      */
94     private CompletableFuture<@Nullable Content> run(Result result) {
95         return run(result, null);
96     }
97
98     /**
99      * Run a default listener with a default result and the given payload.
100      */
101     private CompletableFuture<@Nullable Content> run(byte @Nullable [] payload) {
102         return run(createResult(), payload);
103     }
104
105     /**
106      * Run a default listener with a default result.
107      */
108     private CompletableFuture<@Nullable Content> run() {
109         return run(createResult());
110     }
111
112     @BeforeEach
113     void init() {
114         // required for the request trace
115         when(response.getHeaders()).thenReturn(new HttpFields());
116     }
117
118     // ******** Tests ******** //
119
120     /**
121      * When an exception is thrown during the request phase, the future completes unexceptionally
122      * with no value.
123      */
124     @Test
125     public void requestException() {
126         RuntimeException requestFailure = new RuntimeException("The request failed!");
127         Result result = new Result(request, requestFailure, response);
128
129         CompletableFuture<@Nullable Content> future = run(result);
130
131         assertTrue(future.isDone());
132         assertFalse(future.isCompletedExceptionally());
133         assertNull(future.join());
134     }
135
136     /**
137      * When an exception is thrown during the response phase, the future completes unexceptionally
138      * with no value.
139      */
140     @Test
141     public void responseException() {
142         RuntimeException responseFailure = new RuntimeException("The response failed!");
143         Result result = new Result(request, response, responseFailure);
144
145         CompletableFuture<@Nullable Content> future = run(result);
146
147         assertTrue(future.isDone());
148         assertFalse(future.isCompletedExceptionally());
149         assertNull(future.join());
150     }
151
152     /**
153      * When the remote side does not send any payload, the future completes normally and contains a
154      * empty Content.
155      */
156     @Test
157     public void okWithNoBody() {
158         when(response.getStatus()).thenReturn(HttpStatus.OK_200);
159
160         CompletableFuture<@Nullable Content> future = run();
161
162         assertTrue(future.isDone());
163         assertFalse(future.isCompletedExceptionally());
164
165         Content content = future.join();
166         assertNotNull(content);
167         assertNotNull(content.getRawContent());
168         assertEquals(0, content.getRawContent().length);
169         assertNull(content.getMediaType());
170     }
171
172     /**
173      * When the remote side sends a payload, the future completes normally and contains a Content
174      * object with the payload.
175      */
176     @Test
177     public void okWithBody() {
178         when(response.getStatus()).thenReturn(HttpStatus.OK_200);
179
180         final String textPayload = "foobar";
181         CompletableFuture<@Nullable Content> future = run(textPayload.getBytes());
182
183         assertTrue(future.isDone());
184         assertFalse(future.isCompletedExceptionally());
185
186         Content content = future.join();
187         assertNotNull(content);
188         assertNotNull(content.getRawContent());
189         assertEquals(textPayload, new String(content.getRawContent()));
190         assertNull(content.getMediaType());
191     }
192
193     /**
194      * When the remote side sends a payload and encoding header, the future completes normally
195      * and contains a Content object with the payload. The payload gets decoded using the encoding
196      * the remote sent.
197      */
198     @Test
199     public void okWithEncodedBody() throws UnsupportedEncodingException {
200         final String encodingName = "UTF-16LE";
201         final String fallbackEncodingName = "UTF-8";
202
203         CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
204         HttpResponseListener listener = new HttpResponseListener(future, fallbackEncodingName, 1024 * 1024);
205
206         response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=" + encodingName);
207         when(response.getRequest()).thenReturn(request);
208         listener.onHeaders(response);
209
210         final String textPayload = "漢字編碼方法";
211         setPayload(listener, textPayload.getBytes(encodingName));
212
213         when(response.getStatus()).thenReturn(HttpStatus.OK_200);
214         run(listener);
215
216         assertTrue(future.isDone());
217         assertFalse(future.isCompletedExceptionally());
218
219         Content content = future.join();
220         assertNotNull(content);
221         assertNotNull(content.getRawContent());
222         assertEquals(textPayload, new String(content.getRawContent(), encodingName));
223         assertEquals(textPayload, content.getAsString());
224         assertEquals("text/plain", content.getMediaType());
225     }
226
227     /**
228      * When the remote side sends a payload but no encoding, the future completes normally and
229      * contains a Content object with the payload. The payload gets decoded using the fallback
230      * encoding of the listener.
231      */
232     @Test
233     public void okWithEncodedBodyFallback() throws UnsupportedEncodingException {
234         final String encodingName = "UTF-16BE";
235
236         CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
237         HttpResponseListener listener = new HttpResponseListener(future, encodingName, 1024 * 1024);
238
239         final String textPayload = "汉字编码方法";
240         setPayload(listener, textPayload.getBytes(encodingName));
241
242         when(response.getStatus()).thenReturn(HttpStatus.OK_200);
243         run(listener);
244
245         assertTrue(future.isDone());
246         assertFalse(future.isCompletedExceptionally());
247
248         Content content = future.join();
249         assertNotNull(content);
250         assertNotNull(content.getRawContent());
251         assertEquals(textPayload, new String(content.getRawContent(), encodingName));
252         assertEquals(textPayload, content.getAsString());
253         assertNull(content.getMediaType());
254     }
255
256     /**
257      * When the remote side response with a HTTP/204 and no payload, the future completes normally
258      * and contains an empty Content.
259      */
260     @Test
261     public void nocontent() {
262         when(response.getStatus()).thenReturn(HttpStatus.NO_CONTENT_204);
263
264         CompletableFuture<@Nullable Content> future = run();
265
266         assertTrue(future.isDone());
267         assertFalse(future.isCompletedExceptionally());
268
269         Content content = future.join();
270         assertNotNull(content);
271         assertNotNull(content.getRawContent());
272         assertEquals(0, content.getRawContent().length);
273         assertNull(content.getMediaType());
274     }
275
276     /**
277      * When the remote side response with a HTTP/401, the future completes exceptionally with a
278      * HttpAuthException.
279      */
280     @Test
281     public void unauthorized() {
282         when(response.getStatus()).thenReturn(HttpStatus.UNAUTHORIZED_401);
283
284         CompletableFuture<@Nullable Content> future = run();
285
286         assertTrue(future.isDone());
287         assertTrue(future.isCompletedExceptionally());
288
289         @Nullable
290         CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
291         assertNotNull(exceptionWrapper);
292
293         Throwable exception = exceptionWrapper.getCause();
294         assertNotNull(exception);
295         assertTrue(exception instanceof HttpAuthException);
296     }
297
298     /**
299      * When the remote side responds with anything we don't expect (in this case a HTTP/500), the
300      * future completes exceptionally with an IllegalStateException.
301      */
302     @Test
303     public void unexpectedStatus() {
304         when(response.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500);
305
306         CompletableFuture<@Nullable Content> future = run();
307
308         assertTrue(future.isDone());
309         assertTrue(future.isCompletedExceptionally());
310
311         @Nullable
312         CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
313         assertNotNull(exceptionWrapper);
314
315         Throwable exception = exceptionWrapper.getCause();
316         assertNotNull(exception);
317         assertTrue(exception instanceof IllegalStateException);
318         assertEquals("Response - Code500", exception.getMessage());
319     }
320 }