Skip to content

Commit

Permalink
chore: move and rename http client test API (#18791)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbee authored Oct 10, 2024
1 parent de74df8 commit 86d8f37
Show file tree
Hide file tree
Showing 221 changed files with 629 additions and 565 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2004-2022, University of Oslo
* Copyright (c) 2004-2024, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand All @@ -25,36 +25,29 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.test.web;
package org.hisp.dhis.http;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.hisp.dhis.jsontree.JsonObject;
import org.hisp.dhis.test.web.HttpStatus.Series;
import org.hisp.dhis.test.web.WebClient.HttpResponse;
import org.hisp.dhis.test.web.WebClient.RequestComponent;
import org.hisp.dhis.test.webapi.json.domain.JsonError;

/**
* Helpers needed when testing with {@link WebClient} and {@code
* org.springframework.test.web.servlet.MockMvc}.
* Assertions for {@link HttpClientAdapter} API based tests.
*
* @author Jan Bernitt
* @since 2.42 (extracted from existing utils)
*/
public class WebClientUtils {

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class HttpAssertions {
/**
* Asserts that the {@link HttpResponse} has the expected {@link HttpStatus}.
* Asserts that the {@link HttpClientAdapter.HttpResponse} has the expected {@link HttpStatus}.
*
* <p>If status is {@link HttpStatus#CREATED} the method returns the UID of the created object in
* case it is provided by the response. This is based on a convention used in DHIS2.
Expand All @@ -63,7 +56,7 @@ public class WebClientUtils {
* @param actual the response we actually got
* @return UID of the created object (if available) or {@code null}
*/
public static String assertStatus(HttpStatus expected, HttpResponse actual) {
public static String assertStatus(HttpStatus expected, HttpClientAdapter.HttpResponse actual) {
HttpStatus actualStatus = actual.status();
if (expected != actualStatus) {
// OBS! we use the actual state to not fail the check in error
Expand All @@ -80,19 +73,22 @@ public static String assertStatus(HttpStatus expected, HttpResponse actual) {
}

/**
* Asserts that the {@link HttpResponse} has the expected {@link Series}. This is useful on cases
* where it only matters that operation was {@link Series#SUCCESSFUL} or say {@link
* Series#CLIENT_ERROR} but not which exact code of the series.
* Asserts that the {@link HttpClientAdapter.HttpResponse} has the expected {@link
* HttpStatus.Series}. This is useful on cases where it only matters that operation was {@link
* HttpStatus.Series#SUCCESSFUL} or say {@link HttpStatus.Series#CLIENT_ERROR} but not which exact
* code of the series.
*
* <p>If status is {@link HttpStatus#CREATED} the method returns the UID of the created object in
* case it is provided by the response. This is based on a convention used in DHIS2.
*
* @param expected status {@link Series} we should get
* @param expected status {@link HttpStatus.Series} we should get
* @param actual the response we actually got
* @return UID of the created object (if available) or {@code null}
*/
public static String assertSeries(Series expected, HttpResponse actual) {
Series actualSeries = actual.series();
@CheckForNull
public static String assertSeries(
@Nonnull HttpStatus.Series expected, @Nonnull HttpClientAdapter.HttpResponse actual) {
HttpStatus.Series actualSeries = actual.series();
if (expected != actualSeries) {
// OBS! we use the actual state to not fail the check in error
String msg = actual.error(actualSeries).summary();
Expand All @@ -102,7 +98,7 @@ public static String assertSeries(Series expected, HttpResponse actual) {
return getCreatedId(actual);
}

public static void assertValidLocation(HttpResponse actual) {
public static void assertValidLocation(@Nonnull HttpClientAdapter.HttpResponse actual) {
String location = actual.location();
if (location == null) {
return;
Expand All @@ -115,7 +111,8 @@ public static void assertValidLocation(HttpResponse actual) {
"Location header does contain multiple protocol parts");
}

private static String getCreatedId(HttpResponse response) {
@CheckForNull
private static String getCreatedId(HttpClientAdapter.HttpResponse response) {
HttpStatus actual = response.status();
if (actual == HttpStatus.CREATED) {
JsonObject report = response.contentUnchecked().getObject("response");
Expand All @@ -127,76 +124,11 @@ private static String getCreatedId(HttpResponse response) {
return location == null ? null : location.substring(location.lastIndexOf('/') + 1);
}

public static String substitutePlaceholders(String url, Object[] args) {
if (args.length == 0) return url;

Object[] urlArgs =
Stream.of(args)
.filter(arg -> !(arg instanceof RequestComponent) && !(arg instanceof Path))
.map(arg -> arg == null ? "" : arg)
.toArray();
return String.format(url.replaceAll("\\{[a-zA-Z]+}", "%s"), urlArgs);
}

public static String objectReferences(String... uids) {
StringBuilder str = new StringBuilder();
str.append('[');
for (String uid : uids) {
if (str.length() > 1) {
str.append(',');
}
str.append(objectReference(uid));
}
str.append(']');
return str.toString();
}

public static String objectReference(String uid) {
return String.format("{\"id\":\"%s\"}", uid);
}

public static <T> T callAndFailOnException(Callable<T> op) {
public static <T> T exceptionAsFail(Callable<T> op) {
try {
return op.call();
} catch (Exception ex) {
throw new AssertionError(ex);
}
}

public static RequestComponent[] requestComponentsIn(Object... args) {
return Stream.of(args)
.map(
arg ->
arg instanceof Path path ? new WebClient.Body(fileContent(path.toString())) : arg)
.filter(RequestComponent.class::isInstance)
.toArray(RequestComponent[]::new);
}

@SuppressWarnings("unchecked")
public static <T extends RequestComponent> T getComponent(
Class<T> type, RequestComponent[] components) {
return (T)
Stream.of(components)
.filter(Objects::nonNull)
.filter(c -> c.getClass() == type)
.findFirst()
.orElse(null);
}

public static String fileContent(String filename) {
try {
return Files.readString(
Path.of(
Objects.requireNonNull(WebClientUtils.class.getClassLoader().getResource(filename))
.toURI()),
StandardCharsets.UTF_8);
} catch (IOException | URISyntaxException e) {
fail(e);
return null;
}
}

private WebClientUtils() {
throw new UnsupportedOperationException("util");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.test.web;
package org.hisp.dhis.http;

import static org.apache.commons.lang3.ArrayUtils.insert;
import static org.hisp.dhis.test.web.WebClientUtils.assertSeries;
import static org.hisp.dhis.test.web.WebClientUtils.assertStatus;
import static org.hisp.dhis.test.web.WebClientUtils.callAndFailOnException;
import static org.hisp.dhis.test.web.WebClientUtils.fileContent;
import static org.hisp.dhis.test.web.WebClientUtils.requestComponentsIn;
import static org.hisp.dhis.test.web.WebClientUtils.substitutePlaceholders;
import static org.hisp.dhis.http.HttpAssertions.assertSeries;
import static org.hisp.dhis.http.HttpAssertions.assertStatus;
import static org.hisp.dhis.http.HttpAssertions.exceptionAsFail;
import static org.hisp.dhis.http.HttpClientUtils.fileContent;
import static org.hisp.dhis.http.HttpClientUtils.requestComponentsIn;
import static org.hisp.dhis.http.HttpClientUtils.substitutePlaceholders;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -58,7 +58,7 @@
*/
@FunctionalInterface
@SuppressWarnings("java:S100")
public interface WebClient {
public interface HttpClientAdapter {

/**
* Execute the request with the provided parameters.
Expand Down Expand Up @@ -130,23 +130,27 @@ record Header(String name, Object value) implements RequestComponent {}

record Body(String content) implements RequestComponent {}

@Nonnull
default HttpResponse GET(String url, Object... args) {
return perform(HttpMethod.GET, substitutePlaceholders(url, args), requestComponentsIn(args));
}

@Nonnull
default HttpResponse POST(String url, Object... args) {
return perform(HttpMethod.POST, substitutePlaceholders(url, args), requestComponentsIn(args));
}

@Nonnull
default HttpResponse POST(String url, @Language("json5") String body) {
return perform(HttpMethod.POST, url, new Body(body));
}

@Nonnull
default HttpResponse POST(String url, Path body) {
return callAndFailOnException(
() -> POST(url, Body(fileContent(body.toString())), ContentType(body)));
return exceptionAsFail(() -> POST(url, Body(fileContent(body.toString())), ContentType(body)));
}

@Nonnull
default HttpResponse PATCH(String url, Object... args) {
// Default mime-type is added as first element so that content type in
// arguments does not override it
Expand All @@ -156,40 +160,47 @@ default HttpResponse PATCH(String url, Object... args) {
insert(0, requestComponentsIn(args), ContentType("application/json-patch+json")));
}

@Nonnull
default HttpResponse PATCH(String url, Path body) {
return callAndFailOnException(
return exceptionAsFail(
() ->
PATCH(
url,
Body(fileContent(body.toString())),
ContentType("application/json-patch+json")));
}

@Nonnull
default HttpResponse PATCH(String url, @Language("json5") String body) {
return perform(HttpMethod.PATCH, url, ContentType("application/json-patch+json"), Body(body));
}

@Nonnull
default HttpResponse PUT(String url, Object... args) {
return perform(HttpMethod.PUT, substitutePlaceholders(url, args), requestComponentsIn(args));
}

@Nonnull
default HttpResponse PUT(String url, Path body) {
return callAndFailOnException(
() -> PUT(url, Body(fileContent(body.toString())), ContentType(body)));
return exceptionAsFail(() -> PUT(url, Body(fileContent(body.toString())), ContentType(body)));
}

@Nonnull
default HttpResponse PUT(String url, @Language("json5") String body) {
return perform(HttpMethod.PUT, url, new Body(body));
}

@Nonnull
default HttpResponse DELETE(String url, Object... args) {
return perform(HttpMethod.DELETE, substitutePlaceholders(url, args), requestComponentsIn(args));
}

@Nonnull
default HttpResponse DELETE(String url, @Language("json5") String body) {
return perform(HttpMethod.DELETE, url, new Body(body));
}

@Nonnull
default HttpResponse perform(HttpMethod method, String url, RequestComponent... components) {
// configure headers
String contentMediaType = null;
Expand All @@ -205,15 +216,15 @@ default HttpResponse perform(HttpMethod method, String url, RequestComponent...
}
}
// configure body
Body bodyComponent = WebClientUtils.getComponent(Body.class, components);
Body bodyComponent = HttpClientUtils.getComponent(Body.class, components);
String body = bodyComponent == null ? "" : bodyComponent.content();
String mediaType = contentMediaType != null ? contentMediaType : "application/json";
if (body == null || body.isEmpty()) return perform(method, url, headers, null, null);
if (mediaType.startsWith("application/json")) body = body.replace('\'', '"');
return perform(method, url, headers, mediaType, body);
}

/** Implemented to adapt the {@link WebClient} API to an actual implementation response */
/** Implemented to adapt the {@link HttpClientAdapter} API to an actual implementation response */
interface HttpResponseAdapter {

/**
Expand Down Expand Up @@ -272,7 +283,7 @@ public String content(String contentType) {
String actualContentType = header("Content-Type");
assertNotNull(actualContentType, "response content-type was not set");
if (!actualContentType.startsWith(contentType)) assertEquals(contentType, actualContentType);
return callAndFailOnException(response::getContent);
return exceptionAsFail(response::getContent);
}

public JsonMixed content() {
Expand Down Expand Up @@ -325,7 +336,7 @@ public boolean hasBody() {
}

public JsonMixed contentUnchecked() {
return callAndFailOnException(() -> JsonMixed.of(response.getContent()));
return exceptionAsFail(() -> JsonMixed.of(response.getContent()));
}

public String location() {
Expand Down
Loading

0 comments on commit 86d8f37

Please sign in to comment.