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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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<String>) {
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<BBJsonPatchCreatedDto> {
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 = "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<String> {
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
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.foo.rest.examples.bb.jsonpatchdto

import com.foo.rest.examples.bb.SpringController

class BBJsonPatchDtoController : SpringController(BBJsonPatchDtoApplication::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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<String> ->

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")

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<RestIndividual>) {
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")
}
}
Loading