2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.http.internal.http;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
18 import java.io.UnsupportedEncodingException;
19 import java.nio.ByteBuffer;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.CompletionException;
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;
37 * Unit tests for {@link HttpResponseListenerTest}.
39 * @author Corubba Smith - Initial contribution
42 @ExtendWith(MockitoExtension.class)
43 public class HttpResponseListenerTest {
45 private Request request = mock(Request.class);
46 private Response response = mock(Response.class);
48 // ******** Common methods ******** //
51 * Run the given listener with the given result.
53 private void run(HttpResponseListener listener, Result result) {
54 listener.onComplete(result);
58 * Return a default Result using the request- and response-mocks and no failure.
60 private Result createResult() {
61 return new Result(request, response);
65 * Run the given listener with a default result.
67 private void run(HttpResponseListener listener) {
68 run(listener, createResult());
72 * Set the given payload as body of the response in the buffer of the given listener.
74 private void setPayload(HttpResponseListener listener, byte[] payload) {
75 listener.onContent(null, ByteBuffer.wrap(payload));
79 * Run a default listener with the given result and the given payload.
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);
87 run(listener, result);
92 * Run a default listener with the given result.
94 private CompletableFuture<@Nullable Content> run(Result result) {
95 return run(result, null);
99 * Run a default listener with a default result and the given payload.
101 private CompletableFuture<@Nullable Content> run(byte @Nullable [] payload) {
102 return run(createResult(), payload);
106 * Run a default listener with a default result.
108 private CompletableFuture<@Nullable Content> run() {
109 return run(createResult());
114 // required for the request trace
115 when(response.getHeaders()).thenReturn(new HttpFields());
118 // ******** Tests ******** //
121 * When an exception is thrown during the request phase, the future completes unexceptionally
125 public void requestException() {
126 RuntimeException requestFailure = new RuntimeException("The request failed!");
127 Result result = new Result(request, requestFailure, response);
129 CompletableFuture<@Nullable Content> future = run(result);
131 assertTrue(future.isDone());
132 assertFalse(future.isCompletedExceptionally());
133 assertNull(future.join());
137 * When an exception is thrown during the response phase, the future completes unexceptionally
141 public void responseException() {
142 RuntimeException responseFailure = new RuntimeException("The response failed!");
143 Result result = new Result(request, response, responseFailure);
145 CompletableFuture<@Nullable Content> future = run(result);
147 assertTrue(future.isDone());
148 assertFalse(future.isCompletedExceptionally());
149 assertNull(future.join());
153 * When the remote side does not send any payload, the future completes normally and contains a
157 public void okWithNoBody() {
158 when(response.getStatus()).thenReturn(HttpStatus.OK_200);
160 CompletableFuture<@Nullable Content> future = run();
162 assertTrue(future.isDone());
163 assertFalse(future.isCompletedExceptionally());
165 Content content = future.join();
166 assertNotNull(content);
167 assertNotNull(content.getRawContent());
168 assertEquals(0, content.getRawContent().length);
169 assertNull(content.getMediaType());
173 * When the remote side sends a payload, the future completes normally and contains a Content
174 * object with the payload.
177 public void okWithBody() {
178 when(response.getStatus()).thenReturn(HttpStatus.OK_200);
180 final String textPayload = "foobar";
181 CompletableFuture<@Nullable Content> future = run(textPayload.getBytes());
183 assertTrue(future.isDone());
184 assertFalse(future.isCompletedExceptionally());
186 Content content = future.join();
187 assertNotNull(content);
188 assertNotNull(content.getRawContent());
189 assertEquals(textPayload, new String(content.getRawContent()));
190 assertNull(content.getMediaType());
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
199 public void okWithEncodedBody() throws UnsupportedEncodingException {
200 final String encodingName = "UTF-16LE";
201 final String fallbackEncodingName = "UTF-8";
203 CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
204 HttpResponseListener listener = new HttpResponseListener(future, fallbackEncodingName, 1024 * 1024);
206 response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=" + encodingName);
207 when(response.getRequest()).thenReturn(request);
208 listener.onHeaders(response);
210 final String textPayload = "漢字編碼方法";
211 setPayload(listener, textPayload.getBytes(encodingName));
213 when(response.getStatus()).thenReturn(HttpStatus.OK_200);
216 assertTrue(future.isDone());
217 assertFalse(future.isCompletedExceptionally());
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());
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.
233 public void okWithEncodedBodyFallback() throws UnsupportedEncodingException {
234 final String encodingName = "UTF-16BE";
236 CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
237 HttpResponseListener listener = new HttpResponseListener(future, encodingName, 1024 * 1024);
239 final String textPayload = "汉字编码方法";
240 setPayload(listener, textPayload.getBytes(encodingName));
242 when(response.getStatus()).thenReturn(HttpStatus.OK_200);
245 assertTrue(future.isDone());
246 assertFalse(future.isCompletedExceptionally());
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());
257 * When the remote side response with a HTTP/204 and no payload, the future completes normally
258 * and contains an empty Content.
261 public void nocontent() {
262 when(response.getStatus()).thenReturn(HttpStatus.NO_CONTENT_204);
264 CompletableFuture<@Nullable Content> future = run();
266 assertTrue(future.isDone());
267 assertFalse(future.isCompletedExceptionally());
269 Content content = future.join();
270 assertNotNull(content);
271 assertNotNull(content.getRawContent());
272 assertEquals(0, content.getRawContent().length);
273 assertNull(content.getMediaType());
277 * When the remote side response with a HTTP/401, the future completes exceptionally with a
281 public void unauthorized() {
282 when(response.getStatus()).thenReturn(HttpStatus.UNAUTHORIZED_401);
284 CompletableFuture<@Nullable Content> future = run();
286 assertTrue(future.isDone());
287 assertTrue(future.isCompletedExceptionally());
290 CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
291 assertNotNull(exceptionWrapper);
293 Throwable exception = exceptionWrapper.getCause();
294 assertNotNull(exception);
295 assertTrue(exception instanceof HttpAuthException);
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.
303 public void unexpectedStatus() {
304 when(response.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500);
306 CompletableFuture<@Nullable Content> future = run();
308 assertTrue(future.isDone());
309 assertTrue(future.isCompletedExceptionally());
312 CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
313 assertNotNull(exceptionWrapper);
315 Throwable exception = exceptionWrapper.getCause();
316 assertNotNull(exception);
317 assertTrue(exception instanceof IllegalStateException);
318 assertEquals("Response - Code500", exception.getMessage());