diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index f11a021025..9358ad2df9 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -323,8 +323,8 @@ shaded.javassist - jersey - shaded.jersey + jersey.repackaged + shaded.jersey.repackaged org.aopalliance diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/emptybody/BBEmptyBodyApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/emptybody/BBEmptyBodyApplication.kt new file mode 100644 index 0000000000..963b885049 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/emptybody/BBEmptyBodyApplication.kt @@ -0,0 +1,73 @@ +package com.foo.rest.examples.bb.emptybody + +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.HttpHeaders +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/bbemptybody"]) +@RestController +open class BBEmptyBodyApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BBEmptyBodyApplication::class.java, *args) + } + } + + private fun verifyHeader(headers: HttpHeaders) { + + /* + tricky... + for sure we don't want content-type on empty request, but, in theory, + we might want to have + content-length: 0 + to handle some frameworks that might have issues with it. + but didn't manage to get Jersey to handle it :( + also, it seems like even if we can control Jersey, the HTTP libraries in the + generated tests might have their own different "opinions" of what to send... + what the fucking mess... + TODO if we find fix, would need to update here + */ + + if (!headers[HttpHeaders.CONTENT_LENGTH].isNullOrEmpty()) { + + headers[HttpHeaders.CONTENT_LENGTH]!!.forEach { + val size = it.substringAfterLast(':').toInt() + if(size != 0){ + throw IllegalArgumentException("Content-Length must be null or 0 value: $headers") + } + } + } + if (!headers[HttpHeaders.CONTENT_TYPE].isNullOrEmpty()) { + throw IllegalArgumentException("Content-Length must be null: $headers") + } + } + + @PatchMapping(path = ["/patch"]) + fun patch(@RequestHeader headers: HttpHeaders) : ResponseEntity { + verifyHeader(headers) + CoveredTargets.cover("PATCH") + return ResponseEntity.ok("OK") + } + + @PutMapping(path = ["/put"]) + fun put(@RequestHeader headers: HttpHeaders) : ResponseEntity { + verifyHeader(headers) + CoveredTargets.cover("PUT") + return ResponseEntity.ok("OK") + } + + @PostMapping(path = ["/post"]) + fun post(@RequestHeader headers: HttpHeaders) : ResponseEntity { + verifyHeader(headers) + CoveredTargets.cover("POST") + return ResponseEntity.ok("OK") + } +} \ 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/emptybody/BBEmptyBodyController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/emptybody/BBEmptyBodyController.kt new file mode 100644 index 0000000000..9be93e6423 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/emptybody/BBEmptyBodyController.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.bb.emptybody + +import com.foo.rest.examples.bb.SpringController + +class BBEmptyBodyController : SpringController(BBEmptyBodyApplication::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/emptybody/BBEmptyBodyEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/emptybody/BBEmptyBodyEMTest.kt new file mode 100644 index 0000000000..76605f2d74 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/emptybody/BBEmptyBodyEMTest.kt @@ -0,0 +1,57 @@ +package org.evomaster.e2etests.spring.rest.bb.emptybody + +import com.foo.rest.examples.bb.emptybody.BBEmptyBodyController +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 + +class BBEmptyBodyEMTest : SpringTestBase() { + + companion object { + init { + shouldApplyInstrumentation = false + } + + @BeforeAll + @JvmStatic + fun init() { + initClass(BBEmptyBodyController()) + } + } + + + + @ParameterizedTest + @EnumSource + fun testBlackBoxOutput(outputFormat: OutputFormat) { + + executeAndEvaluateBBTest( + outputFormat, + "emptybody", + 100, + 3, + listOf("PATCH","PUT","POST") + ){ args: MutableList -> + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + assertHasAtLeastOne(solution, HttpVerb.PATCH, 200, "/api/bbemptybody/patch", "OK") + assertNone(solution, HttpVerb.PATCH, 415, "/api/bbemptybody/patch", null) + assertNone(solution, HttpVerb.PATCH, 500, "/api/bbemptybody/patch", null) + + + assertHasAtLeastOne(solution, HttpVerb.PUT, 200, "/api/bbemptybody/put", "OK") + assertNone(solution, HttpVerb.PUT, 415, "/api/bbemptybody/put", null) + assertNone(solution, HttpVerb.PUT, 500, "/api/bbemptybody/put", null) + + assertHasAtLeastOne(solution, HttpVerb.POST, 200, "/api/bbemptybody/post", "OK") + assertNone(solution, HttpVerb.POST, 415, "/api/bbemptybody/post", null) + assertNone(solution, HttpVerb.POST, 500, "/api/bbemptybody/post", null) + } + } +} 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..fd4330c0d3 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 @@ -16,6 +16,8 @@ import org.evomaster.core.problem.enterprise.EnterpriseActionGroup import org.evomaster.core.problem.externalservice.httpws.HttpExternalServiceAction import org.evomaster.core.problem.httpws.HttpWsAction import org.evomaster.core.problem.httpws.HttpWsCallResult +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.core.problem.rest.data.RestCallAction import org.evomaster.core.problem.rest.param.BodyParam import org.evomaster.core.problem.rest.param.HeaderParam import org.evomaster.core.problem.security.data.ActionStubMapping @@ -560,92 +562,107 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { return } - if (bodyParam != null) { + if (bodyParam == null) { - val send = sendBodyCommand() - - when { - format.isJavaOrKotlin() -> lines.add(".contentType(\"${bodyParam.contentType()}\")") - format.isJavaScript() -> lines.add(".set('Content-Type','${bodyParam.contentType()}')") - format.isPython() -> lines.add("headers[\"content-type\"] = \"${bodyParam.contentType()}\"") + if(call is RestCallAction && call.verb == HttpVerb.POST && format.isJavaOrKotlin()){ + // RestAssured automatically add content-type for forms on POST without body :( + lines.add(".noContentType()") } - if (bodyParam.isJson()) { + return + } - if (format.isPython()) { - lines.add("body = {}") - } + val send = sendBodyCommand() - val json = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.JSON, targetFormat = format) + when { + format.isJavaOrKotlin() -> lines.add(".contentType(\"${bodyParam.contentType()}\")") + format.isJavaScript() -> lines.add(".set('Content-Type','${bodyParam.contentType()}')") + format.isPython() -> lines.add("headers[\"content-type\"] = \"${bodyParam.contentType()}\"") + } - printSendJsonBody(json, lines, dtoVar) + if (bodyParam.isJson()) { - } else if (bodyParam.isTextPlain()) { + if (format.isPython()) { + lines.add("body = {}") + } - val body = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.TEXT, targetFormat = format) - // handle body only if it is not black - if (body.isNotBlank()){ - if (body != "\"\"") { - when { - format.isCsharp() -> { - lines.append("new StringContent(\"$body\", Encoding.UTF8, \"${bodyParam.contentType()}\")") - } - format.isPython() -> { - if (body.trim().isNullOrBlank()) { - lines.add("body = \"\"") - } else { - lines.add("body = $body") - } - } - else -> lines.add(".$send($body)") + val json = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.JSON, targetFormat = format) + + printSendJsonBody(json, lines, dtoVar) + + } else if (bodyParam.isTextPlain()) { + + val body = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.TEXT, targetFormat = format) + // handle body only if it is not black + if (body.isNotBlank()) { + if (body != "\"\"") { + when { + format.isCsharp() -> { + lines.append("new StringContent(\"$body\", Encoding.UTF8, \"${bodyParam.contentType()}\")") } - } else { - when { - format.isCsharp() -> { - lines.append("new StringContent(\"${"""\"\""""}\", Encoding.UTF8, \"${bodyParam.contentType()}\")") - } - format.isPython() -> { + + format.isPython() -> { + if (body.trim().isNullOrBlank()) { lines.add("body = \"\"") + } else { + lines.add("body = $body") } - else -> lines.add(".$send(\"${"""\"\""""}\")") } + + else -> lines.add(".$send($body)") } - } + } else { + when { + format.isCsharp() -> { + lines.append("new StringContent(\"${"""\"\""""}\", Encoding.UTF8, \"${bodyParam.contentType()}\")") + } - //BMR: this is needed because, if the string is empty, it causes a 400 (bad request) code on the test end. - // inserting \"\" should prevent that problem - // TODO: get some tests done of this + format.isPython() -> { + lines.add("body = \"\"") + } - } else if (bodyParam.isForm()) { - val body = bodyParam.gene.getValueAsPrintableString( - mode = GeneUtils.EscapeMode.X_WWW_FORM_URLENCODED, - targetFormat = format - ) - when { - format.isCsharp() -> { - lines.append("new StringContent(\"$body\", Encoding.UTF8, \"${bodyParam.contentType()}\")") - } - format.isPython() -> { - lines.add("body = \"$body\"") + else -> lines.add(".$send(\"${"""\"\""""}\")") } - else -> lines.add(".$send(\"$body\")") } - } else if (bodyParam.isXml()) { + } - val xml = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML, targetFormat = format) - // Escape quotes for string literal in generated code - val escapedXml = xml.replace("\\", "\\\\").replace("\"", "\\\"") + //BMR: this is needed because, if the string is empty, it causes a 400 (bad request) code on the test end. + // inserting \"\" should prevent that problem + // TODO: get some tests done of this - when { - format.isPython() -> { - lines.add("body = \"$escapedXml\"") - } - else -> lines.add(".$send(\"$escapedXml\")") + } else if (bodyParam.isForm()) { + val body = bodyParam.gene.getValueAsPrintableString( + mode = GeneUtils.EscapeMode.X_WWW_FORM_URLENCODED, + targetFormat = format + ) + when { + format.isCsharp() -> { + lines.append("new StringContent(\"$body\", Encoding.UTF8, \"${bodyParam.contentType()}\")") } - } else { - LoggingUtil.uniqueWarn(log, "Unhandled type for body payload: " + bodyParam.contentType()) + + format.isPython() -> { + lines.add("body = \"$body\"") + } + + else -> lines.add(".$send(\"$body\")") } + } else if (bodyParam.isXml()) { + + val xml = bodyParam.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML, targetFormat = format) + // Escape quotes for string literal in generated code + val escapedXml = xml.replace("\\", "\\\\").replace("\"", "\\\"") + + when { + format.isPython() -> { + lines.add("body = \"$escapedXml\"") + } + + else -> lines.add(".$send(\"$escapedXml\")") + } + } else { + LoggingUtil.uniqueWarn(log, "Unhandled type for body payload: " + bodyParam.contentType()) } + } fun printSendJsonBody(json: String, lines: Lines, dtoVar: String? = null) { diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt index ea39105dd7..c478e49640 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/fitness/AbstractRestFitness.kt @@ -1103,37 +1103,22 @@ abstract class AbstractRestFitness : HttpWsFitness() { ) } else if (forms != null) { Entity.entity(forms, MediaType.APPLICATION_FORM_URLENCODED_TYPE) - } else if (a.verb == HttpVerb.PUT || a.verb == HttpVerb.PATCH) { - /* - PUT and PATCH must have a payload. But it might happen that it is missing in the Swagger schema - when objects like WebRequest are used. - */ - Entity.entity("", MediaType.APPLICATION_FORM_URLENCODED_TYPE) - //null //cannot be left null, Jersey crash - } else if (a.verb == HttpVerb.POST) { - /* - POST does not enforce payload (isn't it?). However seen issues with Dotnet that gives - 411 if Content-Length is missing... - */ - //builder.header("Content-Length", 0) - // null - /* - yet another critical bug in Jersey that it ignores that header (verified with WireShark) - */ - Entity.entity("", MediaType.APPLICATION_FORM_URLENCODED_TYPE) - //null //cannot be left null, Jersey crash } else { null } - if(bodyEntity != null) { - if(bodyEntity.entity.isEmpty()){ - // Jersey overwrite it... - //builder.header("Content-Type", "") - } else { - builder.header("Content-Type", bodyEntity.mediaType) - } - } + /* + TODO + handling of empty body has been a shit show. + Before, Jersey would crash if left emtpy on PUT/PATCH/POST (which is wrong). + so, had to force empty bodies, which would lead to 415 when wrong type. + but, after upgrading, removing that wrong code was possible, but it leads to another issue: + for some server frameworks, might still expect 'Content-length: 0', which doesn't + seem possible to force in Jersey... :( + in those cases, then some servers might return a 411 :( + this happened when we were supporting C# in WB. + TODO should check if still a problem. if so, should reproduce and try fix + */ val invocation = when (a.verb) { /* diff --git a/pom.xml b/pom.xml index 0c9894a2b6..d48801a861 100644 --- a/pom.xml +++ b/pom.xml @@ -835,7 +835,7 @@ io.rest-assured rest-assured - 4.3.0 + 5.5.7 test