Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
* SUT for the "JSON Patch DTO with NON-PRIMITIVE value" e2e test.
*
* Goal: prove that, with [org.evomaster.core.EMConfig.dtoForRequestPayload] enabled, EvoMaster
* generates BOTH the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation`
* DTO) AND the nested DTO for the NON-PRIMITIVE `value` of those operations (the `Item` object DTO).
*
* The OpenAPI schema is the one auto-generated by Spring (springdoc, served at `/v3/api-docs`);
* no static YAML is used. springdoc names the array-element component after the Kotlin class, so the
* element type is named [Item] and EvoMaster generates a DTO called `Item`. Endpoint under test:
* [patchResource] (PATCH, `application/json-patch+json`).
*
* Key mechanic: the TYPE of the JSON Patch operation `value` is resolved from the OpenAPI schema of a
* sibling operation on the same path (GET 2xx response -> PUT body -> POST body; see
* `JsonPatchSchemaResolver`). We expose, via [getResource] (a GET, which produces no DTO of its own),
* a resource whose patchable fields are ARRAYS OF OBJECTS ([Item]). Plain nested objects would be
* flattened into primitive leaf paths by the JSON Patch builder; an array-of-objects is the case that
* yields a non-primitive value gene, which makes EvoMaster generate a nested DTO for the `Item` object.
*
* Two array fields are exposed on purpose: the builder groups value types by gene class, so both
* arrays collapse into the same value type (array of Item), making the value ALWAYS a non-primitive
* array of objects, while keeping the path enum multi-valued (i.e. mutable during search).
*/
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@RestController
@RequestMapping("/resource")
open class JsonPatchObjectValueApplication {

companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(JsonPatchObjectValueApplication::class.java, *args)
}
}

/** Hardcoded resource whose fields are arrays of objects (exposed as the `ObjectResource` schema). */
private val hardcoded = ObjectResource(
items = listOf(
Item(label = "first", quantity = 1),
Item(label = "second", quantity = 2)
),
archived = listOf(
Item(label = "old", quantity = 0)
)
)

@GetMapping("/{id}", produces = ["application/json"])
fun getResource(@PathVariable id: Long): ResponseEntity<ObjectResource> {
return ResponseEntity.ok(hardcoded)
}

@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity<String> {
return ResponseEntity.ok("OK")
}
}

/**
* Response types used only to expose the resource schema (arrays of objects) to the JSON Patch
* resolver. They are response bodies (not request bodies), so they are never turned into EvoMaster
* DTOs. The EvoMaster DTO for the `value` is named after the [Item] class (its springdoc component).
*/
data class ObjectResource(
val items: List<Item> = emptyList(),
val archived: List<Item> = emptyList()
)

data class Item(val label: String = "", val quantity: Int = 0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
* SUT for the "JSON Patch DTO with STRING value" e2e test.
*
* Goal: prove that, with [org.evomaster.core.EMConfig.dtoForRequestPayload] enabled, EvoMaster
* generates the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation`
* DTO, fields op/path/from/value) and that its `value` is a primitive (string) inlined as a
* literal (i.e. no nested value DTO is produced).
*
* The OpenAPI schema is the one auto-generated by Spring (springdoc, served at `/v3/api-docs`);
* no static YAML is used. Endpoint under test: [patchResource] (PATCH, `application/json-patch+json`).
*
* Key mechanic: the TYPE of the JSON Patch operation `value` is NOT taken from what this controller
* returns at runtime; it is resolved from the OpenAPI schema of a sibling operation on the same path
* (GET 2xx response -> PUT body -> POST body; see `JsonPatchSchemaResolver`). We expose the resource
* schema through [getResource] (a GET, which has no request body and therefore produces NO DTO of
* its own). Its response object has only string fields, so every JSON Patch operation `value` is a
* primitive string. As a consequence, the ONLY DTO EvoMaster can generate for this app is the shared
* `JsonPatchOperation` DTO: if a DTO appears, it is unambiguously the JSON Patch one.
*/
@SpringBootApplication(exclude = [SecurityAutoConfiguration::class])
@RestController
@RequestMapping("/resource")
open class JsonPatchStringValueApplication {

companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(JsonPatchStringValueApplication::class.java, *args)
}
}

/** Hardcoded resource whose fields are all strings (exposed as the `StringResource` schema). */
private val hardcoded = StringResource(name = "EvoMaster", description = "JSON Patch string value")

@GetMapping("/{id}", produces = ["application/json"])
fun getResource(@PathVariable id: Long): ResponseEntity<StringResource> {
return ResponseEntity.ok(hardcoded)
}

@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity<String> {
return ResponseEntity.ok("OK")
}
}

/**
* Response type used only to expose a string-only resource schema to the JSON Patch resolver.
* It is a response body (not a request body), so it is never turned into an EvoMaster DTO.
*/
data class StringResource(val name: String = "", val description: String = "")
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue

import com.foo.rest.examples.spring.openapi.v3.SpringController

/**
* Uses the OpenAPI schema auto-generated by Spring (springdoc): [SpringController.getProblemInfo]
* already points to `/v3/api-docs`, so no override and no static YAML are needed.
*/
class JsonPatchObjectValueController : SpringController(JsonPatchObjectValueApplication::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue

import com.foo.rest.examples.spring.openapi.v3.SpringController

/**
* Uses the OpenAPI schema auto-generated by Spring (springdoc): [SpringController.getProblemInfo]
* already points to `/v3/api-docs`, so no override and no static YAML are needed.
*/
class JsonPatchStringValueController : SpringController(JsonPatchStringValueApplication::class.java)
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ package org.evomaster.e2etests.spring.openapi.v3
import org.evomaster.client.java.controller.InstrumentedSutStarter
import org.evomaster.client.java.instrumentation.InputProperties
import org.evomaster.client.java.instrumentation.InstrumentingAgent
import org.evomaster.client.java.instrumentation.shared.ClassName
import org.evomaster.e2etests.utils.RestTestBase
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import java.util.Optional.ofNullable
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

/**
* Created by arcuri82 on 03-Mar-20.
Expand All @@ -25,4 +33,23 @@ abstract class SpringTestBase : RestTestBase(){
}
}

protected fun initDtoClass(name: String): Pair<KClass<out Any>, Any> {
val className = ClassName("org.foo.dto.$name")
val klass = loadClass(className).kotlin
Assertions.assertNotNull(klass)
return Pair(klass, klass.createInstance())
}

protected fun assertProperty(klass: KClass<out Any>, instance: Any, propertyName: String, propertyValue: Any?) {
val property = klass.memberProperties
.firstOrNull { it.name == propertyName } as? KMutableProperty1<Any, Any?>
Assertions.assertNotNull(property)

property?.let {
it.isAccessible = true
it.set(instance, ofNullable(propertyValue))
}
Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance))
}

}
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert

import com.foo.rest.examples.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertController
import org.evomaster.client.java.instrumentation.shared.ClassName
import org.evomaster.core.problem.rest.data.HttpVerb
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import java.util.Optional.ofNullable
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaMethod

Expand Down Expand Up @@ -161,25 +156,6 @@ class DtoReflectiveAssertEMTest: SpringTestBase() {
assertAdditionalPropertiesFunction(parentKlass, parentInstance, "no_root_key", childInstance)
}

private fun initDtoClass(name: String): Pair<KClass<out Any>, Any> {
val className = ClassName("org.foo.dto.$name")
val klass = loadClass(className).kotlin
Assertions.assertNotNull(klass)
return Pair(klass, klass.createInstance())
}

private fun assertProperty(klass: KClass<out Any>, instance: Any, propertyName: String, propertyValue: Any?) {
val property = klass.memberProperties
.firstOrNull { it.name == propertyName } as? KMutableProperty1<Any, Any?>
Assertions.assertNotNull(property)

property?.let {
it.isAccessible = true
it.set(instance, ofNullable(propertyValue)) // Set the value
}
Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance))
}

private fun assertAdditionalPropertiesFunction(klass: KClass<out Any>, instance: Any, propertyName: String, propertyValue: Any?) {
val setterFunction = klass.memberFunctions.firstOrNull {
it.name == "addAdditionalProperty"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.evomaster.e2etests.spring.openapi.v3.jsonpatchobjectvalue

import com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue.JsonPatchObjectValueController
import org.evomaster.core.problem.rest.data.HttpVerb
import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

/**
* E2E test: JSON Patch DTO with a NON-PRIMITIVE value.
*
* What is being tested:
* When `dtoForRequestPayload` is enabled and the SUT has a JSON Patch endpoint whose resource
* exposes an array-of-objects field, EvoMaster must generate:
* 1. the DTO that represents the JSON Patch operations (the shared `JsonPatchOperation` DTO), and
* 2. the nested DTO for the NON-PRIMITIVE `value` of those operations (the `Item` object DTO).
*
* Why the assertion is unambiguous:
* The SUT only exposes a GET (no request body -> no DTO) and a PATCH (json-patch). Therefore the
* only DTOs EvoMaster can generate are the JSON Patch ones. Reflectively loading
* `org.foo.dto.JsonPatchOperation` and `org.foo.dto.Item` after the generated suite is compiled
* proves that both were generated, i.e. that the value DTO of the JSON Patch operations exists.
*
* This mirrors the reflective-assert approach of
* [org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertEMTest].
*/
class JsonPatchObjectValueDtoEMTest : SpringTestBase() {

companion object {
@BeforeAll
@JvmStatic
fun init() {
initClass(JsonPatchObjectValueController())
}
}

@Test
fun testRunEM() {
runTestHandlingFlakyAndCompilation(
"JsonPatchObjectValueDtoEM",
"org.foo.JsonPatchObjectValueDtoEM",
100,
) { args: MutableList<String> ->

setOption(args, "dtoForRequestPayload", "true")
// This SUT intentionally has no authentication, and the PATCH returns 2xx, so the
// security oracle would flag a fault 208 (Anonymous Modifications). It is irrelevant to
// what we assert here (JSON Patch DTO generation), so we disable it to keep the suite clean.
setOption(args, "security", "false")

val solution = initAndRun(args)

Assertions.assertTrue(solution.individuals.size >= 1)
assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/resource/{id}", "OK")
}

assertJsonPatchOperationDtoCreated()
assertNonPrimitiveValueDtoCreated()
}

/**
* The shared JsonPatchOperation DTO must exist with the RFC 6902 fields (op, path, from, value).
*/
private fun assertJsonPatchOperationDtoCreated() {
val (klass, instance) = initDtoClass("JsonPatchOperation")
assertProperty(klass, instance, "op", "add")
assertProperty(klass, instance, "path", "/items")
assertProperty(klass, instance, "from", "/items")
}

/**
* The nested DTO for the non-primitive `value` of the operations must exist. Its name comes
* from the OpenAPI component `Item`, with fields label (String) and quantity (Int).
*/
private fun assertNonPrimitiveValueDtoCreated() {
val (klass, instance) = initDtoClass("Item")
assertProperty(klass, instance, "label", "a label")
assertProperty(klass, instance, "quantity", 7)
}
}
Loading
Loading