Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
55abe37
JsonPatch to dto
suarezrominajulieta Jun 16, 2026
1af1234
Add external api extract and test
suarezrominajulieta Jun 16, 2026
32ebb56
Merge branch 'master' into feature/jsonPatch_externalApiTest
suarezrominajulieta Jun 16, 2026
d79a3e3
Merge branch 'feature/jsonPatch_e2e' into feature/jsonPatch_externalA…
suarezrominajulieta Jun 16, 2026
2f1f033
externalApi is not for this pr
suarezrominajulieta Jun 20, 2026
5e05132
Merge branch 'feature/jsonPatch_e2e' into feature/jsonPatch_DTOConver…
suarezrominajulieta Jun 20, 2026
fe82830
Fixes for coments and tests
suarezrominajulieta Jun 20, 2026
0054b4e
Merge branch 'master' into feature/jsonPatch_DTOConvertion
suarezrominajulieta Jun 26, 2026
91b0e56
Fixes for comments in pr
suarezrominajulieta Jun 26, 2026
0b16e03
Merge branch 'master' into feature/jsonPatch_DTOConvertion
suarezrominajulieta Jul 5, 2026
9fa31d4
Merge branch 'master' into feature/jsonPatch_DTOConvertion
suarezrominajulieta Jul 7, 2026
a2e50e8
We add 2 tests to prove dto conversion in json patch
suarezrominajulieta Jul 8, 2026
da93924
Merge branch 'master' into feature/jsonPatch_DTOConvertion
suarezrominajulieta Jul 8, 2026
6b48ffa
Fixes for pr
suarezrominajulieta Jul 12, 2026
8973401
Extract duplicated code
suarezrominajulieta Jul 13, 2026
7fe3fcf
Feature flags for xml and jsonpatch
suarezrominajulieta Jul 13, 2026
80a4c7b
Fix for tests
suarezrominajulieta Jul 14, 2026
28545db
Merge branch 'master' into feature/jsonPatch_XML_featureFlag
suarezrominajulieta Jul 14, 2026
a0c37b6
Update options.md
suarezrominajulieta Jul 14, 2026
2c3473c
Add tests with keys in false and assertFalse for coverage
suarezrominajulieta Jul 14, 2026
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
Expand Up @@ -5,8 +5,11 @@ import org.evomaster.core.EMConfig
import org.evomaster.core.output.OutputFormat
import org.evomaster.core.problem.rest.data.HttpVerb
import org.evomaster.e2etests.spring.rest.bb.SpringTestBase
import org.evomaster.e2etests.utils.CoveredTargets
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

Expand Down Expand Up @@ -69,4 +72,35 @@ class BBJsonPatchTest : SpringTestBase() {
assertHasAtLeastOne(solution, HttpVerb.PATCH, 400, "/pets/{id}/sequence", null)
}
}

@Test
fun testBlackBoxWithoutJsonPatchSupport() {
val specificOpTargets = listOf(
"JSON_PATCH_ADD",
"JSON_PATCH_REMOVE",
"JSON_PATCH_REPLACE",
"JSON_PATCH_MOVE",
"JSON_PATCH_COPY",
"JSON_PATCH_TEST",
"JSON_PATCH_SEQUENCE"
)

runBlackBoxEM(OutputFormat.KOTLIN_JUNIT_5, "BBJsonPatchEM_NoSupport", 1000, 3, false) { args ->
setOption(args, "enableJsonPatchGeneSupport", "false")

val solution = initAndRun(args)
assertTrue(solution.individuals.size >= 1)
}

val coveredWithFlagOff = specificOpTargets.count { CoveredTargets.isCovered(it) }
println("=== Flag OFF: $coveredWithFlagOff/${specificOpTargets.size} operation-specific targets covered ===")
specificOpTargets.forEach { target ->
println(" [${ if (CoveredTargets.isCovered(target)) "X" else " " }] $target")
}

assertFalse(
CoveredTargets.areCovered(specificOpTargets),
"Without enableJsonPatchGeneSupport, EvoMaster should NOT cover all JSON Patch operation targets"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import org.evomaster.core.EMConfig
import org.evomaster.core.output.OutputFormat
import org.evomaster.core.problem.rest.data.HttpVerb
import org.evomaster.e2etests.spring.rest.bb.SpringTestBase
import org.evomaster.e2etests.utils.CoveredTargets
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

Expand Down Expand Up @@ -67,4 +70,41 @@ class BBXMLTest : SpringTestBase() {
assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbxml/projects", null)
}
}

@Test
fun testBlackBoxWithoutXmlBodySupport() {
// These targets require a well-formed XML body to reach a 200 response.
// With enableXmlBodyGeneSupport=false, EvoMaster falls back to generic field
// naming (schema ref name or 'body') instead of the actual JAXB element names,
// so Spring's XML deserializer receives structurally wrong documents and returns
// 400 for most requests → the 200-branch targets stay uncovered.
val xmlBodyTargets = listOf(
"XML_TO_STRING",
"EMPLOYEE",
"COMPANY",
"DEPARTMENT",
"ORGANIZATION",
"PERSON_ATTR",
"PROJECT",
"PROJECTS"
)

runBlackBoxEM(OutputFormat.KOTLIN_JUNIT_5, "BBXmlEM_NoSupport", 1000, 3, false) { args ->
setOption(args, "enableXmlBodyGeneSupport", "false")

val solution = initAndRun(args)
assertTrue(solution.individuals.size >= 1)
}

val coveredWithFlagOff = xmlBodyTargets.count { CoveredTargets.isCovered(it) }
println("=== Flag OFF: $coveredWithFlagOff/${xmlBodyTargets.size} XML body targets covered ===")
xmlBodyTargets.forEach { target ->
println(" [${if (CoveredTargets.isCovered(target)) "X" else " "}] $target")
}

assertFalse(
CoveredTargets.areCovered(xmlBodyTargets),
"Without enableXmlBodyGeneSupport, EvoMaster should NOT cover all XML body targets"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void testDeterminismOfLog(boolean enableConstraintHandling){
OpenAPI schema = (new OpenAPIParser()).readLocation("swagger-ahm/ahm.json", null, null).getOpenAPI();
isDeterminismConsumer( new ArrayList<>(), (args) -> RestActionBuilderV3.INSTANCE
.getModelsFromSwagger(schema, new LinkedHashMap<>(),
new RestActionBuilderV3.Options(false,enableConstraintHandling,false,0.0,0.0,true,false,false)));
new RestActionBuilderV3.Options(false,enableConstraintHandling,false,0.0,0.0,true,false,false,true,true)));
}


Expand Down
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
Loading
Loading