diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/headerassertions/BBHeaderAssertionsApplication.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/headerassertions/BBHeaderAssertionsApplication.kt new file mode 100644 index 0000000000..218e74c3da --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/main/kotlin/com/foo/rest/examples/bb/headerassertions/BBHeaderAssertionsApplication.kt @@ -0,0 +1,59 @@ +package com.foo.rest.examples.bb.headerassertions + +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.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RestController +@RequestMapping(path = ["/api/headerassertions"]) +open class BBHeaderAssertionsApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(BBHeaderAssertionsApplication::class.java, *args) + } + } + + @GetMapping(path = ["/ok/401"]) + fun ok401(): ResponseEntity{ + CoveredTargets.cover("ok401") + return ResponseEntity.status(401).header("WWW-Authenticate","Basic realm=\"Realm\"").build() + } + + @GetMapping(path = ["/fail/401"]) + fun fail401(): ResponseEntity{ + CoveredTargets.cover("fail401") + return ResponseEntity.status(401).build() + } + + @GetMapping(path = ["/ok/405"]) + fun ok405(): ResponseEntity{ + CoveredTargets.cover("ok405") + return ResponseEntity.status(405).header("Allow","POST,OPTIONS").build() + } + + @GetMapping(path = ["/fail/405"]) + fun fail405(): ResponseEntity{ + CoveredTargets.cover("fail405") + return ResponseEntity.status(405).build() + } + + @GetMapping(path = ["/ok/426"]) + fun ok426(): ResponseEntity{ + CoveredTargets.cover("ok426") + return ResponseEntity.status(426).header("Upgrade","foo \"bar\" \$x").build() + } + + @GetMapping(path = ["/fail/426"]) + fun fail426(): ResponseEntity{ + CoveredTargets.cover("fail426") + return ResponseEntity.status(426).build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/headerassertions/BBHeaderAssertionsController.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/headerassertions/BBHeaderAssertionsController.kt new file mode 100644 index 0000000000..1850112235 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/com/foo/rest/examples/bb/headerassertions/BBHeaderAssertionsController.kt @@ -0,0 +1,5 @@ +package com.foo.rest.examples.bb.headerassertions + +import com.foo.rest.examples.bb.SpringController + +class BBHeaderAssertionsController : SpringController(BBHeaderAssertionsApplication::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/headerassertions/BBHeaderAssertionsEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/headerassertions/BBHeaderAssertionsEMTest.kt new file mode 100644 index 0000000000..0b96483a1c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-bb/src/test/kotlin/org/evomaster/e2etests/spring/rest/bb/headerassertions/BBHeaderAssertionsEMTest.kt @@ -0,0 +1,83 @@ +package org.evomaster.e2etests.spring.rest.bb.headerassertions + +import com.foo.rest.examples.bb.headerassertions.BBHeaderAssertionsController +import org.evomaster.core.output.OutputFormat +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory +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 BBHeaderAssertionsEMTest : SpringTestBase() { + + companion object { + init { + shouldApplyInstrumentation = false + } + + @BeforeAll + @JvmStatic + fun init() { + initClass(BBHeaderAssertionsController()) + } + } + + + + @ParameterizedTest + @EnumSource + fun testBlackBoxOutput(outputFormat: OutputFormat) { + + executeAndEvaluateBBTest( + outputFormat, + "headerassertions", + 100, + 3, + listOf("ok401","fail401","ok405","fail405","ok426","fail426") + ){ args: MutableList -> + + setOption(args, "useExperimentalOracles", "true") + setOption(args, "statusOracles", "true") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + val prefix = "/api/headerassertions" + + assertHasAtLeastOne(solution, HttpVerb.GET, 401, "$prefix/ok/401", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 401, "$prefix/fail/401", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 405, "$prefix/ok/405", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 405, "$prefix/fail/405", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 426, "$prefix/ok/426", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 426, "$prefix/fail/426", null) + + val faults = DetectedFaultUtils.getDetectedFaults(solution) + assertTrue(faults.isNotEmpty()) + + assertTrue(faults.none{ + it.operationId == "GET:$prefix/ok/401" + && it.category == ExperimentalFaultCategory.HTTP_STATUS_NO_401_IF_NO_WWW_AUTHENTICATE }) + assertTrue(faults.any{ + it.operationId == "GET:$prefix/fail/401" + && it.category == ExperimentalFaultCategory.HTTP_STATUS_NO_401_IF_NO_WWW_AUTHENTICATE }) + + assertTrue(faults.none{ + it.operationId == "GET:$prefix/ok/405" + && it.category == ExperimentalFaultCategory.HTTP_STATUS_NO_405_IF_NO_ALLOW }) + assertTrue(faults.any{ + it.operationId == "GET:$prefix/fail/405" + && it.category == ExperimentalFaultCategory.HTTP_STATUS_NO_405_IF_NO_ALLOW }) + + assertTrue(faults.none{ + it.operationId == "GET:$prefix/ok/426" + && it.category == ExperimentalFaultCategory.HTTP_STATUS_NO_426_IF_NO_UPGRADE }) + assertTrue(faults.any{ + it.operationId == "GET:$prefix/fail/426" + && it.category == ExperimentalFaultCategory.HTTP_STATUS_NO_426_IF_NO_UPGRADE }) + } + } +} 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 701a45417d..fc5b0f7827 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 @@ -819,6 +819,52 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { //---------------------------------------------------------------------------------------- // assertion lines + protected fun addHeaderAssertions(lines: Lines, res: HttpWsCallResult, responseVariableName: String?){ + + val status = res.getStatusCode() + + //TODO: verb order in Allow header is flaky + val allow = res.getAllow() //could had rather checked if was OPTIONS, but we don't have that info as input here + if(!allow.isNullOrBlank() || status == 405){ + addAssertionOnHeader(lines, "allow", res.getHeader("allow"), true, responseVariableName) + } + if(status == 401){ + addAssertionOnHeader(lines, "www-authenticate", res.getHeader("www-authenticate"), false, responseVariableName) + } + if(status == 426) { + addAssertionOnHeader(lines, "upgrade", res.getHeader("upgrade"), false, responseVariableName) + } + } + + protected fun addAssertionOnHeader(lines: Lines, name: String, value: String?, flaky: Boolean, responseVariableName: String?){ + + val instruction = + if(value != null) { + val escaped = GeneUtils.applyEscapes(value, GeneUtils.EscapeMode.ASSERTION, format) + when { + format.isJavaOrKotlin() -> ".header(\"$name\", \"$escaped\")" + format.isJavaScript() -> + "expect($responseVariableName.header[\"$name\"].startsWith(\"$escaped\")).toBe(true);" + format.isPython() -> "assert \"$escaped\" in $responseVariableName.headers[\"$name\"]" + else -> throw IllegalStateException("Unsupported format $format") + } + } else { + when { + format.isJavaOrKotlin() -> ".header(\"$name\", isEmptyOrNullString())" + format.isJavaScript() -> + "expect($responseVariableName.header[\"$name\"]).toBeUndefined();" + format.isPython() -> "assert \"$name\" not in $responseVariableName.headers" + else -> throw IllegalStateException("Unsupported format $format") + } + } + + if(flaky) { + lines.addSingleCommentLine(instruction) + } else { + lines.add(instruction) + } + } + protected fun handleResponseAssertions(lines: Lines, res: HttpWsCallResult, responseVariableName: String?) { assert(responseVariableName != null || format.isJavaOrKotlin()) @@ -837,19 +883,7 @@ abstract class HttpWsTestCaseWriter : ApiTestCaseWriter() { lines.add(".assertThat()") } - val allow = res.getAllow() - if(!allow.isNullOrBlank()){ - val instruction = when { - format.isJavaOrKotlin() -> ".header(\"Allow\", \"$allow\")" - format.isJavaScript() -> - "expect($responseVariableName.header[\"allow\"].startsWith(\"$allow\")).toBe(true);" - format.isPython() -> "assert \"$allow\" in $responseVariableName.headers[\"allow\"]" - else -> throw IllegalStateException("Unsupported format $format") - } - //lines.add(instruction) - //TODO: verb order in Allow header is flaky - lines.addSingleCommentLine(instruction) - } + addHeaderAssertions(lines, res, responseVariableName) if (res.getTooLargeBody()) { lines.addSingleCommentLine("the response payload was too large, above the threshold of ${config.maxResponseByteSize} bytes." +