Skip to content

Commit

Permalink
Add support for conversion of the whole JSON document
Browse files Browse the repository at this point in the history
Closes gh-33018
  • Loading branch information
snicoll committed Jun 12, 2024
1 parent f3d390a commit 69c44de
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.function.Consumer;

import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AssertFactory;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;

Expand All @@ -35,8 +40,12 @@
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.util.Assert;

/**
Expand Down Expand Up @@ -90,6 +99,62 @@ protected AbstractJsonContentAssert(@Nullable JsonContent actual, Class<?> selfT
as("JSON content");
}

/**
* Verify that the actual value can be converted to an instance of the
* given {@code target}, and produce a new {@linkplain AbstractObjectAssert
* assertion} object narrowed to that type.
* @param target the {@linkplain Class type} to convert the actual value to
*/
public <T> AbstractObjectAssert<?, T> convertTo(Class<T> target) {
isNotNull();
T value = convertToTargetType(target);
return Assertions.assertThat(value);
}

/**
* Verify that the actual value can be converted to an instance of the type
* defined by the given {@link AssertFactory} and return a new Assert narrowed
* to that type.
* <p>{@link InstanceOfAssertFactories} provides static factories for all the
* types supported by {@link Assertions#assertThat}. Additional factories can
* be created by implementing {@link AssertFactory}.
* <p>Example: <pre><code class="java">
* // Check that the JSON document is an array of 3 users
* assertThat(json).convertTo(InstanceOfAssertFactories.list(User.class))
* hasSize(3); // ListAssert of User
* </code></pre>
* @param assertFactory the {@link AssertFactory} to use to produce a narrowed
* Assert for the type that it defines.
*/
public <ASSERT extends AbstractAssert<?, ?>> ASSERT convertTo(AssertFactory<?, ASSERT> assertFactory) {
isNotNull();
return assertFactory.createAssert(this::convertToTargetType);
}

@SuppressWarnings("unchecked")
private <T> T convertToTargetType(Type targetType) {
String json = this.actual.getJson();
if (this.jsonMessageConverter == null) {
throw new IllegalStateException(
"No JSON message converter available to convert %s".formatted(json));
}
try {
return (T) this.jsonMessageConverter.read(targetType, getClass(), fromJson(json));
}
catch (Exception ex) {
throw failure(new ValueProcessingFailed(json,
"To convert successfully to:%n %s%nBut it failed:%n %s%n".formatted(
targetType.getTypeName(), ex.getMessage())));
}
}

private HttpInputMessage fromJson(String json) {
MockHttpInputMessage inputMessage = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
inputMessage.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return inputMessage;
}


// JsonPath support

/**
Expand Down Expand Up @@ -525,4 +590,11 @@ private JsonPathNotExpected(String actual, String path) {
}
}

private static final class ValueProcessingFailed extends BasicErrorMessageFactory {

private ValueProcessingFailed(String actualToString, String errorMessage) {
super("%nExpected:%n %s%n%s".formatted(actualToString, errorMessage));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@
import java.util.function.Consumer;
import java.util.stream.Stream;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -100,6 +103,56 @@ void satisfiesAllowFurtherAssertions() {
});
}

@Nested
class ConversionTests {

@Test
void convertToTargetType() {
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
.convertTo(Family.class)
.satisfies(family -> assertThat(family.familyMembers()).hasSize(5));
}

@Test
void convertToIncompatibleTargetTypeShouldFail() {
AbstractJsonContentAssert<?> jsonAssert = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter));
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> jsonAssert.convertTo(Member.class))
.withMessageContainingAll("To convert successfully to:",
Member.class.getName(), "But it failed:");
}

@Test
void convertUsingAssertFactory() {
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
.convertTo(new FamilyAssertFactory())
.hasFamilyMember("Homer");
}

private AssertProvider<AbstractJsonContentAssert<?>> forJson(@Nullable String json,
@Nullable GenericHttpMessageConverter<Object> jsonHttpMessageConverter) {

return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter);
}

private static class FamilyAssertFactory extends InstanceOfAssertFactory<Family, FamilyAssert> {
public FamilyAssertFactory() {
super(Family.class, FamilyAssert::new);
}
}

private static class FamilyAssert extends AbstractObjectAssert<FamilyAssert, Family> {
public FamilyAssert(Family family) {
super(family, FamilyAssert.class);
}

public FamilyAssert hasFamilyMember(String name) {
assertThat(this.actual.familyMembers).anySatisfy(m -> assertThat(m.name()).isEqualTo(name));
return this.myself;
}
}
}

@Nested
class HasPathTests {

Expand Down Expand Up @@ -261,14 +314,14 @@ void asMapIsEmpty() {
void convertToWithoutHttpMessageConverterShouldFail() {
JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]");
assertThatIllegalStateException()
.isThrownBy(() -> path.convertTo(ExtractingPathTests.Member.class))
.isThrownBy(() -> path.convertTo(Member.class))
.withMessage("No JSON message converter available to convert {name=Homer}");
}

@Test
void convertToTargetType() {
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
.extractingPath("$.familyMembers[0]").convertTo(ExtractingPathTests.Member.class)
.extractingPath("$.familyMembers[0]").convertTo(Member.class)
.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
}

Expand All @@ -283,7 +336,7 @@ void convertToIncompatibleTargetTypeShouldFail() {
}

@Test
void convertArrayToParameterizedType() {
void convertArrayUsingAssertFactory() {
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
.extractingPath("$.familyMembers")
.convertTo(InstanceOfAssertFactories.list(Member.class))
Expand Down Expand Up @@ -336,8 +389,6 @@ void isNotEmptyForPathWithFilterNotMatching() {
}


private record Member(String name) {}

private record Customer(long id, String username) {}

private AssertProvider<AbstractJsonContentAssert<?>> forJson(@Nullable String json) {
Expand Down Expand Up @@ -836,6 +887,12 @@ private AssertProvider<AbstractJsonContentAssert<?>> forJson(@Nullable String js
return () -> new TestJsonContentAssert(json, null);
}


record Member(String name) {}

@JsonIgnoreProperties(ignoreUnknown = true)
record Family(List<Member> familyMembers) {}

private static class TestJsonContentAssert extends AbstractJsonContentAssert<TestJsonContentAssert> {

public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
Expand Down

0 comments on commit 69c44de

Please sign in to comment.