diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index a7e09611ba1f..cfe5e5913a62 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -32,6 +32,7 @@ dependencies { optional("org.apache.groovy:groovy") optional("org.apache.tomcat.embed:tomcat-embed-core") optional("org.aspectj:aspectjweaver") + optional("org.assertj:assertj-core") optional("org.hamcrest:hamcrest") optional("org.htmlunit:htmlunit") { exclude group: "commons-logging", module: "commons-logging" diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java new file mode 100644 index 000000000000..5725ac9bb171 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.json; + +import org.assertj.core.api.AssertProvider; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * JSON content usually created from a JSON tester. Generally used only to + * {@link AssertProvider provide} {@link JsonContentAssert} to AssertJ + * {@code assertThat} calls. + * + * @author Phillip Webb + * @author Diego Berrueta + * @since 6.2 + */ +public final class JsonContent implements AssertProvider { + + private final String json; + + @Nullable + private final Class resourceLoadClass; + + /** + * Create a new {@link JsonContent} instance. + * @param json the actual JSON content + * @param resourceLoadClass the source class used to load resources + */ + JsonContent(String json, @Nullable Class resourceLoadClass) { + Assert.notNull(json, "JSON must not be null"); + this.json = json; + this.resourceLoadClass = resourceLoadClass; + } + + /** + * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} + * instead. + */ + @Override + public JsonContentAssert assertThat() { + return new JsonContentAssert(this.json, this.resourceLoadClass, null); + } + + /** + * Return the actual JSON content string. + * @return the JSON content + */ + public String getJson() { + return this.json; + } + + @Override + public String toString() { + return "JsonContent " + this.json; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java new file mode 100644 index 000000000000..a606ce940a2e --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java @@ -0,0 +1,367 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.json; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Path; + +import org.assertj.core.api.AbstractAssert; +import org.skyscreamer.jsonassert.JSONCompare; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.JSONCompareResult; +import org.skyscreamer.jsonassert.comparator.JSONComparator; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.function.ThrowingBiFunction; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link CharSequence} representation of a json document, mostly to + * compare the json document against a target, using {@linkplain JSONCompare + * JSON Assert}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Diego Berrueta + * @author Camille Vienot + * @author Stephane Nicoll + * @since 6.2 + */ +public class JsonContentAssert extends AbstractAssert { + + private final JsonLoader loader; + + /** + * Create a new {@link JsonContentAssert} instance that will load resources + * relative to the given {@code resourceLoadClass}, using the given + * {@code charset}. + * @param json the actual JSON content + * @param resourceLoadClass the source class used to load resources + * @param charset the charset of the JSON resources + */ + public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass, + @Nullable Charset charset) { + + super(json, JsonContentAssert.class); + this.loader = new JsonLoader(resourceLoadClass, charset); + } + + /** + * Create a new {@link JsonContentAssert} instance that will load resources + * relative to the given {@code resourceLoadClass}, using {@code UTF-8}. + * @param json the actual JSON content + * @param resourceLoadClass the source class used to load resources + */ + public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass) { + this(json, resourceLoadClass, null); + } + + + /** + * Verify that the actual value is equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isLenientlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isLenientlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isStrictlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isStrictlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public JsonContentAssert isNotEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON. The + * {@code expected} value can contain the JSON itself or, if it ends with + * {@code .json}, the name of a resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public JsonContentAssert isNotEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.loader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON. The {@code expected} value can + * contain the JSON itself or, if it ends with {@code .json}, the name of a + * resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isNotLenientlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isNotLenientlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON. The {@code expected} value can + * contain the JSON itself or, if it ends with {@code .json}, the name of a + * resource to be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + */ + public JsonContentAssert isNotStrictlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

+ * @param expected a resource containing the expected JSON + */ + public JsonContentAssert isNotStrictlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode)); + } + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator)); + } + + private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson, + ThrowingBiFunction comparator) { + + if (actualJson == null) { + return compareForNull(expectedJson); + } + if (expectedJson == null) { + return compareForNull(actualJson.toString()); + } + try { + return comparator.applyWithException(actualJson.toString(), expectedJson.toString()); + } + catch (Exception ex) { + if (ex instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(ex); + } + } + + private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { + JSONCompareResult result = new JSONCompareResult(); + result.passed(); + if (expectedJson != null) { + result.fail("Expected null JSON"); + } + return result; + } + + private JsonContentAssert assertNotFailed(JSONCompareResult result) { + if (result.failed()) { + failWithMessage("JSON Comparison failure: %s", result.getMessage()); + } + return this; + } + + private JsonContentAssert assertNotPassed(JSONCompareResult result) { + if (result.passed()) { + failWithMessage("JSON Comparison failure: %s", result.getMessage()); + } + return this; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java new file mode 100644 index 000000000000..8fc0efb650d2 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.json; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +/** + * Internal helper used to load JSON from various sources. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Stephane Nicoll + * @since 6.2 + */ +class JsonLoader { + + @Nullable + private final Class resourceLoadClass; + + private final Charset charset; + + JsonLoader(@Nullable Class resourceLoadClass, @Nullable Charset charset) { + this.resourceLoadClass = resourceLoadClass; + this.charset = (charset != null ? charset : StandardCharsets.UTF_8); + } + + @Nullable + String getJson(@Nullable CharSequence source) { + if (source == null) { + return null; + } + if (source.toString().endsWith(".json")) { + return getJson(new ClassPathResource(source.toString(), this.resourceLoadClass)); + } + return source.toString(); + } + + String getJson(Resource source) { + try { + return getJson(source.getInputStream()); + } + catch (IOException ex) { + throw new IllegalStateException("Unable to load JSON from " + source, ex); + } + } + + private String getJson(InputStream source) throws IOException { + return FileCopyUtils.copyToString(new InputStreamReader(source, this.charset)); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/package-info.java b/spring-test/src/main/java/org/springframework/test/json/package-info.java new file mode 100644 index 000000000000..cf1085f3b403 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/package-info.java @@ -0,0 +1,9 @@ +/** + * Testing support for JSON. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.json; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java new file mode 100644 index 000000000000..02c839bd8e03 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java @@ -0,0 +1,479 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.json; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.comparator.DefaultComparator; +import org.skyscreamer.jsonassert.comparator.JSONComparator; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link JsonContentAssert}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +@TestInstance(Lifecycle.PER_CLASS) +class JsonContentAssertTests { + + private static final String SOURCE = loadJson("source.json"); + + private static final String LENIENT_SAME = loadJson("lenient-same.json"); + + private static final String DIFFERENT = loadJson("different.json"); + + private static final JSONComparator COMPARATOR = new DefaultComparator(JSONCompareMode.LENIENT); + + @Test + void isEqualToWhenStringIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo(SOURCE); + } + + @Test + void isEqualToWhenNullActualShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> + assertThat(forJson(null)).isEqualTo(SOURCE)); + } + + @Test + void isEqualToWhenExpectedIsNotAStringShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(SOURCE.getBytes())); + } + + @Test + void isEqualToWhenExpectedIsNullShouldFail() { + CharSequence actual = null; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT)); + } + + @Test + void isEqualToWhenStringIsMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT); + } + + @Test + void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT)); + } + + @Test + void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT); + } + + @Test + void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT)); + } + + Stream source() { + return Stream.of( + Arguments.of(new ClassPathResource("source.json", JsonContentAssertTests.class)), + Arguments.of(new ByteArrayResource(SOURCE.getBytes())), + Arguments.of(new FileSystemResource(createFile(SOURCE))), + Arguments.of(new InputStreamResource(createInputStream(SOURCE)))); + } + + Stream lenientSame() { + return Stream.of( + Arguments.of(new ClassPathResource("lenient-same.json", JsonContentAssertTests.class)), + Arguments.of(new ByteArrayResource(LENIENT_SAME.getBytes())), + Arguments.of(new FileSystemResource(createFile(LENIENT_SAME))), + Arguments.of(new InputStreamResource(createInputStream(LENIENT_SAME)))); + } + + Stream different() { + return Stream.of( + Arguments.of(new ClassPathResource("different.json", JsonContentAssertTests.class)), + Arguments.of(new ByteArrayResource(DIFFERENT.getBytes())), + Arguments.of(new FileSystemResource(createFile(DIFFERENT))), + Arguments.of(new InputStreamResource(createInputStream(DIFFERENT)))); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT); + } + + @ParameterizedTest + @MethodSource("different") + void isEqualToWhenResourceIsNotMatchingAndLenientShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT)); + } + + + @Test + void isEqualToWhenStringIsMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, COMPARATOR); + } + + @Test + void isEqualToWhenStringIsNotMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, COMPARATOR)); + } + + @Test + void isEqualToWhenResourcePathIsMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", COMPARATOR); + } + + @Test + void isEqualToWhenResourcePathIsNotMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", COMPARATOR)); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isEqualToWhenResourceIsMatchingAndComparatorShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR); + } + + @ParameterizedTest + @MethodSource("different") + void isEqualToWhenResourceIsNotMatchingAndComparatorShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR)); + } + + @Test + void isLenientlyEqualToWhenStringIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isLenientlyEqualTo(LENIENT_SAME); + } + + @Test + void isLenientlyEqualToWhenNullActualShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(null)).isLenientlyEqualTo(SOURCE)); + } + + @Test + void isLenientlyEqualToWhenStringIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(DIFFERENT)); + } + + @Test + void isLenientlyEqualToWhenExpectedDoesNotExistShouldFail() { + assertThatIllegalStateException() + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("does-not-exist.json")) + .withMessage("Unable to load JSON from class path resource [org/springframework/test/json/does-not-exist.json]"); + } + + @Test + void isLenientlyEqualToWhenResourcePathIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isLenientlyEqualTo("lenient-same.json"); + } + + @Test + void isLenientlyEqualToWhenResourcePathIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("different.json")); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isLenientlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("different") + void isLenientlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected)); + } + + @Test + void isStrictlyEqualToWhenStringIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isStrictlyEqualTo(SOURCE); + } + + @Test + void isStrictlyEqualToWhenStringIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(LENIENT_SAME)); + } + + @Test + void isStrictlyEqualToWhenResourcePathIsMatchingShouldPass() { + assertThat(forJson(SOURCE)).isStrictlyEqualTo("source.json"); + } + + @Test + void isStrictlyEqualToWhenResourcePathIsNotMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo("lenient-same.json")); + } + + @ParameterizedTest + @MethodSource("source") + void isStrictlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isStrictlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected)); + } + + + @Test + void isNotEqualToWhenStringIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE)); + } + + @Test + void isNotEqualToWhenNullActualShouldPass() { + assertThat(forJson(null)).isNotEqualTo(SOURCE); + } + + @Test + void isNotEqualToWhenStringIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT); + } + + @Test + void isNotEqualToAsObjectWhenExpectedIsNotAStringShouldNotFail() { + assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE.getBytes()); + } + + @Test + void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT)); + } + + @Test + void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT); + } + + @Test + void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT)); + } + + @Test + void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo("different.json", JSONCompareMode.LENIENT); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotEqualToWhenResourceIsMatchingAndLenientShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(forJson(SOURCE)) + .isNotEqualTo(expected, JSONCompareMode.LENIENT)); + } + + @ParameterizedTest + @MethodSource("different") + void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + @Test + void isNotEqualToWhenStringIsMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, COMPARATOR)); + } + + @Test + void isNotEqualToWhenStringIsNotMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, COMPARATOR); + } + + @Test + void isNotEqualToWhenResourcePathIsMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", COMPARATOR)); + } + + @Test + void isNotEqualToWhenResourcePathIsNotMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo("different.json", COMPARATOR); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class).isThrownBy( + () -> assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR)); + } + + @ParameterizedTest + @MethodSource("different") + void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR); + } + + @Test + void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createResource(LENIENT_SAME), COMPARATOR)); + } + + @Test + void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass() { + assertThat(forJson(SOURCE)).isNotEqualTo(createResource(DIFFERENT), COMPARATOR); + } + + @Test + void isNotLenientlyEqualToWhenNullActualShouldPass() { + assertThat(forJson(null)).isNotLenientlyEqualTo(SOURCE); + } + + @Test + void isNotLenientlyEqualToWhenStringIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(DIFFERENT); + } + + @Test + void isNotLenientlyEqualToWhenResourcePathIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("lenient-same.json")); + } + + @Test + void isNotLenientlyEqualToWhenResourcePathIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("different.json"); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotLenientlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected)); + } + + @ParameterizedTest + @MethodSource("different") + void isNotLenientlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected); + } + + @Test + void isNotStrictlyEqualToWhenStringIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(SOURCE)); + } + + @Test + void isNotStrictlyEqualToWhenStringIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(LENIENT_SAME); + } + + @Test + void isNotStrictlyEqualToWhenResourcePathIsMatchingShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("source.json")); + } + + @Test + void isNotStrictlyEqualToWhenResourcePathIsNotMatchingShouldPass() { + assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("lenient-same.json"); + } + + @ParameterizedTest + @MethodSource("source") + void isNotStrictlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected)); + } + + @ParameterizedTest + @MethodSource("lenientSame") + void isNotStrictlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) { + assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected); + } + + @Test + void isNullWhenActualIsNullShouldPass() { + assertThat(forJson(null)).isNull(); + } + + private Path createFile(String content) { + try { + Path temp = Files.createTempFile("file", ".json"); + Files.writeString(temp, content); + return temp; + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private InputStream createInputStream(String content) { + return new ByteArrayInputStream(content.getBytes()); + } + + private Resource createResource(String content) { + return new ByteArrayResource(content.getBytes()); + } + + private static String loadJson(String path) { + try { + ClassPathResource resource = new ClassPathResource(path, JsonContentAssertTests.class); + return new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + + } + + private AssertProvider forJson(@Nullable String json) { + return () -> new JsonContentAssert(json, JsonContentAssertTests.class); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java new file mode 100644 index 000000000000..6e4131c46f66 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.json; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link JsonContent}. + * + * @author Phillip Webb + */ +class JsonContentTests { + + private static final String JSON = "{\"name\":\"spring\", \"age\":100}"; + + @Test + void createWhenJsonIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new JsonContent(null, null)) + .withMessageContaining("JSON must not be null"); + } + + @Test + @SuppressWarnings("deprecation") + void assertThatShouldReturnJsonContentAssert() { + JsonContent content = new JsonContent(JSON, getClass()); + assertThat(content.assertThat()).isInstanceOf(JsonContentAssert.class); + } + + @Test + void getJsonShouldReturnJson() { + JsonContent content = new JsonContent(JSON, getClass()); + assertThat(content.getJson()).isEqualTo(JSON); + } + + @Test + void toStringShouldReturnString() { + JsonContent content = new JsonContent(JSON, getClass()); + assertThat(content.toString()).isEqualTo("JsonContent " + JSON); + } + +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/different.json b/spring-test/src/test/resources/org/springframework/test/json/different.json new file mode 100644 index 000000000000..d641ea86e155 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/different.json @@ -0,0 +1,6 @@ +{ + "gnirps": [ + "boot", + "framework" + ] +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/example.json b/spring-test/src/test/resources/org/springframework/test/json/example.json new file mode 100644 index 000000000000..cb218493f63a --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/example.json @@ -0,0 +1,4 @@ +{ + "name": "Spring", + "age": 123 +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/lenient-same.json b/spring-test/src/test/resources/org/springframework/test/json/lenient-same.json new file mode 100644 index 000000000000..89367f7bf4a2 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/lenient-same.json @@ -0,0 +1,6 @@ +{ + "spring": [ + "framework", + "boot" + ] +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/nulls.json b/spring-test/src/test/resources/org/springframework/test/json/nulls.json new file mode 100644 index 000000000000..1c1d3078254a --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/nulls.json @@ -0,0 +1,4 @@ +{ + "valuename": "spring", + "nullname": null +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/simpsons.json b/spring-test/src/test/resources/org/springframework/test/json/simpsons.json new file mode 100644 index 000000000000..1117d6864e17 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/simpsons.json @@ -0,0 +1,36 @@ +{ + "familyMembers": [ + { + "name": "Homer" + }, + { + "name": "Marge" + }, + { + "name": "Bart" + }, + { + "name": "Lisa" + }, + { + "name": "Maggie" + } + ], + "indexedFamilyMembers": { + "father": { + "name": "Homer" + }, + "mother": { + "name": "Marge" + }, + "son": { + "name": "Bart" + }, + "daughter": { + "name": "Lisa" + }, + "baby": { + "name": "Maggie" + } + } +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/source.json b/spring-test/src/test/resources/org/springframework/test/json/source.json new file mode 100644 index 000000000000..1b179b925301 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/source.json @@ -0,0 +1,6 @@ +{ + "spring": [ + "boot", + "framework" + ] +} diff --git a/spring-test/src/test/resources/org/springframework/test/json/types.json b/spring-test/src/test/resources/org/springframework/test/json/types.json new file mode 100644 index 000000000000..dd2dda3f1901 --- /dev/null +++ b/spring-test/src/test/resources/org/springframework/test/json/types.json @@ -0,0 +1,18 @@ +{ + "str": "foo", + "num": 5, + "pi": 3.1415926, + "bool": true, + "arr": [ + 42 + ], + "colorMap": { + "red": "rojo" + }, + "whitespace": " ", + "emptyString": "", + "emptyArray": [ + ], + "emptyMap": { + } +}