From c28c6a56a3dbda63fb21b27a8dddb14cbb008e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Mon, 6 Jul 2026 23:58:31 -0300 Subject: [PATCH 1/2] Add new test to prove that json patch output is still string, and the rest is dto --- .../jsonpatchdto/BBJsonPatchDtoApplication.kt | 80 ++++++++++ .../jsonpatchdto/BBJsonPatchDtoController.kt | 5 + .../bb/jsonpatchdto/BBJsonPatchDtoTest.kt | 138 ++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt new file mode 100644 index 0000000000..55b6f57227 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt @@ -0,0 +1,80 @@ +package com.foo.rest.examples.bb.jsonpatchdto + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +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.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping("/api/jsonpatchdto/resources") +open class BBJsonPatchDtoApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BBJsonPatchDtoApplication::class.java, *args) + } + } + + private val createdId = 1L + private val mapper = ObjectMapper() + + @ApiResponses(value = [ + ApiResponse(responseCode = "201", description = "Created") + ]) + @PostMapping(consumes = ["application/json"], produces = ["application/json"]) + fun create(@RequestBody dto: BBJsonPatchResourceDto): ResponseEntity { + if (dto.name.isNotBlank()) + CoveredTargets.cover("JSON_PATCH_DTO_CREATED_WITH_NAME") + + CoveredTargets.cover("JSON_PATCH_DTO_CREATED") + return ResponseEntity.status(HttpStatus.CREATED).body(BBJsonPatchCreatedDto(createdId)) + } + + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Patched"), + ApiResponse(responseCode = "400", description = "Invalid patch document"), + ApiResponse(responseCode = "404", description = "Missing resource") + ]) + @PatchMapping("/{id}", consumes = ["application/json-patch+json"], produces = ["text/plain"]) + fun patch(@PathVariable id: Long, @RequestBody body: String): ResponseEntity { + if (id != createdId) + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("missing") + + val patch = parsePatchDocument(body) + if (patch == null || patch.none { it.path("op").asText() == "replace" }) + return ResponseEntity.badRequest().body("invalid patch") + + CoveredTargets.cover("JSON_PATCH_DTO_PATCHED") + return ResponseEntity.ok("patched") + } + + private fun parsePatchDocument(body: String): JsonNode? = + try { + mapper.readTree(body).takeIf { it.isArray } + } catch (e: Exception) { + null + } +} + +data class BBJsonPatchResourceDto( + val name: String = "", + val age: Int = 0 +) + +data class BBJsonPatchCreatedDto( + val id: Long = 0 +) \ 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/jsonpatchdto/BBJsonPatchDtoController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoController.kt new file mode 100644 index 0000000000..116df1d054 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoController.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.bb.jsonpatchdto + +import com.foo.rest.examples.bb.SpringController + +class BBJsonPatchDtoController : SpringController(BBJsonPatchDtoApplication::class.java) \ 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/jsonpatchdto/BBJsonPatchDtoTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt new file mode 100644 index 0000000000..da5376860b --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt @@ -0,0 +1,138 @@ +package org.evomaster.e2etests.spring.rest.bb.jsonpatchdto + +import com.foo.rest.examples.bb.jsonpatchdto.BBJsonPatchDtoController +import org.evomaster.core.EMConfig +import org.evomaster.core.output.OutputFormat +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.data.RestPath +import org.evomaster.core.search.Solution +import org.evomaster.e2etests.spring.rest.bb.SpringTestBase +import org.evomaster.e2etests.utils.BlackBoxUtils +import org.junit.jupiter.api.Assertions.assertFalse +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 +import java.nio.file.Files +import java.nio.file.Paths + +class BBJsonPatchDtoTest : SpringTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun init() { + val config = EMConfig() + initClass(BBJsonPatchDtoController(), config) + } + } + + @ParameterizedTest + @EnumSource(value = OutputFormat::class, names = ["JAVA_JUNIT_5", "KOTLIN_JUNIT_5"]) + fun testJsonPatchDoesNotUseDtoWhenDtoPayloadsAreEnabled(outputFormat: OutputFormat) { + val outputFolderName = "BBJsonPatchDtoEM" + + executeAndEvaluateBBTest( + outputFormat, + outputFolderName, + 1000, + 3, + listOf( + "JSON_PATCH_DTO_CREATED", + "JSON_PATCH_DTO_CREATED_WITH_NAME", + "JSON_PATCH_DTO_PATCHED" + ) + ) { args: MutableList -> + + setOption(args, "dtoForRequestPayload", "true") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.POST, 201, "/api/jsonpatchdto/resources", null) + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/api/jsonpatchdto/resources/{id}", "patched") + assertHasAtLeastOne(solution, HttpVerb.PATCH, 404, "/api/jsonpatchdto/resources/{id}", "missing") + + // assertHasAtLeastOne only checks each call exists somewhere in the solution, independently + // and in any order. Here we additionally require a single generated test that first creates the + // resource with a POST (201) and then manipulates it with a PATCH (200), which is what proves the + // "DTO used for POST followed by a PATCH without DTO" scenario requested for this feature. + assertPostThenPatchInSameTest(solution) + } + + val generatedCode = readGeneratedCode(outputFormat, outputFolderName) + assertTrue( + generatedCode.contains("@JsonProperty(\"name\")") + && generatedCode.contains("@JsonProperty(\"age\")"), + "POST request body should be emitted as a DTO with the Resource request fields" + ) + assertFalse( + generatedCode.contains("JsonPatchOperation"), + "JSON Patch request body should keep using raw string JSON instead of a DTO" + ) + } + + /** + * Verifies that the solution contains at least one generated test (individual) in which a successful POST + * that creates the resource (201) is followed, later in the same test, by a successful PATCH (200) on the + * created resource. This is stronger than [assertHasAtLeastOne], which only checks each call exists somewhere + * in the solution independently and in any order. + */ + private fun assertPostThenPatchInSameTest(solution: Solution) { + val postPath = RestPath("/api/jsonpatchdto/resources") + val patchPath = RestPath("/api/jsonpatchdto/resources/{id}") + + val found = solution.individuals.any { ind -> + val actions = ind.individual.seeMainExecutableActions() + val results = ind.seeResults(actions) + + var createdIndex = -1 + for (i in actions.indices) { + val action = actions[i] + val result = results[i] as RestCallResult + + if (createdIndex < 0) { + if (action.verb == HttpVerb.POST + && result.getStatusCode() == 201 + && action.path.isEquivalent(postPath) + ) { + createdIndex = i + } + } else if (action.verb == HttpVerb.PATCH + && result.getStatusCode() == 200 + && action.path.isEquivalent(patchPath) + ) { + return@any true + } + } + false + } + + assertTrue( + found, + "Expected a single generated test where a POST (201) creating the resource is followed by a PATCH (200) on it" + ) + } + + private fun readGeneratedCode(outputFormat: OutputFormat, outputFolderName: String): String { + val baseLocation = if (outputFormat.isJava()) { + BlackBoxUtils.baseLocationForJava + } else { + BlackBoxUtils.baseLocationForKotlin + } + + val basePath = listOf( + Paths.get(baseLocation), + Paths.get("core-tests/e2e-tests/spring/spring-rest-bb", baseLocation) + ).first { Files.exists(it) } + + return Files.walk(basePath) + .filter { Files.isRegularFile(it) } + .filter { it.toString().contains(outputFolderName) } + .map { Files.readString(it) } + .toList() + .joinToString("\n") + } +} \ No newline at end of file From 1f4c997a9622720a66c55733d5c77dd7f045d3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ROMINA=20JULIETA=20SU=C3=81REZ?= Date: Tue, 7 Jul 2026 13:37:50 -0300 Subject: [PATCH 2/2] Fix redundancy --- .../examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt | 1 - .../spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt | 4 ---- 2 files changed, 5 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt index 55b6f57227..c1bddf77d4 100644 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/jsonpatchdto/BBJsonPatchDtoApplication.kt @@ -45,7 +45,6 @@ open class BBJsonPatchDtoApplication { } @ApiResponses(value = [ - ApiResponse(responseCode = "200", description = "Patched"), ApiResponse(responseCode = "400", description = "Invalid patch document"), ApiResponse(responseCode = "404", description = "Missing resource") ]) diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt index da5376860b..df3a546062 100644 --- a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/jsonpatchdto/BBJsonPatchDtoTest.kt @@ -55,10 +55,6 @@ class BBJsonPatchDtoTest : SpringTestBase() { assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/api/jsonpatchdto/resources/{id}", "patched") assertHasAtLeastOne(solution, HttpVerb.PATCH, 404, "/api/jsonpatchdto/resources/{id}", "missing") - // assertHasAtLeastOne only checks each call exists somewhere in the solution, independently - // and in any order. Here we additionally require a single generated test that first creates the - // resource with a POST (201) and then manipulates it with a PATCH (200), which is what proves the - // "DTO used for POST followed by a PATCH without DTO" scenario requested for this feature. assertPostThenPatchInSameTest(solution) }