From 55abe37d393182c91b45d8430fd7e09d91570d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 15 Jun 2026 21:05:08 -0300 Subject: [PATCH 01/12] JsonPatch to dto --- .../evomaster/core/output/dto/DtoWriter.kt | 40 +++++++- .../evomaster/core/output/dto/GeneToDto.kt | 99 +++++++++++++++++++ .../output/service/HttpWsTestCaseWriter.kt | 8 +- .../core/output/dto/DtoWriterJsonPatchTest.kt | 69 +++++++++++++ .../core/output/dto/GeneToDtoJsonPatchTest.kt | 88 +++++++++++++++++ 5 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt create mode 100644 core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index 7a0351a2eb..3f527d6178 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -23,6 +23,7 @@ import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.numeric.LongGene import org.evomaster.core.search.gene.placeholder.CycleObjectGene import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene import org.evomaster.core.search.gene.regex.RegexGene import org.evomaster.core.search.gene.string.Base64StringGene import org.evomaster.core.search.gene.string.StringGene @@ -119,11 +120,7 @@ class DtoWriter( gene is ObjectGene -> calculateDtoFromObject(gene, actionName) gene is ArrayGene<*> -> calculateDtoFromArray(gene, actionName) gene is FixedMapGene<*, *> -> calculateDtoFromFixedMapGene(gene, actionName) - // TODO: a JsonPatchDocumentGene is currently skipped from DTO collection. Once we decide - // how a JSON Patch document should be rendered when a test case is written (it is not a - // regular object/array DTO but an RFC 6902 array of operations), this should build and - // emit the corresponding DTO instead of returning. - gene is JsonPatchDocumentGene -> return + gene is JsonPatchDocumentGene -> calculateDtoFromJsonPatch(gene) isPrimitiveGene(gene) -> return else -> { throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $actionName") @@ -131,6 +128,39 @@ class DtoWriter( } } + /** + * Collects the DTO needed to render a JSON Patch document (RFC 6902) as a typed payload. + * + * A single shared [GeneToDto.JSON_PATCH_OPERATION_DTO] class is used for all patch operations, + * holding every field used across the operation types: "op" and "path" (always present), + * "from" (move/copy) and "value" (add/replace/test). The "value" field is typed as the generic + * object type since a JSON Patch value can be any JSON value; fields not used by a given + * operation are left null and skipped on serialization (see @JsonInclude(NON_NULL)). + * + * When an operation carries an object or array value, the corresponding nested DTOs are also + * collected so the value can be rendered as a proper object instead of stringified JSON. + */ + private fun calculateDtoFromJsonPatch(gene: JsonPatchDocumentGene) { + val dtoName = GeneToDto.JSON_PATCH_OPERATION_DTO + val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(it) } + dtoClass.addField(GeneToDto.FIELD_OP, DtoField(GeneToDto.FIELD_OP, "String")) + dtoClass.addField(GeneToDto.FIELD_PATH, DtoField(GeneToDto.FIELD_PATH, "String")) + dtoClass.addField(GeneToDto.FIELD_FROM, DtoField(GeneToDto.FIELD_FROM, "String")) + dtoClass.addField(GeneToDto.FIELD_VALUE, DtoField(GeneToDto.FIELD_VALUE, anyType())) + dtoCollector[dtoName] = dtoClass + + gene.operations.filterIsInstance().forEach { operation -> + when (val valueGene = operation.pathValueChoice.activeGene().second.getLeafGene()) { + is ObjectGene -> calculateDtoFromObject(valueGene, GeneToDto.FIELD_VALUE) + is ArrayGene<*> -> calculateDtoFromArray(valueGene, GeneToDto.FIELD_VALUE) + } + } + } + + private fun anyType(): String { + return if (outputFormat.isJava()) "Object" else "Any" + } + private fun calculateDtoFromFixedMapGene(gene: FixedMapGene<*, *>, actionName: String) { val dtoName = TestWriterUtils.safeVariableName(actionName) val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(dtoName) } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index bcd2c2299a..22d6fb868b 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -12,6 +12,11 @@ import org.evomaster.core.search.gene.collection.PairGene import org.evomaster.core.search.gene.datetime.DateGene import org.evomaster.core.search.gene.datetime.DateTimeGene import org.evomaster.core.search.gene.datetime.TimeGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchOperationGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathOnlyGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene import org.evomaster.core.search.gene.numeric.DoubleGene import org.evomaster.core.search.gene.numeric.FloatGene import org.evomaster.core.search.gene.numeric.IntegerGene @@ -36,6 +41,19 @@ class GeneToDto( val outputFormat: OutputFormat ) { + companion object { + /** + * Shared DTO class name used to represent a single JSON Patch operation (RFC 6902). + * A single class is enough since all operations share the same set of possible fields; + * fields not used by a given operation are simply left null and skipped on serialization. + */ + const val JSON_PATCH_OPERATION_DTO = "JsonPatchOperation" + const val FIELD_OP = "op" + const val FIELD_PATH = "path" + const val FIELD_FROM = "from" + const val FIELD_VALUE = "value" + } + private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java) private var dtoOutput: DtoOutput = if (outputFormat.isJava()) { @@ -67,6 +85,7 @@ class GeneToDto( } is ChoiceGene<*> -> TestWriterUtils.safeVariableName(fallback) is FixedMapGene<*,*> -> TestWriterUtils.safeVariableName(fallback) + is JsonPatchDocumentGene -> JSON_PATCH_OPERATION_DTO else -> throw IllegalStateException("Gene $gene is not supported for DTO payloads for action: $fallback") } } @@ -85,10 +104,90 @@ class GeneToDto( is ArrayGene<*> -> getArrayDtoCall(gene, dtoName, counters, null, capitalize) is ChoiceGene<*> -> getDtoCall(gene.activeGene(), dtoName, counters, capitalize) is FixedMapGene<*,*> -> getFixedMapGeneDtoCall(gene, dtoName, counters) + is JsonPatchDocumentGene -> getJsonPatchDtoCall(gene, counters) else -> throw RuntimeException("BUG: Gene $gene (with type ${this::class.java.simpleName}) should not be creating DTOs") } } + /** + * A JSON Patch document (RFC 6902) is rendered as a list of [JSON_PATCH_OPERATION_DTO] objects, + * one per active operation in the document. This mirrors the JSON array structure of the payload + * while keeping the generated test readable and type-safe. + */ + private fun getJsonPatchDtoCall(gene: JsonPatchDocumentGene, counters: MutableList): DtoCall { + val listVarName = "list_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" + val result = mutableListOf() + result.add(dtoOutput.getNewListStatement(JSON_PATCH_OPERATION_DTO, listVarName)) + + var operationCounter = 1 + gene.operations.forEach { operation -> + val childCounter = mutableListOf().apply { + addAll(counters) + add(operationCounter++) + } + val operationCall = getJsonPatchOperationCall(operation, childCounter) + result.addAll(operationCall.objectCalls) + result.add(dtoOutput.getAddElementToListStatement(listVarName, operationCall.varName)) + } + + return DtoCall(listVarName, result) + } + + private fun getJsonPatchOperationCall(operation: JsonPatchOperationGene, counters: MutableList): DtoCall { + val varName = "dto_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" + val result = mutableListOf() + result.add(dtoOutput.getNewObjectStatement(JSON_PATCH_OPERATION_DTO, varName)) + result.add(dtoOutput.getSetterStatement(varName, FIELD_OP, "\"${operation.operationName}\"")) + + when (operation) { + is JsonPatchPathOnlyGene -> { + result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(operation.pathGene))) + } + is JsonPatchFromPathGene -> { + result.add(dtoOutput.getSetterStatement(varName, FIELD_FROM, renderLeafValue(operation.fromGene))) + result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(operation.pathGene))) + } + is JsonPatchPathValueGene -> { + val pair = operation.pathValueChoice.activeGene() + result.add(dtoOutput.getSetterStatement(varName, FIELD_PATH, renderLeafValue(pair.first))) + setJsonPatchValue(varName, pair.second.getLeafGene(), counters, result) + } + } + + return DtoCall(varName, result) + } + + /** + * Sets the "value" field of a JSON Patch operation. Primitive values are inlined as literals, + * while object and array values reuse the regular DTO/list generation so nested structures + * are rendered as proper objects rather than stringified JSON. + */ + private fun setJsonPatchValue( + varName: String, + valueGene: Gene, + counters: MutableList, + result: MutableList + ) { + when (valueGene) { + is ObjectGene -> { + val childCall = getDtoCall(valueGene, getDtoName(valueGene, FIELD_VALUE, true), counters, true) + result.addAll(childCall.objectCalls) + result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, childCall.varName)) + } + is ArrayGene<*> -> { + val childCall = getArrayDtoCall(valueGene, getDtoName(valueGene, FIELD_VALUE, true), counters, FIELD_VALUE, true) + result.addAll(childCall.objectCalls) + result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, childCall.varName)) + } + else -> result.add(dtoOutput.getSetterStatement(varName, FIELD_VALUE, renderLeafValue(valueGene))) + } + } + + private fun renderLeafValue(gene: Gene): String { + val leafGene = gene.getLeafGene() + return "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(leafGene)}" + } + private fun getObjectDtoCall(gene: ObjectGene, dtoName: String, counters: MutableList): DtoCall { val dtoVarName = "dto_${dtoName}_${counters.joinToString("_")}" diff --git a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt index 44e268d9b4..e502bf255b 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/service/HttpWsTestCaseWriter.kt @@ -128,11 +128,13 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { val bodyParam = call.parameters.find { p -> p is BodyParam } as BodyParam? if (bodyParam != null && bodyParam.isJson() && payloadIsValidJson(bodyParam)) { val primaryGene = bodyParam.primaryGene() - if (primaryGene.getWrappedGene(JsonPatchDocumentGene::class.java) != null) { - return "" + val actionName = call.getName() + val jsonPatchGene = primaryGene.getWrappedGene(JsonPatchDocumentGene::class.java) + if (jsonPatchGene != null) { + // A JSON Patch document is rendered as a List DTO (RFC 6902). + return generateDtoCall(jsonPatchGene, actionName, lines).varName } val choiceGene = primaryGene.getWrappedGene(ChoiceGene::class.java) - val actionName = call.getName() if (choiceGene != null) { // We only generate DTOs for ChoiceGene objects that contain either an ObjectGene or ArrayGene in their // genes. This check is necessary since when using `example` and `default` entries, diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt new file mode 100644 index 0000000000..83433a6c33 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt @@ -0,0 +1,69 @@ +package org.evomaster.core.output.dto + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.output.Termination +import org.evomaster.core.output.naming.RestActionTestCaseUtils.getEvaluatedIndividualWith +import org.evomaster.core.output.naming.RestActionTestCaseUtils.getRestCallAction +import org.evomaster.core.problem.api.param.Param +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.core.problem.rest.param.BodyParam +import org.evomaster.core.search.Solution +import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.collection.EnumGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import java.nio.file.Paths +import java.util.Collections.singletonList + +/** + * Verifies that [DtoWriter] collects the shared JsonPatchOperation DTO (RFC 6902) with every + * field used across operation types, so a JSON Patch payload can be written as a DTO. + */ +class DtoWriterJsonPatchTest { + + private val outputTestSuitePath = Paths.get("./target/dto-writer-json-patch-test") + private val testPackage = "test.package" + + private fun jsonPatchBodyParam(): Param { + val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) + val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } + return BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) + } + + private fun jsonPatchSolution(): Solution<*> { + val action = getRestCallAction("/items/{id}", HttpVerb.PATCH, mutableListOf(jsonPatchBodyParam())) + val eIndividual = getEvaluatedIndividualWith(action) + return Solution(singletonList(eIndividual), "", "", Termination.NONE, emptyList(), emptyList()) + } + + @Test + fun collectsJsonPatchOperationDtoWithAllFields() { + val dtoWriter = DtoWriter(OutputFormat.KOTLIN_JUNIT_5) + dtoWriter.write(outputTestSuitePath, testPackage, jsonPatchSolution()) + + val dtos = dtoWriter.getCollectedDtos() + val operationDto = dtos[GeneToDto.JSON_PATCH_OPERATION_DTO] + assertNotNull(operationDto, "Expected a ${GeneToDto.JSON_PATCH_OPERATION_DTO} DTO to be collected") + + val fields = operationDto!!.fieldsMap + assertEquals(DtoField(GeneToDto.FIELD_OP, "String"), fields[GeneToDto.FIELD_OP]) + assertEquals(DtoField(GeneToDto.FIELD_PATH, "String"), fields[GeneToDto.FIELD_PATH]) + assertEquals(DtoField(GeneToDto.FIELD_FROM, "String"), fields[GeneToDto.FIELD_FROM]) + // value is the generic object type since a JSON Patch value can be any JSON value + assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Any"), fields[GeneToDto.FIELD_VALUE]) + } + + @Test + fun valueFieldIsObjectForJavaOutput() { + val dtoWriter = DtoWriter(OutputFormat.JAVA_JUNIT_5) + dtoWriter.write(outputTestSuitePath, testPackage, jsonPatchSolution()) + + val operationDto = dtoWriter.getCollectedDtos()[GeneToDto.JSON_PATCH_OPERATION_DTO] + assertNotNull(operationDto) + assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Object"), operationDto!!.fieldsMap[GeneToDto.FIELD_VALUE]) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt new file mode 100644 index 0000000000..c09c75612e --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/GeneToDtoJsonPatchTest.kt @@ -0,0 +1,88 @@ +package org.evomaster.core.output.dto + +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.service.Randomness +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** + * Verifies that a [JsonPatchDocumentGene] (RFC 6902) is rendered by [GeneToDto] as a + * List instead of stringified JSON. + */ +class GeneToDtoJsonPatchTest { + + private val dtoName = GeneToDto.JSON_PATCH_OPERATION_DTO + + private fun patchDoc(seed: Long = 42L): JsonPatchDocumentGene { + val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) + val doc = JsonPatchDocumentGene("patch", schema) + doc.doInitialize(Randomness().apply { updateSeed(seed) }) + return doc + } + + private fun setOpValue(line: String): String = + Regex("""\.setOp\("(.*?)"\)""").find(line)!!.groupValues[1] + + @Test + fun rendersAsListOfJsonPatchOperationDtosKotlin() { + val doc = patchDoc() + val dtoCall = GeneToDto(OutputFormat.KOTLIN_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false) + val calls = dtoCall.objectCalls + + assertEquals("list_${dtoName}_0", dtoCall.varName) + assertEquals("val list_${dtoName}_0 = mutableListOf<$dtoName>()", calls.first()) + + val ops = doc.operations + // One object instantiation and one add-to-list per operation + assertEquals(ops.size, calls.count { it.contains("= $dtoName()") }) + assertEquals(ops.size, calls.count { it.startsWith("list_${dtoName}_0.add(dto_${dtoName}_") }) + + // Every operation sets its "op", and the multiset of op names matches the document + val opNamesInCode = calls.filter { it.contains(".setOp(") }.map { setOpValue(it) } + assertEquals(ops.map { it.operationName }.sorted(), opNamesInCode.sorted()) + } + + @Test + fun setsOnlyTheFieldsRelevantToEachOperation() { + val doc = patchDoc() + val calls = GeneToDto(OutputFormat.KOTLIN_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false).objectCalls + + val ops = doc.operations + // "from" only for move/copy, "value" only for add/replace/test + assertEquals(ops.count { it is JsonPatchFromPathGene }, calls.count { it.contains(".setFrom(") }) + assertEquals(ops.count { it is JsonPatchPathValueGene }, calls.count { it.contains(".setValue(") }) + // "op" and "path" are present in every operation + assertEquals(ops.size, calls.count { it.contains(".setOp(") }) + assertEquals(ops.size, calls.count { it.contains(".setPath(") }) + } + + @Test + fun primitiveValuesAreInlinedAsLiterals() { + // Force every operation type to render; assert string values are quoted and int values are bare + val doc = patchDoc(seed = 7L) + val calls = GeneToDto(OutputFormat.KOTLIN_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false).objectCalls + + calls.filter { it.contains(".setValue(") }.forEach { line -> + val value = Regex("""\.setValue\((.*)\)""").find(line)!!.groupValues[1] + val isQuotedString = value.startsWith("\"") && value.endsWith("\"") + val isInt = value.toIntOrNull() != null + assertTrue(isQuotedString || isInt, "Unexpected non-literal value rendering: $line") + } + } + + @Test + fun rendersWithJavaSyntax() { + val doc = patchDoc() + val calls = GeneToDto(OutputFormat.JAVA_JUNIT_5).getDtoCall(doc, dtoName, mutableListOf(0), false).objectCalls + + assertEquals("List<$dtoName> list_${dtoName}_0 = new ArrayList<$dtoName>();", calls.first()) + assertTrue(calls.any { it.contains("$dtoName dto_${dtoName}_0_1 = new $dtoName();") }) + } +} From 1af12342ebe421402d505fb8d310eb5eb4296524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 15 Jun 2026 22:39:44 -0300 Subject: [PATCH 02/12] Add external api extract and test --- .../bb/httppatch/HttpPatchApplication.kt | 332 ++++++++++++++++++ .../bb/httppatch/HttpPatchController.kt | 15 + .../spring/rest/bb/httppatch/HttpPatchTest.kt | 71 ++++ 3 files changed, 418 insertions(+) create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt new file mode 100644 index 0000000000..a54855aa68 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt @@ -0,0 +1,332 @@ +package com.foo.rest.examples.bb.httppatch + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.ObjectNode +import org.evomaster.e2etests.utils.CoveredTargets +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.* +import java.net.URI + +/** + * Black-box SUT for the JSON Patch (RFC 6902) e2e test. + * + * This is a vendored, self-contained slice of the open-source project + * https://github.com/cassiomolin/http-patch-spring (the same API whose OpenAPI schema is + * already committed in core/src/test/resources/swagger/sut/http-patch-spring.json). + * + * Differences from the original, on purpose: + * - in-memory store instead of a real database, so state can be reset between runs; + * - JSON Patch applied manually with Jackson (no extra dependency); + * - the patch is applied TRANSACTIONALLY: it is computed on a copy, the result is validated, + * and only persisted if the resulting Contact is still valid. Any malformed/inapplicable + * patch, or a patch that would leave the resource invalid, is rejected with a 4xx and the + * stored object is left untouched. This is what guarantees the API "does not break" while + * EvoMaster fuzzes it with potentially destructive patches. + */ +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/contacts") +open class HttpPatchApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(HttpPatchApplication::class.java, *args) + } + + private val store = LinkedHashMap() + private var counter = 0L + + private fun initialContacts(): List = listOf( + Contact( + name = "Ada Lovelace", + birthday = "1815-12-10", + favorite = true, + notes = "mathematician", + groups = mutableListOf("friends", "science"), + work = Work("Analyst", "Analytical Engine Co"), + phones = mutableListOf(Phone("555-0001", "home")), + emails = mutableListOf(Email("ada@example.com", "work")) + ), + // Note: 'work' is intentionally null here, so patches targeting /work/title + // exercise the "navigate into a missing parent" path (handled as 409, not 500). + Contact( + name = "Alan Turing", + birthday = "1912-06-23", + favorite = false, + notes = null, + groups = mutableListOf("science"), + work = null, + phones = mutableListOf(), + emails = mutableListOf() + ) + ) + + /** + * Re-seeds the in-memory store to its initial state. Invoked by the EmbeddedSutController + * (HttpPatchController.resetStateOfSUT) so that destructive patches from one test do not + * leak into the next one. + */ + @JvmStatic + fun reset() { + synchronized(store) { + store.clear() + counter = 0 + for (c in initialContacts()) { + val id = ++counter + store[id] = c.copy(id = id) + } + } + } + } + + init { + reset() + } + + private val mapper = ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + /** + * All non-2xx responses use a small JSON body. Returning a plain string under + * Content-Type application/json would be served as invalid JSON, which (a) is itself a + * response/schema-mismatch fault and (b) makes the generated black-box tests crash on + * JSON.parse (e.g. JS/Jest with superagent). A JSON object keeps every response valid JSON. + */ + private fun problem(status: Int, message: String): ResponseEntity = + ResponseEntity.status(status).body(mapOf("message" to message)) + + @GetMapping + fun findContacts(): ResponseEntity> = + ResponseEntity.ok(synchronized(store) { store.values.toList() }) + + @PostMapping(consumes = ["application/json"], produces = ["application/json"]) + fun createContact(@RequestBody contact: Contact): ResponseEntity { + if (contact.name.isNullOrBlank()) + return problem(422, "name is required") + val created = synchronized(store) { + val id = ++counter + contact.copy(id = id).also { store[id] = it } + } + // 201 + Location header so EvoMaster can bind subsequent calls (incl. its cleanup DELETE) + // to the created resource. Without this, cleanup hits a non-existent id and EvoMaster's + // BlackBoxRestFitness.handleCleanUpActions fails with "Wrong status: 404". + return ResponseEntity.created(URI.create("/contacts/${created.id}")).body(created) + } + + @GetMapping("/{id}", produces = ["application/json"]) + fun findContact(@PathVariable id: Long): ResponseEntity { + val contact = synchronized(store) { store[id] } ?: return ResponseEntity.notFound().build() + return ResponseEntity.ok(contact) + } + + @PutMapping("/{id}", consumes = ["application/json"]) + fun updateContact(@PathVariable id: Long, @RequestBody contact: Contact): ResponseEntity { + synchronized(store) { store[id] } ?: return ResponseEntity.notFound().build() + if (contact.name.isNullOrBlank()) + return problem(422, "name is required") + synchronized(store) { store[id] = contact.copy(id = id) } + return ResponseEntity.ok().build() + } + + // Idempotent on purpose: returns 204 whether or not the id existed. EvoMaster's black-box + // cleanup phase (BlackBoxRestFitness.handleCleanUpActions) issues a DELETE bound to each + // created resource and asserts a 2xx/403 status. Returning 404 for a missing id there would + // crash the whole search with "Wrong status: 404", so we keep DELETE idempotent (also valid + // per RFC 7231: a 2xx with no body is acceptable for an already-absent resource). + @DeleteMapping("/{id}") + fun deleteContact(@PathVariable id: Long): ResponseEntity { + synchronized(store) { store.remove(id) } + return ResponseEntity.noContent().build() + } + + @PatchMapping("/{id}", consumes = ["application/json-patch+json"], produces = ["application/json"]) + fun patchContact(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + val current = synchronized(store) { store[id] } ?: return ResponseEntity.notFound().build() + + val patchNode = try { + mapper.readTree(body) + } catch (e: Exception) { + return problem(400, "Malformed JSON document") + } + + // Apply on a tree copy of the resource. Any structural problem -> 4xx, never 500. + val patched: JsonNode = try { + ContactJsonPatch.apply(mapper.valueToTree(current), patchNode) + } catch (e: PatchException) { + if (e.status == 409) CoveredTargets.cover("JSONPATCH_CONFLICT") + return problem(e.status, e.message ?: "Patch operation failed") + } catch (e: Exception) { + return problem(400, "Could not apply patch document") + } + + // Validate the result before persisting it. + val updated = try { + mapper.treeToValue(patched, Contact::class.java) + } catch (e: Exception) { + CoveredTargets.cover("JSONPATCH_INVALID_RESOURCE") + return problem(422, "Patched resource is not a valid Contact") + } + if (updated.name.isNullOrBlank()) { + CoveredTargets.cover("JSONPATCH_INVALID_RESOURCE") + return problem(422, "name is required") + } + + synchronized(store) { store[id] = updated.copy(id = id) } + CoveredTargets.cover("JSONPATCH_APPLIED_OK") + return ResponseEntity.ok(updated.copy(id = id)) + } +} + +/** + * Minimal manual JSON Patch (RFC 6902) applier over a Jackson tree. + * Mutates a deep copy of the resource in place. Throws [PatchException] (a 4xx) for any + * malformed or inapplicable operation, so the controller never returns a 500 for bad input. + */ +object ContactJsonPatch { + + fun apply(resource: JsonNode, patch: JsonNode): JsonNode { + if (patch !is ArrayNode) throw PatchException(400, "Patch document must be a JSON array") + + val root: JsonNode = resource.deepCopy() + for (op in patch) { + if (op !is ObjectNode) throw PatchException(400, "Each operation must be a JSON object") + when (val opName = op.path("op").asText(null) + ?: throw PatchException(400, "Operation is missing 'op'")) { + "add" -> { + CoveredTargets.cover("JSONPATCH_OP_ADD") + add(root, pointer(op, "path"), requireValue(op)) + } + "remove" -> { + CoveredTargets.cover("JSONPATCH_OP_REMOVE") + remove(root, pointer(op, "path")) + } + "replace" -> { + CoveredTargets.cover("JSONPATCH_OP_REPLACE") + replace(root, pointer(op, "path"), requireValue(op)) + } + "move" -> { + CoveredTargets.cover("JSONPATCH_OP_MOVE") + val from = pointer(op, "from") + val value = read(root, from) + remove(root, from) + add(root, pointer(op, "path"), value) + } + "copy" -> { + CoveredTargets.cover("JSONPATCH_OP_COPY") + val value = read(root, pointer(op, "from")).deepCopy() + add(root, pointer(op, "path"), value) + } + "test" -> { + CoveredTargets.cover("JSONPATCH_OP_TEST") + if (read(root, pointer(op, "path")) != requireValue(op)) + throw PatchException(409, "Test operation failed") + } + else -> throw PatchException(400, "Unsupported operation: $opName") + } + } + return root + } + + private fun pointer(op: ObjectNode, field: String): String = + op.path(field).asText(null) ?: throw PatchException(400, "Operation is missing '$field'") + + private fun requireValue(op: ObjectNode): JsonNode = + if (op.has("value")) op.get("value") else throw PatchException(400, "Operation is missing 'value'") + + private fun tokens(p: String): List { + if (p.isEmpty()) return emptyList() + if (!p.startsWith("/")) throw PatchException(400, "Invalid JSON Pointer: $p") + return p.substring(1).split("/").map { it.replace("~1", "/").replace("~0", "~") } + } + + private fun child(node: JsonNode, token: String): JsonNode? = when (node) { + is ObjectNode -> if (node.has(token)) node.get(token) else null + is ArrayNode -> token.toIntOrNull()?.let { if (it in 0 until node.size()) node.get(it) else null } + else -> null + } + + private fun parentAndKey(root: JsonNode, p: String): Pair { + val tk = tokens(p) + if (tk.isEmpty()) throw PatchException(400, "Root path '' is not supported") + var node = root + for (i in 0 until tk.size - 1) { + node = child(node, tk[i]) ?: throw PatchException(409, "Path not found: $p") + } + return node to tk.last() + } + + private fun read(root: JsonNode, p: String): JsonNode { + var node = root + for (t in tokens(p)) node = child(node, t) ?: throw PatchException(409, "Path not found: $p") + return node + } + + private fun add(root: JsonNode, p: String, value: JsonNode) { + val (parent, key) = parentAndKey(root, p) + when (parent) { + is ObjectNode -> parent.replace(key, value) + is ArrayNode -> { + if (key == "-") parent.add(value) + else { + val idx = key.toIntOrNull() ?: throw PatchException(400, "Invalid array index: $key") + if (idx < 0 || idx > parent.size()) throw PatchException(409, "Array index out of bounds: $idx") + parent.insert(idx, value) + } + } + else -> throw PatchException(409, "Cannot add into a non-container at $p") + } + } + + private fun remove(root: JsonNode, p: String) { + val (parent, key) = parentAndKey(root, p) + when (parent) { + is ObjectNode -> if (parent.has(key)) parent.remove(key) else throw PatchException(409, "Path not found: $p") + is ArrayNode -> { + val idx = key.toIntOrNull() ?: throw PatchException(400, "Invalid array index: $key") + if (idx < 0 || idx >= parent.size()) throw PatchException(409, "Array index out of bounds: $idx") + parent.remove(idx) + } + else -> throw PatchException(409, "Cannot remove from a non-container at $p") + } + } + + private fun replace(root: JsonNode, p: String, value: JsonNode) { + val (parent, key) = parentAndKey(root, p) + when (parent) { + is ObjectNode -> if (parent.has(key)) parent.replace(key, value) else throw PatchException(409, "Path not found: $p") + is ArrayNode -> { + val idx = key.toIntOrNull() ?: throw PatchException(400, "Invalid array index: $key") + if (idx < 0 || idx >= parent.size()) throw PatchException(409, "Array index out of bounds: $idx") + parent.set(idx, value) + } + else -> throw PatchException(409, "Cannot replace in a non-container at $p") + } + } +} + +class PatchException(val status: Int, message: String) : RuntimeException(message) + +data class Contact( + var id: Long? = null, + var name: String? = null, + var birthday: String? = null, + var favorite: Boolean? = null, + var notes: String? = null, + var groups: MutableList? = null, + var work: Work? = null, + var phones: MutableList? = null, + var emails: MutableList? = null +) + +data class Work(var title: String? = null, var company: String? = null) + +data class Phone(var phone: String? = null, var type: String? = null) + +data class Email(var email: String? = null, var type: String? = null) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt new file mode 100644 index 0000000000..a4e309f9d4 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt @@ -0,0 +1,15 @@ +package com.foo.rest.examples.bb.httppatch + +import com.foo.rest.examples.bb.SpringController + +class HttpPatchController : SpringController(HttpPatchApplication::class.java) { + + /** + * Re-seed the in-memory store before each EvoMaster action sequence, so that destructive + * JSON Patch documents (e.g. removing a required field) cannot corrupt the state used by + * subsequent calls. This is what keeps the e2e test deterministic and the SUT robust. + */ + override fun resetStateOfSUT() { + HttpPatchApplication.reset() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt new file mode 100644 index 0000000000..26fc86e67c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt @@ -0,0 +1,71 @@ +package org.evomaster.e2etests.spring.rest.bb.httppatch + +import com.foo.rest.examples.bb.httppatch.HttpPatchController +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.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +/** + * End-to-end test for JSON Patch (RFC 6902) against a realistic, vendored API + * (a slice of https://github.com/cassiomolin/http-patch-spring, exposing PATCH /contacts/{id} + * with media type application/json-patch+json). + * + * Unlike BBJsonPatchTest -- which uses a synthetic SUT that only checks the *presence* of each + * operation and never mutates state -- here the patch is actually applied to a real Contact + * resource. This exercises: + * - resolving the resource schema from the sibling GET /contacts/{id} response, so generated + * paths/values reference real fields (/name, /notes, /favorite, /work/title, ...); + * - that EvoMaster covers all six operations (add/remove/replace/move/copy/test); + * - that a patch which would leave the resource invalid (e.g. removing the required name) is + * detected and rejected (422) without corrupting state -> JSONPATCH_INVALID_RESOURCE. + */ +class HttpPatchTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + val config = EMConfig() + initClass(HttpPatchController(), config) + } + } + + @ParameterizedTest + @EnumSource + fun testBlackBoxOutput(outputFormat: OutputFormat) { + executeAndEvaluateBBTest( + outputFormat, + "HttpPatchEM", + 1000, + 3, + listOf( + "JSONPATCH_OP_ADD", + "JSONPATCH_OP_REMOVE", + "JSONPATCH_OP_REPLACE", + "JSONPATCH_OP_MOVE", + "JSONPATCH_OP_COPY", + "JSONPATCH_OP_TEST", + "JSONPATCH_APPLIED_OK", + "JSONPATCH_INVALID_RESOURCE", + "JSONPATCH_CONFLICT" + ) + ) { args: MutableList -> + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + // A patch that applies cleanly and persists. + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/contacts/{id}", null) + // A patch that would leave the resource invalid -> rejected, state untouched. + assertHasAtLeastOne(solution, HttpVerb.PATCH, 422, "/contacts/{id}", null) + // A structurally inapplicable patch (bad path / failed test op) -> conflict. + assertHasAtLeastOne(solution, HttpVerb.PATCH, 409, "/contacts/{id}", null) + } + } +} From 2f1f033a1fb7cca9ceaf32b076b2ed18a415f3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Fri, 19 Jun 2026 23:28:17 -0300 Subject: [PATCH 03/12] externalApi is not for this pr --- .../bb/httppatch/HttpPatchApplication.kt | 332 ------------------ .../bb/httppatch/HttpPatchController.kt | 15 - .../spring/rest/bb/httppatch/HttpPatchTest.kt | 71 ---- 3 files changed, 418 deletions(-) delete mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt delete mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt delete mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt deleted file mode 100644 index a54855aa68..0000000000 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchApplication.kt +++ /dev/null @@ -1,332 +0,0 @@ -package com.foo.rest.examples.bb.httppatch - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode -import org.evomaster.e2etests.utils.CoveredTargets -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.* -import java.net.URI - -/** - * Black-box SUT for the JSON Patch (RFC 6902) e2e test. - * - * This is a vendored, self-contained slice of the open-source project - * https://github.com/cassiomolin/http-patch-spring (the same API whose OpenAPI schema is - * already committed in core/src/test/resources/swagger/sut/http-patch-spring.json). - * - * Differences from the original, on purpose: - * - in-memory store instead of a real database, so state can be reset between runs; - * - JSON Patch applied manually with Jackson (no extra dependency); - * - the patch is applied TRANSACTIONALLY: it is computed on a copy, the result is validated, - * and only persisted if the resulting Contact is still valid. Any malformed/inapplicable - * patch, or a patch that would leave the resource invalid, is rejected with a 4xx and the - * stored object is left untouched. This is what guarantees the API "does not break" while - * EvoMaster fuzzes it with potentially destructive patches. - */ -@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) -@RestController -@RequestMapping("/contacts") -open class HttpPatchApplication { - - companion object { - @JvmStatic - fun main(args: Array) { - SpringApplication.run(HttpPatchApplication::class.java, *args) - } - - private val store = LinkedHashMap() - private var counter = 0L - - private fun initialContacts(): List = listOf( - Contact( - name = "Ada Lovelace", - birthday = "1815-12-10", - favorite = true, - notes = "mathematician", - groups = mutableListOf("friends", "science"), - work = Work("Analyst", "Analytical Engine Co"), - phones = mutableListOf(Phone("555-0001", "home")), - emails = mutableListOf(Email("ada@example.com", "work")) - ), - // Note: 'work' is intentionally null here, so patches targeting /work/title - // exercise the "navigate into a missing parent" path (handled as 409, not 500). - Contact( - name = "Alan Turing", - birthday = "1912-06-23", - favorite = false, - notes = null, - groups = mutableListOf("science"), - work = null, - phones = mutableListOf(), - emails = mutableListOf() - ) - ) - - /** - * Re-seeds the in-memory store to its initial state. Invoked by the EmbeddedSutController - * (HttpPatchController.resetStateOfSUT) so that destructive patches from one test do not - * leak into the next one. - */ - @JvmStatic - fun reset() { - synchronized(store) { - store.clear() - counter = 0 - for (c in initialContacts()) { - val id = ++counter - store[id] = c.copy(id = id) - } - } - } - } - - init { - reset() - } - - private val mapper = ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - - /** - * All non-2xx responses use a small JSON body. Returning a plain string under - * Content-Type application/json would be served as invalid JSON, which (a) is itself a - * response/schema-mismatch fault and (b) makes the generated black-box tests crash on - * JSON.parse (e.g. JS/Jest with superagent). A JSON object keeps every response valid JSON. - */ - private fun problem(status: Int, message: String): ResponseEntity = - ResponseEntity.status(status).body(mapOf("message" to message)) - - @GetMapping - fun findContacts(): ResponseEntity> = - ResponseEntity.ok(synchronized(store) { store.values.toList() }) - - @PostMapping(consumes = ["application/json"], produces = ["application/json"]) - fun createContact(@RequestBody contact: Contact): ResponseEntity { - if (contact.name.isNullOrBlank()) - return problem(422, "name is required") - val created = synchronized(store) { - val id = ++counter - contact.copy(id = id).also { store[id] = it } - } - // 201 + Location header so EvoMaster can bind subsequent calls (incl. its cleanup DELETE) - // to the created resource. Without this, cleanup hits a non-existent id and EvoMaster's - // BlackBoxRestFitness.handleCleanUpActions fails with "Wrong status: 404". - return ResponseEntity.created(URI.create("/contacts/${created.id}")).body(created) - } - - @GetMapping("/{id}", produces = ["application/json"]) - fun findContact(@PathVariable id: Long): ResponseEntity { - val contact = synchronized(store) { store[id] } ?: return ResponseEntity.notFound().build() - return ResponseEntity.ok(contact) - } - - @PutMapping("/{id}", consumes = ["application/json"]) - fun updateContact(@PathVariable id: Long, @RequestBody contact: Contact): ResponseEntity { - synchronized(store) { store[id] } ?: return ResponseEntity.notFound().build() - if (contact.name.isNullOrBlank()) - return problem(422, "name is required") - synchronized(store) { store[id] = contact.copy(id = id) } - return ResponseEntity.ok().build() - } - - // Idempotent on purpose: returns 204 whether or not the id existed. EvoMaster's black-box - // cleanup phase (BlackBoxRestFitness.handleCleanUpActions) issues a DELETE bound to each - // created resource and asserts a 2xx/403 status. Returning 404 for a missing id there would - // crash the whole search with "Wrong status: 404", so we keep DELETE idempotent (also valid - // per RFC 7231: a 2xx with no body is acceptable for an already-absent resource). - @DeleteMapping("/{id}") - fun deleteContact(@PathVariable id: Long): ResponseEntity { - synchronized(store) { store.remove(id) } - return ResponseEntity.noContent().build() - } - - @PatchMapping("/{id}", consumes = ["application/json-patch+json"], produces = ["application/json"]) - fun patchContact(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { - val current = synchronized(store) { store[id] } ?: return ResponseEntity.notFound().build() - - val patchNode = try { - mapper.readTree(body) - } catch (e: Exception) { - return problem(400, "Malformed JSON document") - } - - // Apply on a tree copy of the resource. Any structural problem -> 4xx, never 500. - val patched: JsonNode = try { - ContactJsonPatch.apply(mapper.valueToTree(current), patchNode) - } catch (e: PatchException) { - if (e.status == 409) CoveredTargets.cover("JSONPATCH_CONFLICT") - return problem(e.status, e.message ?: "Patch operation failed") - } catch (e: Exception) { - return problem(400, "Could not apply patch document") - } - - // Validate the result before persisting it. - val updated = try { - mapper.treeToValue(patched, Contact::class.java) - } catch (e: Exception) { - CoveredTargets.cover("JSONPATCH_INVALID_RESOURCE") - return problem(422, "Patched resource is not a valid Contact") - } - if (updated.name.isNullOrBlank()) { - CoveredTargets.cover("JSONPATCH_INVALID_RESOURCE") - return problem(422, "name is required") - } - - synchronized(store) { store[id] = updated.copy(id = id) } - CoveredTargets.cover("JSONPATCH_APPLIED_OK") - return ResponseEntity.ok(updated.copy(id = id)) - } -} - -/** - * Minimal manual JSON Patch (RFC 6902) applier over a Jackson tree. - * Mutates a deep copy of the resource in place. Throws [PatchException] (a 4xx) for any - * malformed or inapplicable operation, so the controller never returns a 500 for bad input. - */ -object ContactJsonPatch { - - fun apply(resource: JsonNode, patch: JsonNode): JsonNode { - if (patch !is ArrayNode) throw PatchException(400, "Patch document must be a JSON array") - - val root: JsonNode = resource.deepCopy() - for (op in patch) { - if (op !is ObjectNode) throw PatchException(400, "Each operation must be a JSON object") - when (val opName = op.path("op").asText(null) - ?: throw PatchException(400, "Operation is missing 'op'")) { - "add" -> { - CoveredTargets.cover("JSONPATCH_OP_ADD") - add(root, pointer(op, "path"), requireValue(op)) - } - "remove" -> { - CoveredTargets.cover("JSONPATCH_OP_REMOVE") - remove(root, pointer(op, "path")) - } - "replace" -> { - CoveredTargets.cover("JSONPATCH_OP_REPLACE") - replace(root, pointer(op, "path"), requireValue(op)) - } - "move" -> { - CoveredTargets.cover("JSONPATCH_OP_MOVE") - val from = pointer(op, "from") - val value = read(root, from) - remove(root, from) - add(root, pointer(op, "path"), value) - } - "copy" -> { - CoveredTargets.cover("JSONPATCH_OP_COPY") - val value = read(root, pointer(op, "from")).deepCopy() - add(root, pointer(op, "path"), value) - } - "test" -> { - CoveredTargets.cover("JSONPATCH_OP_TEST") - if (read(root, pointer(op, "path")) != requireValue(op)) - throw PatchException(409, "Test operation failed") - } - else -> throw PatchException(400, "Unsupported operation: $opName") - } - } - return root - } - - private fun pointer(op: ObjectNode, field: String): String = - op.path(field).asText(null) ?: throw PatchException(400, "Operation is missing '$field'") - - private fun requireValue(op: ObjectNode): JsonNode = - if (op.has("value")) op.get("value") else throw PatchException(400, "Operation is missing 'value'") - - private fun tokens(p: String): List { - if (p.isEmpty()) return emptyList() - if (!p.startsWith("/")) throw PatchException(400, "Invalid JSON Pointer: $p") - return p.substring(1).split("/").map { it.replace("~1", "/").replace("~0", "~") } - } - - private fun child(node: JsonNode, token: String): JsonNode? = when (node) { - is ObjectNode -> if (node.has(token)) node.get(token) else null - is ArrayNode -> token.toIntOrNull()?.let { if (it in 0 until node.size()) node.get(it) else null } - else -> null - } - - private fun parentAndKey(root: JsonNode, p: String): Pair { - val tk = tokens(p) - if (tk.isEmpty()) throw PatchException(400, "Root path '' is not supported") - var node = root - for (i in 0 until tk.size - 1) { - node = child(node, tk[i]) ?: throw PatchException(409, "Path not found: $p") - } - return node to tk.last() - } - - private fun read(root: JsonNode, p: String): JsonNode { - var node = root - for (t in tokens(p)) node = child(node, t) ?: throw PatchException(409, "Path not found: $p") - return node - } - - private fun add(root: JsonNode, p: String, value: JsonNode) { - val (parent, key) = parentAndKey(root, p) - when (parent) { - is ObjectNode -> parent.replace(key, value) - is ArrayNode -> { - if (key == "-") parent.add(value) - else { - val idx = key.toIntOrNull() ?: throw PatchException(400, "Invalid array index: $key") - if (idx < 0 || idx > parent.size()) throw PatchException(409, "Array index out of bounds: $idx") - parent.insert(idx, value) - } - } - else -> throw PatchException(409, "Cannot add into a non-container at $p") - } - } - - private fun remove(root: JsonNode, p: String) { - val (parent, key) = parentAndKey(root, p) - when (parent) { - is ObjectNode -> if (parent.has(key)) parent.remove(key) else throw PatchException(409, "Path not found: $p") - is ArrayNode -> { - val idx = key.toIntOrNull() ?: throw PatchException(400, "Invalid array index: $key") - if (idx < 0 || idx >= parent.size()) throw PatchException(409, "Array index out of bounds: $idx") - parent.remove(idx) - } - else -> throw PatchException(409, "Cannot remove from a non-container at $p") - } - } - - private fun replace(root: JsonNode, p: String, value: JsonNode) { - val (parent, key) = parentAndKey(root, p) - when (parent) { - is ObjectNode -> if (parent.has(key)) parent.replace(key, value) else throw PatchException(409, "Path not found: $p") - is ArrayNode -> { - val idx = key.toIntOrNull() ?: throw PatchException(400, "Invalid array index: $key") - if (idx < 0 || idx >= parent.size()) throw PatchException(409, "Array index out of bounds: $idx") - parent.set(idx, value) - } - else -> throw PatchException(409, "Cannot replace in a non-container at $p") - } - } -} - -class PatchException(val status: Int, message: String) : RuntimeException(message) - -data class Contact( - var id: Long? = null, - var name: String? = null, - var birthday: String? = null, - var favorite: Boolean? = null, - var notes: String? = null, - var groups: MutableList? = null, - var work: Work? = null, - var phones: MutableList? = null, - var emails: MutableList? = null -) - -data class Work(var title: String? = null, var company: String? = null) - -data class Phone(var phone: String? = null, var type: String? = null) - -data class Email(var email: String? = null, var type: String? = null) \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt deleted file mode 100644 index a4e309f9d4..0000000000 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/httppatch/HttpPatchController.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.foo.rest.examples.bb.httppatch - -import com.foo.rest.examples.bb.SpringController - -class HttpPatchController : SpringController(HttpPatchApplication::class.java) { - - /** - * Re-seed the in-memory store before each EvoMaster action sequence, so that destructive - * JSON Patch documents (e.g. removing a required field) cannot corrupt the state used by - * subsequent calls. This is what keeps the e2e test deterministic and the SUT robust. - */ - override fun resetStateOfSUT() { - HttpPatchApplication.reset() - } -} diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt deleted file mode 100644 index 26fc86e67c..0000000000 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/httppatch/HttpPatchTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.evomaster.e2etests.spring.rest.bb.httppatch - -import com.foo.rest.examples.bb.httppatch.HttpPatchController -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.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource - -/** - * End-to-end test for JSON Patch (RFC 6902) against a realistic, vendored API - * (a slice of https://github.com/cassiomolin/http-patch-spring, exposing PATCH /contacts/{id} - * with media type application/json-patch+json). - * - * Unlike BBJsonPatchTest -- which uses a synthetic SUT that only checks the *presence* of each - * operation and never mutates state -- here the patch is actually applied to a real Contact - * resource. This exercises: - * - resolving the resource schema from the sibling GET /contacts/{id} response, so generated - * paths/values reference real fields (/name, /notes, /favorite, /work/title, ...); - * - that EvoMaster covers all six operations (add/remove/replace/move/copy/test); - * - that a patch which would leave the resource invalid (e.g. removing the required name) is - * detected and rejected (422) without corrupting state -> JSONPATCH_INVALID_RESOURCE. - */ -class HttpPatchTest : SpringTestBase() { - - companion object { - @BeforeAll - @JvmStatic - fun init() { - val config = EMConfig() - initClass(HttpPatchController(), config) - } - } - - @ParameterizedTest - @EnumSource - fun testBlackBoxOutput(outputFormat: OutputFormat) { - executeAndEvaluateBBTest( - outputFormat, - "HttpPatchEM", - 1000, - 3, - listOf( - "JSONPATCH_OP_ADD", - "JSONPATCH_OP_REMOVE", - "JSONPATCH_OP_REPLACE", - "JSONPATCH_OP_MOVE", - "JSONPATCH_OP_COPY", - "JSONPATCH_OP_TEST", - "JSONPATCH_APPLIED_OK", - "JSONPATCH_INVALID_RESOURCE", - "JSONPATCH_CONFLICT" - ) - ) { args: MutableList -> - - val solution = initAndRun(args) - - assertTrue(solution.individuals.size >= 1) - - // A patch that applies cleanly and persists. - assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/contacts/{id}", null) - // A patch that would leave the resource invalid -> rejected, state untouched. - assertHasAtLeastOne(solution, HttpVerb.PATCH, 422, "/contacts/{id}", null) - // A structurally inapplicable patch (bad path / failed test op) -> conflict. - assertHasAtLeastOne(solution, HttpVerb.PATCH, 409, "/contacts/{id}", null) - } - } -} From fe828308dad98aa1883ba80911b0c37ee395c8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Sat, 20 Jun 2026 00:33:32 -0300 Subject: [PATCH 04/12] Fixes for coments and tests --- .../evomaster/core/output/dto/DtoWriter.kt | 13 +----- .../evomaster/core/output/dto/GeneToDto.kt | 20 ++------ .../core/output/TestCaseWriterTest.kt | 37 +++++++++++++++ .../core/output/dto/DtoWriterJsonPatchTest.kt | 46 +++++++++++++++++-- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index 3f527d6178..bce25ebc8a 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -128,18 +128,7 @@ class DtoWriter( } } - /** - * Collects the DTO needed to render a JSON Patch document (RFC 6902) as a typed payload. - * - * A single shared [GeneToDto.JSON_PATCH_OPERATION_DTO] class is used for all patch operations, - * holding every field used across the operation types: "op" and "path" (always present), - * "from" (move/copy) and "value" (add/replace/test). The "value" field is typed as the generic - * object type since a JSON Patch value can be any JSON value; fields not used by a given - * operation are left null and skipped on serialization (see @JsonInclude(NON_NULL)). - * - * When an operation carries an object or array value, the corresponding nested DTOs are also - * collected so the value can be rendered as a proper object instead of stringified JSON. - */ + // Registers the shared JsonPatchOperation DTO and collects nested DTOs for object/array values. private fun calculateDtoFromJsonPatch(gene: JsonPatchDocumentGene) { val dtoName = GeneToDto.JSON_PATCH_OPERATION_DTO val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(it) } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 22d6fb868b..20bb119809 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -42,11 +42,7 @@ class GeneToDto( ) { companion object { - /** - * Shared DTO class name used to represent a single JSON Patch operation (RFC 6902). - * A single class is enough since all operations share the same set of possible fields; - * fields not used by a given operation are simply left null and skipped on serialization. - */ + // Shared DTO class name for all JSON Patch operations (RFC 6902). const val JSON_PATCH_OPERATION_DTO = "JsonPatchOperation" const val FIELD_OP = "op" const val FIELD_PATH = "path" @@ -109,11 +105,7 @@ class GeneToDto( } } - /** - * A JSON Patch document (RFC 6902) is rendered as a list of [JSON_PATCH_OPERATION_DTO] objects, - * one per active operation in the document. This mirrors the JSON array structure of the payload - * while keeping the generated test readable and type-safe. - */ + // Renders a JSON Patch document as a List, one DTO per active operation. private fun getJsonPatchDtoCall(gene: JsonPatchDocumentGene, counters: MutableList): DtoCall { val listVarName = "list_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" val result = mutableListOf() @@ -133,6 +125,7 @@ class GeneToDto( return DtoCall(listVarName, result) } + // Renders a single RFC 6902 operation as a JsonPatchOperation DTO with only its relevant fields set. private fun getJsonPatchOperationCall(operation: JsonPatchOperationGene, counters: MutableList): DtoCall { val varName = "dto_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" val result = mutableListOf() @@ -157,11 +150,7 @@ class GeneToDto( return DtoCall(varName, result) } - /** - * Sets the "value" field of a JSON Patch operation. Primitive values are inlined as literals, - * while object and array values reuse the regular DTO/list generation so nested structures - * are rendered as proper objects rather than stringified JSON. - */ + // Sets the "value" field: primitives are inlined as literals, objects/arrays delegate to DTO generation. private fun setJsonPatchValue( varName: String, valueGene: Gene, @@ -183,6 +172,7 @@ class GeneToDto( } } + // Returns the printable string representation of a gene's leaf value with its language-specific suffix. private fun renderLeafValue(gene: Gene): String { val leafGene = gene.getLeafGene() return "${leafGene.getValueAsPrintableString(targetFormat = outputFormat)}${getValueSuffix(leafGene)}" diff --git a/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt index d2454da456..81523a8733 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt @@ -27,8 +27,11 @@ import org.evomaster.core.search.gene.UUIDGene import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.output.dto.GeneToDto +import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene import org.evomaster.core.search.gene.utils.GeneUtils import org.evomaster.core.search.gene.wrapper.OptionalGene +import org.evomaster.core.search.service.Randomness import org.evomaster.core.sql.schema.TableId import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -1641,6 +1644,40 @@ public void test() throws Exception { } } + @Test + fun testJsonPatchBodyRenderedAsDto() { + val format = OutputFormat.KOTLIN_JUNIT_5 + val baseUrlOfSut = "baseUrlOfSut" + + val schema = ObjectGene("body", listOf(StringGene("name"), IntegerGene("age"))) + val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } + val bodyParam = BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) + + val action = RestCallAction("1", HttpVerb.PATCH, RestPath("/items/1"), mutableListOf(bodyParam)) + val individual = RestIndividual(mutableListOf(action), SampleType.RANDOM) + TestUtils.doInitializeIndividualForTesting(individual, Randomness().apply { updateSeed(42L) }) + + val fitnessVal = FitnessValue(0.0) + val result = RestCallResult(action.getLocalId()).apply { setStatusCode(200) } + val ei = EvaluatedIndividual(fitnessVal, individual, listOf(result)) + + val config = getConfig(format) + config.dtoForRequestPayload = true + config.problemType = EMConfig.ProblemType.REST + + val test = TestCase(test = ei, name = "test") + val writer = RestTestCaseWriter(config, PartialOracles()) + val output = writer.convertToCompilableTestCode(test, baseUrlOfSut).toString() + + // The JSON Patch body must be rendered as a DTO list, not as a raw JSON string. + assertTrue(output.contains("list_${GeneToDto.JSON_PATCH_OPERATION_DTO}_"), + "Expected DTO list variable in generated output") + assertTrue(output.contains(".body(list_${GeneToDto.JSON_PATCH_OPERATION_DTO}_"), + "Expected DTO variable passed as body argument") + assertFalse(output.contains("{\"op\":"), + "Body must not contain raw JSON string representation of a patch operation") + } + @Test fun testInActiveBodyParamInTest(){ val stringGene = StringGene("stringGene") diff --git a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt index 83433a6c33..73594607ea 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/dto/DtoWriterJsonPatchTest.kt @@ -1,28 +1,34 @@ package org.evomaster.core.output.dto +import org.evomaster.core.TestUtils import org.evomaster.core.output.OutputFormat import org.evomaster.core.output.Termination import org.evomaster.core.output.naming.RestActionTestCaseUtils.getEvaluatedIndividualWith import org.evomaster.core.output.naming.RestActionTestCaseUtils.getRestCallAction import org.evomaster.core.problem.api.param.Param +import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.core.problem.rest.data.RestCallResult +import org.evomaster.core.problem.rest.data.RestIndividual import org.evomaster.core.problem.rest.param.BodyParam +import org.evomaster.core.search.EvaluatedIndividual +import org.evomaster.core.search.FitnessValue import org.evomaster.core.search.Solution import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.search.gene.collection.ArrayGene import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.jsonpatch.JsonPatchDocumentGene +import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.string.StringGene +import org.evomaster.core.search.service.Randomness import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.nio.file.Paths import java.util.Collections.singletonList -/** - * Verifies that [DtoWriter] collects the shared JsonPatchOperation DTO (RFC 6902) with every - * field used across operation types, so a JSON Patch payload can be written as a DTO. - */ class DtoWriterJsonPatchTest { private val outputTestSuitePath = Paths.get("./target/dto-writer-json-patch-test") @@ -66,4 +72,36 @@ class DtoWriterJsonPatchTest { assertNotNull(operationDto) assertEquals(DtoField(GeneToDto.FIELD_VALUE, "Object"), operationDto!!.fieldsMap[GeneToDto.FIELD_VALUE]) } + + @Test + fun collectsNestedObjectDtoWhenValueIsArrayOfObjects() { + // Schema where the only patchable field is an array of objects: + // every add/replace/test operation will have an ArrayGene as its value gene, + // so calculateDtoFromJsonPatch must visit calculateDtoFromArray for those operations. + val tagSchema = ObjectGene("Tag", listOf(StringGene("label")), refType = "Tag") + val schema = ObjectGene("body", listOf(ArrayGene("tags", tagSchema))) + val typeGene = EnumGene("contentType", listOf("application/json-patch+json")).apply { index = 0 } + val bodyParam = BodyParam(gene = JsonPatchDocumentGene("patch", schema), typeGene = typeGene) + + val action = getRestCallAction("/items/{id}", HttpVerb.PATCH, mutableListOf(bodyParam)) + val individual = RestIndividual(mutableListOf(action), SampleType.RANDOM) + TestUtils.doInitializeIndividualForTesting(individual, Randomness().apply { updateSeed(42L) }) + + val result = RestCallResult(action.getLocalId()).apply { setStatusCode(200) } + val ei = EvaluatedIndividual(FitnessValue(0.0), individual, listOf(result)) + val solution = Solution(singletonList(ei), "", "", Termination.NONE, emptyList(), emptyList()) + + val dtoWriter = DtoWriter(OutputFormat.KOTLIN_JUNIT_5) + dtoWriter.write(outputTestSuitePath, testPackage, solution) + val dtos = dtoWriter.getCollectedDtos() + + assertNotNull(dtos[GeneToDto.JSON_PATCH_OPERATION_DTO]) + + val patchGene = bodyParam.primaryGene() as JsonPatchDocumentGene + val pathValueOps = patchGene.operations.filterIsInstance() + assertTrue(pathValueOps.isNotEmpty(), "Seed 42L must produce at least one add/replace/test operation") + // All add/replace/test operations carry an ArrayGene value in this schema; + // the nested Tag DTO must therefore be collected. + assertNotNull(dtos["Tag"], "Nested Tag DTO must be collected for operations with array-of-objects value") + } } From 91b0e56bc9564393a8fe0e588b9180f14c5c74f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Fri, 26 Jun 2026 11:27:41 -0300 Subject: [PATCH 05/12] Fixes for comments in pr --- .../org/evomaster/core/output/dto/DtoWriter.kt | 12 +++++++----- .../org/evomaster/core/output/dto/GeneToDto.kt | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index bce25ebc8a..17de99acc7 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -128,13 +128,15 @@ class DtoWriter( } } - // Registers the shared JsonPatchOperation DTO and collects nested DTOs for object/array values. + /** + * Registers the shared JsonPatchOperation DTO and collects nested DTOs for object/array values. + */ private fun calculateDtoFromJsonPatch(gene: JsonPatchDocumentGene) { val dtoName = GeneToDto.JSON_PATCH_OPERATION_DTO val dtoClass = dtoCollector.computeIfAbsent(dtoName) { DtoClass(it) } - dtoClass.addField(GeneToDto.FIELD_OP, DtoField(GeneToDto.FIELD_OP, "String")) - dtoClass.addField(GeneToDto.FIELD_PATH, DtoField(GeneToDto.FIELD_PATH, "String")) - dtoClass.addField(GeneToDto.FIELD_FROM, DtoField(GeneToDto.FIELD_FROM, "String")) + dtoClass.addField(GeneToDto.FIELD_OP, DtoField(GeneToDto.FIELD_OP, GeneToDto.TYPE_STRING)) + dtoClass.addField(GeneToDto.FIELD_PATH, DtoField(GeneToDto.FIELD_PATH, GeneToDto.TYPE_STRING)) + dtoClass.addField(GeneToDto.FIELD_FROM, DtoField(GeneToDto.FIELD_FROM, GeneToDto.TYPE_STRING)) dtoClass.addField(GeneToDto.FIELD_VALUE, DtoField(GeneToDto.FIELD_VALUE, anyType())) dtoCollector[dtoName] = dtoClass @@ -147,7 +149,7 @@ class DtoWriter( } private fun anyType(): String { - return if (outputFormat.isJava()) "Object" else "Any" + return if (outputFormat.isJava()) GeneToDto.TYPE_JAVA_OBJECT else GeneToDto.TYPE_KOTLIN_ANY } private fun calculateDtoFromFixedMapGene(gene: FixedMapGene<*, *>, actionName: String) { diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 20bb119809..7d97f7c17e 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -48,6 +48,9 @@ class GeneToDto( const val FIELD_PATH = "path" const val FIELD_FROM = "from" const val FIELD_VALUE = "value" + const val TYPE_STRING = "String" + const val TYPE_JAVA_OBJECT = "Object" + const val TYPE_KOTLIN_ANY = "Any" } private val log: Logger = LoggerFactory.getLogger(GeneToDto::class.java) @@ -105,7 +108,9 @@ class GeneToDto( } } - // Renders a JSON Patch document as a List, one DTO per active operation. + /** + * Renders a JSON Patch document as a List, one DTO per active operation. + */ private fun getJsonPatchDtoCall(gene: JsonPatchDocumentGene, counters: MutableList): DtoCall { val listVarName = "list_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" val result = mutableListOf() @@ -125,7 +130,9 @@ class GeneToDto( return DtoCall(listVarName, result) } - // Renders a single RFC 6902 operation as a JsonPatchOperation DTO with only its relevant fields set. + /** + * Renders a single RFC 6902 operation as a JsonPatchOperation DTO with only its relevant fields set. + */ private fun getJsonPatchOperationCall(operation: JsonPatchOperationGene, counters: MutableList): DtoCall { val varName = "dto_${JSON_PATCH_OPERATION_DTO}_${counters.joinToString("_")}" val result = mutableListOf() @@ -150,7 +157,9 @@ class GeneToDto( return DtoCall(varName, result) } - // Sets the "value" field: primitives are inlined as literals, objects/arrays delegate to DTO generation. + /** + * Sets the "value" field: primitives are inlined as literals, objects/arrays delegate to DTO generation. + */ private fun setJsonPatchValue( varName: String, valueGene: Gene, From a2e50e85c9eaa8a5a36205d7657506adbb6533e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Tue, 7 Jul 2026 23:57:41 -0300 Subject: [PATCH 06/12] We add 2 tests to prove dto conversion in json patch --- .../JsonPatchObjectValueApplication.kt | 81 ++++++++++++++ .../JsonPatchStringValueApplication.kt | 63 +++++++++++ .../JsonPatchObjectValueController.kt | 9 ++ .../JsonPatchStringValueController.kt | 9 ++ .../JsonPatchObjectValueDtoEMTest.kt | 103 ++++++++++++++++++ .../JsonPatchStringValueDtoEMTest.kt | 94 ++++++++++++++++ 6 files changed, 359 insertions(+) create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt new file mode 100644 index 0000000000..02aa487b63 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueApplication.kt @@ -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) { + 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 { + return ResponseEntity.ok(hardcoded) + } + + @PatchMapping("/{id}", consumes = ["application/json-patch+json"]) + fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + 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 = emptyList(), + val archived: List = emptyList() +) + +data class Item(val label: String = "", val quantity: Int = 0) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt new file mode 100644 index 0000000000..efbefa26c1 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueApplication.kt @@ -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) { + 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 { + return ResponseEntity.ok(hardcoded) + } + + @PatchMapping("/{id}", consumes = ["application/json-patch+json"]) + fun patchResource(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + 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 = "") diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt new file mode 100644 index 0000000000..0206f510dd --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueController.kt @@ -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) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt new file mode 100644 index 0000000000..68bedb5633 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueController.kt @@ -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) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt new file mode 100644 index 0000000000..d31cea8285 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt @@ -0,0 +1,103 @@ +package org.evomaster.e2etests.spring.openapi.v3.jsonpatchobjectvalue + +import com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue.JsonPatchObjectValueController +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.memberProperties +import kotlin.reflect.jvm.isAccessible + +/** + * 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 -> + + setOption(args, "dtoForRequestPayload", "true") + + 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) + } + + private fun initDtoClass(name: String): Pair, 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, instance: Any, propertyName: String, propertyValue: Any?) { + val property = klass.memberProperties + .firstOrNull { it.name == propertyName } as? KMutableProperty1 + Assertions.assertNotNull(property) + + property?.let { + it.isAccessible = true + it.set(instance, ofNullable(propertyValue)) + } + Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt new file mode 100644 index 0000000000..1de78a82dd --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt @@ -0,0 +1,94 @@ +package org.evomaster.e2etests.spring.openapi.v3.jsonpatchstringvalue + +import com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue.JsonPatchStringValueController +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.memberProperties +import kotlin.reflect.jvm.isAccessible + +/** + * E2E test: JSON Patch DTO with a PRIMITIVE (string) value. + * + * What is being tested: + * When `dtoForRequestPayload` is enabled and the SUT has a JSON Patch endpoint whose resource + * fields are all strings, EvoMaster must generate the DTO that represents the JSON Patch + * operations (the shared `JsonPatchOperation` DTO, with the RFC 6902 fields op/path/from/value), + * and each operation `value` must be a primitive string inlined as a literal (no nested value DTO). + * + * Why the assertion is unambiguous: + * The SUT only exposes a GET (no request body -> no DTO) and a PATCH (json-patch). Therefore, if + * EvoMaster generates any DTO at all, it can only be the JSON Patch one. Reflectively loading + * `org.foo.dto.JsonPatchOperation` after the generated suite is compiled proves it was generated. + * + * This mirrors the reflective-assert approach of + * [org.evomaster.e2etests.spring.openapi.v3.dtoreflectiveassert.DtoReflectiveAssertEMTest]. + */ +class JsonPatchStringValueDtoEMTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(JsonPatchStringValueController()) + } + } + + @Test + fun testRunEM() { + runTestHandlingFlakyAndCompilation( + "JsonPatchStringValueDtoEM", + "org.foo.JsonPatchStringValueDtoEM", + 100, + ) { args: MutableList -> + + setOption(args, "dtoForRequestPayload", "true") + + val solution = initAndRun(args) + + Assertions.assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/resource/{id}", "OK") + } + + assertJsonPatchOperationDtoCreatedWithStringValue() + } + + /** + * The shared JsonPatchOperation DTO must exist with the RFC 6902 fields (op, path, from, value). + * Because the resource only has string fields, `value` is a primitive: we prove it can hold a String. + */ + private fun assertJsonPatchOperationDtoCreatedWithStringValue() { + val (klass, instance) = initDtoClass("JsonPatchOperation") + assertProperty(klass, instance, "op", "replace") + assertProperty(klass, instance, "path", "/name") + assertProperty(klass, instance, "from", "/description") + // A JSON Patch value can be any JSON value; here it is a primitive string inlined as a literal. + assertProperty(klass, instance, "value", "EvoMaster") + } + + private fun initDtoClass(name: String): Pair, 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, instance: Any, propertyName: String, propertyValue: Any?) { + val property = klass.memberProperties + .firstOrNull { it.name == propertyName } as? KMutableProperty1 + Assertions.assertNotNull(property) + + property?.let { + it.isAccessible = true + it.set(instance, ofNullable(propertyValue)) + } + Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) + } +} From 6b48ffa11db8a2ccc4772598c55040a59bd09846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Sun, 12 Jul 2026 20:12:55 -0300 Subject: [PATCH 07/12] Fixes for pr --- .../spring/openapi/v3/SpringTestBase.kt | 27 +++++++++++++++++ .../JsonPatchObjectValueDtoEMTest.kt | 30 +++---------------- .../JsonPatchStringValueDtoEMTest.kt | 30 +++---------------- .../evomaster/core/output/dto/DtoWriter.kt | 1 - .../evomaster/core/output/dto/GeneToDto.kt | 5 ++-- 5 files changed, 37 insertions(+), 56 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt index c2a88daa27..7716891b60 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/SpringTestBase.kt @@ -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. @@ -25,4 +33,23 @@ abstract class SpringTestBase : RestTestBase(){ } } + protected fun initDtoClass(name: String): Pair, 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, instance: Any, propertyName: String, propertyValue: Any?) { + val property = klass.memberProperties + .firstOrNull { it.name == propertyName } as? KMutableProperty1 + Assertions.assertNotNull(property) + + property?.let { + it.isAccessible = true + it.set(instance, ofNullable(propertyValue)) + } + Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) + } + } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt index d31cea8285..786126acab 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchobjectvalue/JsonPatchObjectValueDtoEMTest.kt @@ -1,18 +1,11 @@ package org.evomaster.e2etests.spring.openapi.v3.jsonpatchobjectvalue import com.foo.rest.examples.spring.openapi.v3.jsonpatchobjectvalue.JsonPatchObjectValueController -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.memberProperties -import kotlin.reflect.jvm.isAccessible /** * E2E test: JSON Patch DTO with a NON-PRIMITIVE value. @@ -51,6 +44,10 @@ class JsonPatchObjectValueDtoEMTest : SpringTestBase() { ) { args: MutableList -> 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) @@ -81,23 +78,4 @@ class JsonPatchObjectValueDtoEMTest : SpringTestBase() { assertProperty(klass, instance, "label", "a label") assertProperty(klass, instance, "quantity", 7) } - - private fun initDtoClass(name: String): Pair, 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, instance: Any, propertyName: String, propertyValue: Any?) { - val property = klass.memberProperties - .firstOrNull { it.name == propertyName } as? KMutableProperty1 - Assertions.assertNotNull(property) - - property?.let { - it.isAccessible = true - it.set(instance, ofNullable(propertyValue)) - } - Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) - } } diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt index 1de78a82dd..9f7d43dd1d 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/jsonpatchstringvalue/JsonPatchStringValueDtoEMTest.kt @@ -1,18 +1,11 @@ package org.evomaster.e2etests.spring.openapi.v3.jsonpatchstringvalue import com.foo.rest.examples.spring.openapi.v3.jsonpatchstringvalue.JsonPatchStringValueController -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.memberProperties -import kotlin.reflect.jvm.isAccessible /** * E2E test: JSON Patch DTO with a PRIMITIVE (string) value. @@ -50,6 +43,10 @@ class JsonPatchStringValueDtoEMTest : SpringTestBase() { ) { args: MutableList -> 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) @@ -72,23 +69,4 @@ class JsonPatchStringValueDtoEMTest : SpringTestBase() { // A JSON Patch value can be any JSON value; here it is a primitive string inlined as a literal. assertProperty(klass, instance, "value", "EvoMaster") } - - private fun initDtoClass(name: String): Pair, 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, instance: Any, propertyName: String, propertyValue: Any?) { - val property = klass.memberProperties - .firstOrNull { it.name == propertyName } as? KMutableProperty1 - Assertions.assertNotNull(property) - - property?.let { - it.isAccessible = true - it.set(instance, ofNullable(propertyValue)) - } - Assertions.assertEquals(ofNullable(propertyValue), property?.get(instance)) - } } diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt index 17de99acc7..dda00a5848 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/DtoWriter.kt @@ -138,7 +138,6 @@ class DtoWriter( dtoClass.addField(GeneToDto.FIELD_PATH, DtoField(GeneToDto.FIELD_PATH, GeneToDto.TYPE_STRING)) dtoClass.addField(GeneToDto.FIELD_FROM, DtoField(GeneToDto.FIELD_FROM, GeneToDto.TYPE_STRING)) dtoClass.addField(GeneToDto.FIELD_VALUE, DtoField(GeneToDto.FIELD_VALUE, anyType())) - dtoCollector[dtoName] = dtoClass gene.operations.filterIsInstance().forEach { operation -> when (val valueGene = operation.pathValueChoice.activeGene().second.getLeafGene()) { diff --git a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt index 7d97f7c17e..9f39a1e946 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/dto/GeneToDto.kt @@ -116,11 +116,10 @@ class GeneToDto( val result = mutableListOf() result.add(dtoOutput.getNewListStatement(JSON_PATCH_OPERATION_DTO, listVarName)) - var operationCounter = 1 - gene.operations.forEach { operation -> + gene.operations.forEachIndexed { index, operation -> val childCounter = mutableListOf().apply { addAll(counters) - add(operationCounter++) + add(index + 1) } val operationCall = getJsonPatchOperationCall(operation, childCounter) result.addAll(operationCall.objectCalls) From 89734016c8532167255358fa4646a5cbd5ab2908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Sun, 12 Jul 2026 21:05:44 -0300 Subject: [PATCH 08/12] Extract duplicated code --- .../DtoReflectiveAssertEMTest.kt | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt index 5387959de2..2fb3076534 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/dtoreflectiveassert/DtoReflectiveAssertEMTest.kt @@ -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 @@ -161,25 +156,6 @@ class DtoReflectiveAssertEMTest: SpringTestBase() { assertAdditionalPropertiesFunction(parentKlass, parentInstance, "no_root_key", childInstance) } - private fun initDtoClass(name: String): Pair, 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, instance: Any, propertyName: String, propertyValue: Any?) { - val property = klass.memberProperties - .firstOrNull { it.name == propertyName } as? KMutableProperty1 - 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, instance: Any, propertyName: String, propertyValue: Any?) { val setterFunction = klass.memberFunctions.firstOrNull { it.name == "addAdditionalProperty" From 7fe3fcf7febfee5b6a62f4d1b27a6ff63a53c7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Sun, 12 Jul 2026 23:31:40 -0300 Subject: [PATCH 09/12] Feature flags for xml and jsonpatch --- .../kotlin/org/evomaster/core/EMConfig.kt | 10 +++++++++ .../rest/builder/RestActionBuilderV3.kt | 21 ++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt index ed830a3220..0984284049 100644 --- a/core/src/main/kotlin/org/evomaster/core/EMConfig.kt +++ b/core/src/main/kotlin/org/evomaster/core/EMConfig.kt @@ -1372,6 +1372,16 @@ class EMConfig { "Only available for JVM languages") var dtoForRequestPayload = false + @Experimental + @Cfg("Enable JSON Patch (RFC 6902) gene support when the request Content-Type is 'application/json-patch+json'." + + " When false, such endpoints are treated as regular JSON bodies, reproducing the behavior before this feature was introduced.") + var enableJsonPatchGeneSupport = true + + @Experimental + @Cfg("Enable XML-aware field naming for body genes when the request Content-Type is XML." + + " When false, body gene names fall back to the pre-feature behavior (schema ref name or 'body').") + var enableXmlBodyGeneSupport = true + @Important(6.0) @Cfg("Host name or IP address of where the SUT EvoMaster Controller Driver is listening on." + " This option is only needed for white-box testing.") diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt index ebf0e6ddaa..19a168f25e 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/builder/RestActionBuilderV3.kt @@ -115,6 +115,10 @@ object RestActionBuilderV3 { val enableAdvancedFormats: Boolean = true, val inferFormatFromNames: Boolean = true, + + val enableJsonPatchGeneSupport: Boolean = true, + + val enableXmlBodyGeneSupport: Boolean = true, ){ constructor(config: EMConfig): this( enableConstraintHandling = config.enableSchemaConstraintHandling, @@ -123,7 +127,9 @@ object RestActionBuilderV3 { probUseExamples = config.probRestExamples, usingWhiteBox = !config.blackBox, enableAdvancedFormats = config.enableAdvancedFormats, - inferFormatFromNames = config.inferFormatFromNames + inferFormatFromNames = config.inferFormatFromNames, + enableJsonPatchGeneSupport = config.enableJsonPatchGeneSupport, + enableXmlBodyGeneSupport = config.enableXmlBodyGeneSupport, ) init { @@ -748,7 +754,8 @@ object RestActionBuilderV3 { listOf() } - val isJsonPatch = verb == HttpVerb.PATCH && bodies.keys.any { it.contains("json-patch") } + val isJsonPatch = options.enableJsonPatchGeneSupport && + verb == HttpVerb.PATCH && bodies.keys.any { it.contains("json-patch") } val name: String var gene: Gene @@ -774,9 +781,13 @@ object RestActionBuilderV3 { } gene = JsonPatchDocumentGene(name, resourceGene) } else { - // $ref schemas do not carry XML metadata; resolving the reference is required to obtain the correct XML element name from the target schema - val deref = obj.schema.`$ref`?.let { ref -> SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema - name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" + if (options.enableXmlBodyGeneSupport) { + // $ref schemas do not carry XML metadata; resolving the reference is required to obtain the correct XML element name from the target schema + val deref = obj.schema.`$ref`?.let { ref -> SchemaUtils.getReferenceSchema(schemaHolder, currentSchema, ref, messages) } ?: obj.schema + name = deref?.xml?.name ?: deref?.`$ref`?.substringAfterLast("/") ?: "body" + } else { + name = obj.schema.`$ref`?.substringAfterLast("/") ?: "body" + } gene = getGene(name, obj.schema, schemaHolder, currentSchema, referenceClassDef = null, options = options, messages = messages, examples = examples) } From 80a4c7bb08b2e33c74a30f17cf9507866d0f22f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 13 Jul 2026 21:31:44 -0300 Subject: [PATCH 10/12] Fix for tests --- .../spring/examples/adaptivehypermutation/DeterminismTest.java | 2 +- .../spring/examples/adaptivehypermutation/DeterminismTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java b/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java index 75c9268ddd..04f266caf0 100644 --- a/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java +++ b/core-tests/e2e-tests/spring/spring-rest-h2-v1/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java @@ -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))); } diff --git a/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java b/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java index 2ddc3b2b3e..2ea9a8143c 100644 --- a/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java +++ b/core-tests/jdk-8/spring-rest-openapi-v2-tests/src/test/java/org/evomaster/e2etests/spring/examples/adaptivehypermutation/DeterminismTest.java @@ -27,7 +27,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)); }); } From a0c37b697a571d0ea16f850484dfcbfb55b137c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 13 Jul 2026 23:02:45 -0300 Subject: [PATCH 11/12] Update options.md --- docs/options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/options.md b/docs/options.md index 39190eeb64..1888042468 100644 --- a/docs/options.md +++ b/docs/options.md @@ -287,9 +287,11 @@ There are 3 types of options: |`enableAdvancedFormats`| __Boolean__. Whether to enable the handling of new type formats in OpenAPI schemas, e.g., the ones introduced in 3.1.0. *Default value*: `false`.| |`enableCustomizedMethodForMockObjectHandling`| __Boolean__. Whether to apply customized method (i.e., implement 'customizeMockingRPCExternalService' for external services or 'customizeMockingDatabase' for database) to handle mock object. *Default value*: `false`.| |`enableCustomizedMethodForScheduleTaskHandling`| __Boolean__. Whether to apply customized method (i.e., implement 'customizeScheduleTaskInvocation' for invoking schedule task) to invoke schedule task. *Default value*: `false`.| +|`enableJsonPatchGeneSupport`| __Boolean__. Enable JSON Patch (RFC 6902) gene support when the request Content-Type is 'application/json-patch+json'. When false, such endpoints are treated as regular JSON bodies, reproducing the behavior before this feature was introduced. *Default value*: `true`.| |`enableRPCCustomizedTestOutput`| __Boolean__. Whether to enable customized RPC Test output if 'customizeRPCTestOutput' is implemented. *Default value*: `false`.| |`enableStaticFlakyInference`| __Boolean__. Specify whether to infer potential flakiness statically from response values, such as timestamps, UUIDs, hashes and runtime-specific messages. *Depends on*: `handleFlakiness=true`. *Default value*: `true`.| |`enableWriteSnapshotTests`| __Boolean__. Enable to print snapshots of the generated tests during the search in an interval defined in snapshotsInterval. *Default value*: `false`.| +|`enableXmlBodyGeneSupport`| __Boolean__. Enable XML-aware field naming for body genes when the request Content-Type is XML. When false, body gene names fall back to the pre-feature behavior (schema ref name or 'body'). *Default value*: `true`.| |`execNumForDetectFlakiness`| __Int__. Specify the number of re-executions for detecting flakiness in tests. Set to 0 to disable re-execution based flakiness detection. *Constraints*: `min=0.0`. *Depends on*: `handleFlakiness=true`. *Default value*: `1`.| |`executiveSummary`| __Boolean__. Generate an executive summary, containing an example of each category of potential faults found.NOTE: This option is only meaningful when used in conjunction with test suite splitting. *Default value*: `false`.| |`expectationsActive`| __Boolean__. Enable Expectation Generation. If enabled, expectations will be generated. A variable called expectationsMasterSwitch is added to the test suite, with a default value of false. If set to true, an expectation that fails will cause the test case containing it to fail. *Default value*: `false`.| From 2c3473c62dfae80b873ce80a29d8a79cb5bac62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 13 Jul 2026 23:52:11 -0300 Subject: [PATCH 12/12] Add tests with keys in false and assertFalse for coverage --- .../rest/bb/jsonpatch/BBJsonPatchTest.kt | 34 ++++++++++++++++ .../e2etests/spring/rest/bb/xml/BBXMLTest.kt | 40 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt index f4d5879149..00915490f2 100644 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatch/BBJsonPatchTest.kt @@ -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 @@ -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" + ) + } } \ No newline at end of file diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt index 6435011e75..cb6cb60fcd 100644 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/xml/BBXMLTest.kt @@ -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 @@ -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" + ) + } } \ No newline at end of file