diff --git a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/keyvalues.kt b/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/keyvalues.kt index 1740e2d0d55..8fc0c9d1859 100644 --- a/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/keyvalues.kt +++ b/kotest-assertions/kotest-assertions-json/src/jvmMain/kotlin/io/kotest/assertions/json/keyvalues.kt @@ -3,6 +3,10 @@ package io.kotest.assertions.json import com.jayway.jsonpath.InvalidPathException import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.PathNotFoundException +import io.kotest.assertions.Actual +import io.kotest.assertions.Expected +import io.kotest.assertions.intellijFormatError +import io.kotest.assertions.print.print import io.kotest.matchers.Matcher import io.kotest.matchers.MatcherResult import io.kotest.matchers.should @@ -21,23 +25,42 @@ inline fun String.shouldNotContainJsonKeyValue(path: String, value: this shouldNot containJsonKeyValue(path, value) inline fun containJsonKeyValue(path: String, t: T) = object : Matcher { - override fun test(value: String?): MatcherResult { - val sub = when (value) { - null -> value - else -> if (value.length < 50) value.trim() else value.substring(0, 50).trim() + "..." - } + private fun keyIsAbsentFailure() = MatcherResult( + false, + { "Expected given to contain json key <'$path'> but key was not found." }, + { "Expected given to not contain json key <'$path'> but key was found." } + ) - val passed = value != null && try { - JsonPath.parse(value).read(path, T::class.java) == t + private fun invalidJsonFailure(actualJson: String?) = MatcherResult( + false, + { "Expected a valid JSON, but was ${if (actualJson == null) "null" else "empty" }" }, + { "Expected a valid JSON, but was ${if (actualJson == null) "null" else "empty" }" }, + ) + + private fun extractKey(value: String?): T? { + return try { + JsonPath.parse(value).read(path, T::class.java) } catch (e: PathNotFoundException) { - false + null } catch (e: InvalidPathException) { throw AssertionError("$path is not a valid JSON path") } + } + + override fun test(value: String?): MatcherResult { + if (value.isNullOrEmpty()) return invalidJsonFailure(value) + + val sub = + if (value.length < 50) value.trim() + else value.substring(0, 50).trim() + "..." + + val actualKeyValue = extractKey(value) + val passed = t == actualKeyValue + if (!passed && actualKeyValue == null) return keyIsAbsentFailure() return MatcherResult( passed, - { "$sub should contain the element $path = $t" }, + { "Value mismatch at '$path': ${intellijFormatError(Expected(t.print()), Actual(actualKeyValue.print()))}" }, { "$sub should not contain the element $path = $t" } diff --git a/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/ContainJsonKeyValueTest.kt b/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/ContainJsonKeyValueTest.kt new file mode 100644 index 00000000000..7a8c44a6a5c --- /dev/null +++ b/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/ContainJsonKeyValueTest.kt @@ -0,0 +1,89 @@ +package com.sksamuel.kotest.tests.json + +import io.kotest.assertions.asClue +import io.kotest.assertions.json.shouldContainJsonKeyValue +import io.kotest.assertions.json.shouldNotContainJsonKeyValue +import io.kotest.assertions.shouldFail +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import org.intellij.lang.annotations.Language + +class ContainJsonKeyValueTest : StringSpec({ + @Language("JSON") + val json = """ + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95, + "code": 1 + } + } + } + """.trimIndent() + + "Negated assertions" { + "".shouldNotContainJsonKeyValue("$.store.bicycle.color", "red") + "{}".shouldNotContainJsonKeyValue("$.store.bicycle.color", "red") + """{ "foo": "bar" }""".shouldNotContainJsonKeyValue("foo", "baz") + shouldFail { + """{ "foo": "bar" }""".shouldNotContainJsonKeyValue("foo", "bar") + }.message shouldBe """{ "foo": "bar" } should not contain the element foo = bar""" + } + + "Failure message states if key is missing, when it's missing" { + shouldFail { + json.shouldContainJsonKeyValue("$.bicycle.engine", "V2") + }.message shouldBe """ + Expected given to contain json key <'$.bicycle.engine'> but key was not found. + """.trimIndent() + } + + "Failure message states states value mismatch if key is present with different value" { + shouldFail { + json.shouldContainJsonKeyValue("$.store.book[0].price", 9.95) + }.message shouldBe """ + Value mismatch at '$.store.book[0].price': expected:<9.95> but was:<8.95> + """.trimIndent() + } + + "test json key value" { + json.shouldContainJsonKeyValue("$.store.bicycle.color", "red") + json.shouldContainJsonKeyValue("$.store.book[0].category", "reference") + json.shouldContainJsonKeyValue("$.store.book[0].price", 8.95) + json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh") + json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh") + json.shouldContainJsonKeyValue("$.store.bicycle.code", 1L) + + json.shouldNotContainJsonKeyValue("$.store.book[1].author", "JK Rowling") + + shouldThrow { null.shouldContainJsonKeyValue("ab", "cd") }.message shouldBe + "Expected a valid JSON, but was null" + + shouldThrow { "".shouldContainJsonKeyValue("ab", "cd") }.message shouldBe + "Expected a valid JSON, but was empty" + + "contract should work".asClue { + fun use(@Suppress("UNUSED_PARAMETER") json: String) {} + + val nullableJson = """{"data": "value"}""" + nullableJson.shouldContainJsonKeyValue("data", "value") + use(nullableJson) + } + } +}) diff --git a/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/JvmJsonAssertionsTest.kt b/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/JvmJsonAssertionsTest.kt index f3e33e9b498..23eae5ee0d6 100644 --- a/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/JvmJsonAssertionsTest.kt +++ b/kotest-assertions/kotest-assertions-json/src/jvmTest/kotlin/com/sksamuel/kotest/tests/json/JvmJsonAssertionsTest.kt @@ -65,41 +65,6 @@ class JvmJsonAssertionsTest : StringSpec({ } } - "test json key value" { - json.shouldContainJsonKeyValue("$.store.bicycle.color", "red") - json.shouldContainJsonKeyValue("$.store.book[0].category", "reference") - json.shouldContainJsonKeyValue("$.store.book[0].price", 8.95) - json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh") - json.shouldContainJsonKeyValue("$.store.book[1].author", "Evelyn Waugh") - json.shouldContainJsonKeyValue("$.store.bicycle.code", 1L) - - json.shouldNotContainJsonKeyValue("$.store.book[1].author", "JK Rowling") - - shouldFail { json.shouldContainJsonKeyValue("$.store.bicycle.wheels", 2) } - .message shouldBe """{ - "store": { - "book": [ - {... should contain the element ${'$'}.store.bicycle.wheels = 2 - """.trimIndent() - - shouldThrow { - json.shouldContainJsonKeyValue("$.store.book[1].author", "JK Rowling") - }.message shouldBe """{ - "store": { - "book": [ - {... should contain the element ${'$'}.store.book[1].author = JK Rowling""" - - shouldThrow { null.shouldContainJsonKeyValue("ab", "cd") } - - "contract should work".asClue { - fun use(@Suppress("UNUSED_PARAMETER") json: String) {} - - val nullableJson = """{"data": "value"}""" - nullableJson.shouldContainJsonKeyValue("data", "value") - use(nullableJson) - } - } - "test json match by resource" { val testJson1 = """ { "name" : "sam", "location" : "chicago" } """