[http] Support all 2xx response codes (#12594)

* [http] Add tests
* [http] Treat all success codes as such
* All 2xx http codes mean success, so treat them the same way 200 is.

Fixes #11369

Signed-off-by: Corubba Smith <corubba@gmx.de>
This commit is contained in:
Corubba 2022-05-08 10:53:20 +02:00 committed by GitHub
parent 4e56b9b734
commit 961696ead6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 326 additions and 10 deletions

View File

@ -13,6 +13,7 @@
package org.openhab.binding.http.internal.http;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@ -63,18 +64,11 @@ public class HttpResponseListener extends BufferingResponseListener {
logger.warn("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(), request.getMethod(),
request.getContent(), result.getFailure().toString());
future.complete(null);
} else if (HttpStatus.isSuccess(response.getStatus())) {
String encoding = Objects.requireNonNullElse(getEncoding(), fallbackEncoding);
future.complete(new Content(getContent(), encoding, getMediaType()));
} else {
switch (response.getStatus()) {
case HttpStatus.OK_200:
byte[] content = getContent();
String encoding = getEncoding();
if (content != null) {
future.complete(
new Content(content, encoding == null ? fallbackEncoding : encoding, getMediaType()));
} else {
future.complete(null);
}
break;
case HttpStatus.UNAUTHORIZED_401:
logger.debug("Requesting '{}' (method='{}', content='{}') failed: Authorization error",
request.getURI(), request.getMethod(), request.getContent());

View File

@ -0,0 +1,320 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.http.internal.http;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Unit tests for {@link HttpResponseListenerTest}.
*
* @author Corubba Smith - Initial contribution
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
public class HttpResponseListenerTest {
private Request request = mock(Request.class);
private Response response = mock(Response.class);
// ******** Common methods ******** //
/**
* Run the given listener with the given result.
*/
private void run(HttpResponseListener listener, Result result) {
listener.onComplete(result);
}
/**
* Return a default Result using the request- and response-mocks and no failure.
*/
private Result createResult() {
return new Result(request, response);
}
/**
* Run the given listener with a default result.
*/
private void run(HttpResponseListener listener) {
run(listener, createResult());
}
/**
* Set the given payload as body of the response in the buffer of the given listener.
*/
private void setPayload(HttpResponseListener listener, byte[] payload) {
listener.onContent(null, ByteBuffer.wrap(payload));
}
/**
* Run a default listener with the given result and the given payload.
*/
private CompletableFuture<@Nullable Content> run(Result result, byte @Nullable [] payload) {
CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
HttpResponseListener listener = new HttpResponseListener(future, null, 1024 * 1024);
if (null != payload) {
setPayload(listener, payload);
}
run(listener, result);
return future;
}
/**
* Run a default listener with the given result.
*/
private CompletableFuture<@Nullable Content> run(Result result) {
return run(result, null);
}
/**
* Run a default listener with a default result and the given payload.
*/
private CompletableFuture<@Nullable Content> run(byte @Nullable [] payload) {
return run(createResult(), payload);
}
/**
* Run a default listener with a default result.
*/
private CompletableFuture<@Nullable Content> run() {
return run(createResult());
}
@BeforeEach
void init() {
// required for the request trace
when(response.getHeaders()).thenReturn(new HttpFields());
}
// ******** Tests ******** //
/**
* When a exception is thrown during the request phase, the future completes unexceptionally
* with no value.
*/
@Test
public void requestException() {
RuntimeException requestFailure = new RuntimeException("The request failed!");
Result result = new Result(request, requestFailure, response);
CompletableFuture<@Nullable Content> future = run(result);
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
assertNull(future.join());
}
/**
* When a exception is thrown during the response phase, the future completes unexceptionally
* with no value.
*/
@Test
public void responseException() {
RuntimeException responseFailure = new RuntimeException("The response failed!");
Result result = new Result(request, response, responseFailure);
CompletableFuture<@Nullable Content> future = run(result);
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
assertNull(future.join());
}
/**
* When the remote side does not send any payload, the future completes normally and contains a
* empty Content.
*/
@Test
public void okWithNoBody() {
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
CompletableFuture<@Nullable Content> future = run();
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
Content content = future.join();
assertNotNull(content);
assertNotNull(content.getRawContent());
assertEquals(0, content.getRawContent().length);
assertNull(content.getMediaType());
}
/**
* When the remote side sends a payload, the future completes normally and contains a Content
* object with the payload.
*/
@Test
public void okWithBody() {
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
final String textPayload = "foobar";
CompletableFuture<@Nullable Content> future = run(textPayload.getBytes());
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
Content content = future.join();
assertNotNull(content);
assertNotNull(content.getRawContent());
assertEquals(textPayload, new String(content.getRawContent()));
assertNull(content.getMediaType());
}
/**
* When the remote side sends a payload and encoding header, the future completes normally
* and contains a Content object with the payload. The payload gets decoded using the encoding
* the remote sent.
*/
@Test
public void okWithEncodedBody() throws UnsupportedEncodingException {
final String encodingName = "UTF-16LE";
final String fallbackEncodingName = "UTF-8";
CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
HttpResponseListener listener = new HttpResponseListener(future, fallbackEncodingName, 1024 * 1024);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=" + encodingName);
when(response.getRequest()).thenReturn(request);
listener.onHeaders(response);
final String textPayload = "漢字編碼方法";
setPayload(listener, textPayload.getBytes(encodingName));
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
run(listener);
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
Content content = future.join();
assertNotNull(content);
assertNotNull(content.getRawContent());
assertEquals(textPayload, new String(content.getRawContent(), encodingName));
assertEquals(textPayload, content.getAsString());
assertEquals("text/plain", content.getMediaType());
}
/**
* When the remote side sends a payload but no encoding, the future completes normally and
* contains a Content object with the payload. The payload gets decoded using the fallback
* encoding of the listener.
*/
@Test
public void okWithEncodedBodyFallback() throws UnsupportedEncodingException {
final String encodingName = "UTF-16BE";
CompletableFuture<@Nullable Content> future = new CompletableFuture<>();
HttpResponseListener listener = new HttpResponseListener(future, encodingName, 1024 * 1024);
final String textPayload = "汉字编码方法";
setPayload(listener, textPayload.getBytes(encodingName));
when(response.getStatus()).thenReturn(HttpStatus.OK_200);
run(listener);
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
Content content = future.join();
assertNotNull(content);
assertNotNull(content.getRawContent());
assertEquals(textPayload, new String(content.getRawContent(), encodingName));
assertEquals(textPayload, content.getAsString());
assertNull(content.getMediaType());
}
/**
* When the remote side response with a HTTP/204 and no payload, the future completes normally
* and contains a empty Content.
*/
@Test
public void nocontent() {
when(response.getStatus()).thenReturn(HttpStatus.NO_CONTENT_204);
CompletableFuture<@Nullable Content> future = run();
assertTrue(future.isDone());
assertFalse(future.isCompletedExceptionally());
Content content = future.join();
assertNotNull(content);
assertNotNull(content.getRawContent());
assertEquals(0, content.getRawContent().length);
assertNull(content.getMediaType());
}
/**
* When the remote side response with a HTTP/401, the future completes exceptionally with a
* HttpAuthException.
*/
@Test
public void unauthorized() {
when(response.getStatus()).thenReturn(HttpStatus.UNAUTHORIZED_401);
CompletableFuture<@Nullable Content> future = run();
assertTrue(future.isDone());
assertTrue(future.isCompletedExceptionally());
@Nullable
CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
assertNotNull(exceptionWrapper);
Throwable exception = exceptionWrapper.getCause();
assertNotNull(exception);
assertTrue(exception instanceof HttpAuthException);
}
/**
* When the remote side responds with anything we don't expect (in this case a HTTP/500), the
* future completes exceptionally with a IllegalStateException.
*/
@Test
public void unexpectedStatus() {
when(response.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500);
CompletableFuture<@Nullable Content> future = run();
assertTrue(future.isDone());
assertTrue(future.isCompletedExceptionally());
@Nullable
CompletionException exceptionWrapper = assertThrows(CompletionException.class, () -> future.join());
assertNotNull(exceptionWrapper);
Throwable exception = exceptionWrapper.getCause();
assertNotNull(exception);
assertTrue(exception instanceof IllegalStateException);
assertEquals("Response - Code500", exception.getMessage());
}
}

View File

@ -0,0 +1,2 @@
# to run through all code-branches
org.slf4j.simpleLogger.log.org.openhab.binding.http=trace