Skip to content

Commit a5ed32d

Browse files
authored
Merge pull request #1603 from WebFuzzing/feature/jsonPatch_DTOConvertion
json patch dto conversion
2 parents d2090a6 + 3fceeac commit a5ed32d

14 files changed

Lines changed: 701 additions & 32 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.PatchMapping
9+
import org.springframework.web.bind.annotation.PathVariable
10+
import org.springframework.web.bind.annotation.RequestBody
11+
import org.springframework.web.bind.annotation.RequestMapping
12+
import org.springframework.web.bind.annotation.RestController
13+
14+
/**
15+
* SUT for the "JSON Patch DTO with NON-PRIMITIVE value" e2e test.
16+
*
17+
* Goal: prove that, with [org.evomaster.core.EMConfig.dtoForRequestPayload] enabled, EvoMaster
18+
* generates BOTH the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation`
19+
* DTO) AND the nested DTO for the NON-PRIMITIVE `value` of those operations (the `Item` object DTO).
20+
*
21+
* The OpenAPI schema is the one auto-generated by Spring (springdoc, served at `/v3/api-docs`);
22+
* no static YAML is used. springdoc names the array-element component after the Kotlin class, so the
23+
* element type is named [Item] and EvoMaster generates a DTO called `Item`. Endpoint under test:
24+
* [patchResource] (PATCH, `application/json-patch+json`).
25+
*
26+
* Key mechanic: the TYPE of the JSON Patch operation `value` is resolved from the OpenAPI schema of a
27+
* sibling operation on the same path (GET 2xx response -> PUT body -> POST body; see
28+
* `JsonPatchSchemaResolver`). We expose, via [getResource] (a GET, which produces no DTO of its own),
29+
* a resource whose patchable fields are ARRAYS OF OBJECTS ([Item]). Plain nested objects would be
30+
* flattened into primitive leaf paths by the JSON Patch builder; an array-of-objects is the case that
31+
* yields a non-primitive value gene, which makes EvoMaster generate a nested DTO for the `Item` object.
32+
*
33+
* Two array fields are exposed on purpose: the builder groups value types by gene class, so both
34+
* arrays collapse into the same value type (array of Item), making the value ALWAYS a non-primitive
35+
* array of objects, while keeping the path enum multi-valued (i.e. mutable during search).
36+
*/
37+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
38+
@RestController
39+
@RequestMapping("/resource")
40+
open class JsonPatchObjectValueApplication {
41+
42+
companion object {
43+
@JvmStatic
44+
fun main(args: Array<String>) {
45+
SpringApplication.run(JsonPatchObjectValueApplication::class.java, *args)
46+
}
47+
}
48+
49+
/** Hardcoded resource whose fields are arrays of objects (exposed as the `ObjectResource` schema). */
50+
private val hardcoded = ObjectResource(
51+
items = listOf(
52+
Item(label = "first", quantity = 1),
53+
Item(label = "second", quantity = 2)
54+
),
55+
archived = listOf(
56+
Item(label = "old", quantity = 0)
57+
)
58+
)
59+
60+
@GetMapping("/{id}", produces = ["application/json"])
61+
fun getResource(@PathVariable id: Long): ResponseEntity<ObjectResource> {
62+
return ResponseEntity.ok(hardcoded)
63+
}
64+
65+
@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
66+
fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity<String> {
67+
return ResponseEntity.ok("OK")
68+
}
69+
}
70+
71+
/**
72+
* Response types used only to expose the resource schema (arrays of objects) to the JSON Patch
73+
* resolver. They are response bodies (not request bodies), so they are never turned into EvoMaster
74+
* DTOs. The EvoMaster DTO for the `value` is named after the [Item] class (its springdoc component).
75+
*/
76+
data class ObjectResource(
77+
val items: List<Item> = emptyList(),
78+
val archived: List<Item> = emptyList()
79+
)
80+
81+
data class Item(val label: String = "", val quantity: Int = 0)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue
2+
3+
import org.springframework.boot.SpringApplication
4+
import org.springframework.boot.autoconfigure.SpringBootApplication
5+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.bind.annotation.GetMapping
8+
import org.springframework.web.bind.annotation.PatchMapping
9+
import org.springframework.web.bind.annotation.PathVariable
10+
import org.springframework.web.bind.annotation.RequestBody
11+
import org.springframework.web.bind.annotation.RequestMapping
12+
import org.springframework.web.bind.annotation.RestController
13+
14+
/**
15+
* SUT for the "JSON Patch DTO with STRING value" e2e test.
16+
*
17+
* Goal: prove that, with [org.evomaster.core.EMConfig.dtoForRequestPayload] enabled, EvoMaster
18+
* generates the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation`
19+
* DTO, fields op/path/from/value) and that its `value` is a primitive (string) inlined as a
20+
* literal (i.e. no nested value DTO is produced).
21+
*
22+
* The OpenAPI schema is the one auto-generated by Spring (springdoc, served at `/v3/api-docs`);
23+
* no static YAML is used. Endpoint under test: [patchResource] (PATCH, `application/json-patch+json`).
24+
*
25+
* Key mechanic: the TYPE of the JSON Patch operation `value` is NOT taken from what this controller
26+
* returns at runtime; it is resolved from the OpenAPI schema of a sibling operation on the same path
27+
* (GET 2xx response -> PUT body -> POST body; see `JsonPatchSchemaResolver`). We expose the resource
28+
* schema through [getResource] (a GET, which has no request body and therefore produces NO DTO of
29+
* its own). Its response object has only string fields, so every JSON Patch operation `value` is a
30+
* primitive string. As a consequence, the ONLY DTO EvoMaster can generate for this app is the shared
31+
* `JsonPatchOperation` DTO: if a DTO appears, it is unambiguously the JSON Patch one.
32+
*/
33+
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
34+
@RestController
35+
@RequestMapping("/resource")
36+
open class JsonPatchStringValueApplication {
37+
38+
companion object {
39+
@JvmStatic
40+
fun main(args: Array<String>) {
41+
SpringApplication.run(JsonPatchStringValueApplication::class.java, *args)
42+
}
43+
}
44+
45+
/** Hardcoded resource whose fields are all strings (exposed as the `StringResource` schema). */
46+
private val hardcoded = StringResource(name = "EvoMaster", description = "JSON Patch string value")
47+
48+
@GetMapping("/{id}", produces = ["application/json"])
49+
fun getResource(@PathVariable id: Long): ResponseEntity<StringResource> {
50+
return ResponseEntity.ok(hardcoded)
51+
}
52+
53+
@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
54+
fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity<String> {
55+
return ResponseEntity.ok("OK")
56+
}
57+
}
58+
59+
/**
60+
* Response type used only to expose a string-only resource schema to the JSON Patch resolver.
61+
* It is a response body (not a request body), so it is never turned into an EvoMaster DTO.
62+
*/
63+
data class StringResource(val name: String = "", val description: String = "")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
5+
/**
6+
* Uses the OpenAPI schema auto-generated by Spring (springdoc): [SpringController.getProblemInfo]
7+
* already points to `/v3/api-docs`, so no override and no static YAML are needed.
8+
*/
9+
class JsonPatchObjectValueController : SpringController(JsonPatchObjectValueApplication::class.java)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue
2+
3+
import com.foo.rest.examples.spring.openapi.v3.SpringController
4+
5+
/**
6+
* Uses the OpenAPI schema auto-generated by Spring (springdoc): [SpringController.getProblemInfo]
7+
* already points to `/v3/api-docs`, so no override and no static YAML are needed.
8+
*/
9+
class JsonPatchStringValueController : SpringController(JsonPatchStringValueApplication::class.java)

core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@ package org.evomaster.e2etests.spring.openapi.v3
33
import org.evomaster.client.java.controller.InstrumentedSutStarter
44
import org.evomaster.client.java.instrumentation.InputProperties
55
import org.evomaster.client.java.instrumentation.InstrumentingAgent
6+
import org.evomaster.client.java.instrumentation.shared.ClassName
67
import org.evomaster.e2etests.utils.RestTestBase
8+
import org.junit.jupiter.api.Assertions
79
import org.junit.jupiter.api.BeforeAll
10+
import java.util.Optional.ofNullable
11+
import kotlin.reflect.KClass
12+
import kotlin.reflect.KMutableProperty1
13+
import kotlin.reflect.full.createInstance
14+
import kotlin.reflect.full.memberProperties
15+
import kotlin.reflect.jvm.isAccessible
816

917
/**
1018
* Created by arcuri82 on 03-Mar-20.
@@ -25,4 +33,23 @@ abstract class SpringTestBase : RestTestBase(){
2533
}
2634
}
2735

36+
protected fun initDtoClass(name: String): Pair<KClass<out Any>, Any> {
37+
val className = ClassName("org.foo.dto.$name")
38+
val klass = loadClass(className).kotlin
39+
Assertions.assertNotNull(klass)
40+
return Pair(klass, klass.createInstance())
41+
}
42+
43+
protected fun assertProperty(klass: KClass<out Any>, instance: Any, propertyName: String, propertyValue: Any?) {
44+
val property = klass.memberProperties
45+
.firstOrNull { it.name == propertyName } as? KMutableProperty1<Any, Any?>
46+
Assertions.assertNotNull(property)
47+
48+
property?.let {
49+
it.isAccessible = true
50+
it.set(instance, ofNullable(propertyValue))
51+
}
52+
Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance))
53+
}
54+
2855
}

core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
package org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert
22

33
import com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertController
4-
import org.evomaster.client.java.instrumentation.shared.ClassName
54
import org.evomaster.core.problem.rest.data.HttpVerb
65
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
76
import org.junit.jupiter.api.Assertions
87
import org.junit.jupiter.api.BeforeAll
98
import org.junit.jupiter.api.Test
10-
import java.util.Optional.ofNullable
119
import kotlin.reflect.KClass
12-
import kotlin.reflect.KMutableProperty1
13-
import kotlin.reflect.full.createInstance
1410
import kotlin.reflect.full.memberFunctions
15-
import kotlin.reflect.full.memberProperties
1611
import kotlin.reflect.jvm.isAccessible
1712
import kotlin.reflect.jvm.javaMethod
1813

@@ -161,25 +156,6 @@ class DtoReflectiveAssertEMTest: SpringTestBase() {
161156
assertAdditionalPropertiesFunction(parentKlass, parentInstance, "no_root_key", childInstance)
162157
}
163158

164-
private fun initDtoClass(name: String): Pair<KClass<out Any>, Any> {
165-
val className = ClassName("org.foo.dto.$name")
166-
val klass = loadClass(className).kotlin
167-
Assertions.assertNotNull(klass)
168-
return Pair(klass, klass.createInstance())
169-
}
170-
171-
private fun assertProperty(klass: KClass<out Any>, instance: Any, propertyName: String, propertyValue: Any?) {
172-
val property = klass.memberProperties
173-
.firstOrNull { it.name == propertyName } as? KMutableProperty1<Any, Any?>
174-
Assertions.assertNotNull(property)
175-
176-
property?.let {
177-
it.isAccessible = true
178-
it.set(instance, ofNullable(propertyValue)) // Set the value
179-
}
180-
Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance))
181-
}
182-
183159
private fun assertAdditionalPropertiesFunction(klass: KClass<out Any>, instance: Any, propertyName: String, propertyValue: Any?) {
184160
val setterFunction = klass.memberFunctions.firstOrNull {
185161
it.name == "addAdditionalProperty"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.evomaster.e2etests.spring.openapi.v3.jsonpatchobjectvalue
2+
3+
import com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue.JsonPatchObjectValueController
4+
import org.evomaster.core.problem.rest.data.HttpVerb
5+
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
6+
import org.junit.jupiter.api.Assertions
7+
import org.junit.jupiter.api.BeforeAll
8+
import org.junit.jupiter.api.Test
9+
10+
/**
11+
* E2E test: JSON Patch DTO with a NON-PRIMITIVE value.
12+
*
13+
* What is being tested:
14+
* When `dtoForRequestPayload` is enabled and the SUT has a JSON Patch endpoint whose resource
15+
* exposes an array-of-objects field, EvoMaster must generate:
16+
* 1. the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation` DTO), and
17+
* 2. the nested DTO for the NON-PRIMITIVE `value` of those operations (the `Item` object DTO).
18+
*
19+
* Why the assertion is unambiguous:
20+
* The SUT only exposes a GET (no request body -> no DTO) and a PATCH (json-patch). Therefore the
21+
* only DTOs EvoMaster can generate are the JSON Patch ones. Reflectively loading
22+
* `org.foo.dto.JsonPatchOperation` and `org.foo.dto.Item` after the generated suite is compiled
23+
* proves that both were generated, i.e. that the value DTO of the JSON Patch operations exists.
24+
*
25+
* This mirrors the reflective-assert approach of
26+
* [org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertEMTest].
27+
*/
28+
class JsonPatchObjectValueDtoEMTest : SpringTestBase() {
29+
30+
companion object {
31+
@BeforeAll
32+
@JvmStatic
33+
fun init() {
34+
initClass(JsonPatchObjectValueController())
35+
}
36+
}
37+
38+
@Test
39+
fun testRunEM() {
40+
runTestHandlingFlakyAndCompilation(
41+
"JsonPatchObjectValueDtoEM",
42+
"org.foo.JsonPatchObjectValueDtoEM",
43+
100,
44+
) { args: MutableList<String> ->
45+
46+
setOption(args, "dtoForRequestPayload", "true")
47+
// This SUT intentionally has no authentication, and the PATCH returns 2xx, so the
48+
// security oracle would flag a fault 208 (Anonymous Modifications). It is irrelevant to
49+
// what we assert here (JSON Patch DTO generation), so we disable it to keep the suite clean.
50+
setOption(args, "security", "false")
51+
52+
val solution = initAndRun(args)
53+
54+
Assertions.assertTrue(solution.individuals.size >= 1)
55+
assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/resource/{id}", "OK")
56+
}
57+
58+
assertJsonPatchOperationDtoCreated()
59+
assertNonPrimitiveValueDtoCreated()
60+
}
61+
62+
/**
63+
* The shared JsonPatchOperation DTO must exist with the RFC 6902 fields (op, path, from, value).
64+
*/
65+
private fun assertJsonPatchOperationDtoCreated() {
66+
val (klass, instance) = initDtoClass("JsonPatchOperation")
67+
assertProperty(klass, instance, "op", "add")
68+
assertProperty(klass, instance, "path", "/items")
69+
assertProperty(klass, instance, "from", "/items")
70+
}
71+
72+
/**
73+
* The nested DTO for the non-primitive `value` of the operations must exist. Its name comes
74+
* from the OpenAPI component `Item`, with fields label (String) and quantity (Int).
75+
*/
76+
private fun assertNonPrimitiveValueDtoCreated() {
77+
val (klass, instance) = initDtoClass("Item")
78+
assertProperty(klass, instance, "label", "a label")
79+
assertProperty(klass, instance, "quantity", 7)
80+
}
81+
}

0 commit comments

Comments
 (0)