diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/base/HttpInvalidLocationApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/base/HttpInvalidLocationApplication.kt new file mode 100644 index 0000000000..905533f546 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/base/HttpInvalidLocationApplication.kt @@ -0,0 +1,55 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.base + +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.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/resources"]) +@RestController +open class HttpInvalidLocationApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(HttpInvalidLocationApplication::class.java, *args) + } + + private val data = mutableMapOf() + + fun reset(){ + data.clear() + } + } + + + @GetMapping(path = ["/{id}"]) + open fun get(@PathVariable("id") id: Int): ResponseEntity { + val value = data[id] ?: return ResponseEntity.status(404).build() + return ResponseEntity.status(200).body(value) + } + + + @PutMapping(path = ["/{id}"]) + open fun put( + @PathVariable("id") id: Int + ): ResponseEntity { + + val isNew = !data.containsKey(id) + data[id] = "$id" + + // bug: Location header points to a different id than the one + // actually stored, so a follow-up GET on it will return 404 + + val status = if (isNew) 201 else 200 + return ResponseEntity.status(status) + .header("Location", "/api/resources/${id + 1000}") + .build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/fullpath/HttpInvalidLocationFullPathApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/fullpath/HttpInvalidLocationFullPathApplication.kt new file mode 100644 index 0000000000..e3ac1f03aa --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/fullpath/HttpInvalidLocationFullPathApplication.kt @@ -0,0 +1,59 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.fullpath + +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.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.servlet.support.ServletUriComponentsBuilder + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/resources"]) +@RestController +open class HttpInvalidLocationFullPathApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(HttpInvalidLocationFullPathApplication::class.java, *args) + } + + private val data = mutableMapOf() + + fun reset(){ + data.clear() + } + } + + + @GetMapping(path = ["/{id}"]) + open fun get(@PathVariable("id") id: Int): ResponseEntity { + val value = data[id] ?: return ResponseEntity.status(404).build() + return ResponseEntity.status(200).body(value) + } + + + @PutMapping(path = ["/{id}"]) + open fun put( + @PathVariable("id") id: Int + ): ResponseEntity { + + val isNew = !data.containsKey(id) + data[id] = "$id" + + val location = ServletUriComponentsBuilder + .fromCurrentContextPath() + .path("/api/resources/{id}") + .buildAndExpand(id + 1000) + .toUri() + + val status = if (isNew) 201 else 200 + return ResponseEntity.status(status) + .header("Location", location.toString()) + .build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/locationget/HttpInvalidLocationGetApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/locationget/HttpInvalidLocationGetApplication.kt new file mode 100644 index 0000000000..9dc862efa5 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/locationget/HttpInvalidLocationGetApplication.kt @@ -0,0 +1,39 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.locationget + +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.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/resources"]) +@RestController +open class HttpInvalidLocationGetApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(HttpInvalidLocationGetApplication::class.java, *args) + } + + fun reset(){ + } + } + + + @GetMapping(path = ["/{id}"]) + open fun get(@PathVariable("id") id: Int): ResponseEntity { + return ResponseEntity.status(200).header("Location", "/api/resources/${id + 1000}/notfound").body("Resource with id $id") + } + + @GetMapping(path = ["/{id}/notfound"]) + open fun getNotFound(@PathVariable("id") id: Int): ResponseEntity { + return ResponseEntity.status(404).body("Not found") + } + +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/notvalidpath/HttpInvalidLocationNotValidApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/notvalidpath/HttpInvalidLocationNotValidApplication.kt new file mode 100644 index 0000000000..7d4eb2da18 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/notvalidpath/HttpInvalidLocationNotValidApplication.kt @@ -0,0 +1,55 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.notvalidpath + +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.PathVariable +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/resources"]) +@RestController +open class HttpInvalidLocationNotValidApplication { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(HttpInvalidLocationNotValidApplication::class.java, *args) + } + + private val data = mutableMapOf() + + fun reset(){ + data.clear() + } + } + + + @GetMapping(path = ["/{id}"]) + open fun get(@PathVariable("id") id: Int): ResponseEntity { + val value = data[id] ?: return ResponseEntity.status(404).build() + return ResponseEntity.status(200).body(value) + } + + + @PutMapping(path = ["/{id}"]) + open fun put( + @PathVariable("id") id: Int + ): ResponseEntity { + + val isNew = !data.containsKey(id) + data[id] = "$id" + + // bug: Location header points to a different id than the one + // actually stored, so a follow-up GET on it will return 404 + + val status = if (isNew) 201 else 200 + return ResponseEntity.status(status) + .header("Location", "/somePathThatDoesNotExist") + .build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationController.kt new file mode 100644 index 0000000000..ea8ec4537c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationController.kt @@ -0,0 +1,12 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.base.HttpInvalidLocationApplication + + +class HttpInvalidLocationController: SpringController(HttpInvalidLocationApplication::class.java){ + + override fun resetStateOfSUT() { + HttpInvalidLocationApplication.reset() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathController.kt new file mode 100644 index 0000000000..39f5bf17e1 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathController.kt @@ -0,0 +1,13 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.fullpath + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.base.HttpInvalidLocationApplication +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.notvalidpath.HttpInvalidLocationNotValidApplication + + +class HttpInvalidLocationFullPathController: SpringController(HttpInvalidLocationFullPathApplication::class.java){ + + override fun resetStateOfSUT() { + HttpInvalidLocationFullPathApplication.reset() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetController.kt new file mode 100644 index 0000000000..ff2962025c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetController.kt @@ -0,0 +1,13 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.locationget + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.base.HttpInvalidLocationApplication +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.locationget.HttpInvalidLocationGetApplication + + +class HttpInvalidLocationGetController: SpringController(HttpInvalidLocationGetApplication::class.java){ + + override fun resetStateOfSUT() { + HttpInvalidLocationGetApplication.reset() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidController.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidController.kt new file mode 100644 index 0000000000..3670179687 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidController.kt @@ -0,0 +1,13 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.notvalidpath + +import com.foo.rest.examples.spring.openapi.v3.SpringController +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.base.HttpInvalidLocationApplication +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.notvalidpath.HttpInvalidLocationNotValidApplication + + +class HttpInvalidLocationNotValidController: SpringController(HttpInvalidLocationNotValidApplication::class.java){ + + override fun resetStateOfSUT() { + HttpInvalidLocationNotValidApplication.reset() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationEMTest.kt new file mode 100644 index 0000000000..c021f85d61 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationEMTest.kt @@ -0,0 +1,44 @@ +package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.HttpInvalidLocationController +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class HttpInvalidLocationEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(HttpInvalidLocationController()) + } + } + + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "HttpInvalidLocationEM", + 20 + ) { args: MutableList -> + + setOption(args, "security", "false") + setOption(args, "schemaOracles", "false") + setOption(args, "httpOracles", "true") + setOption(args, "useExperimentalOracles", "true") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) + assertTrue({ ExperimentalFaultCategory.HTTP_INVALID_LOCATION in faults }) + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathEMTest.kt new file mode 100644 index 0000000000..0af2da58c9 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathEMTest.kt @@ -0,0 +1,43 @@ +package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.fullpath.HttpInvalidLocationFullPathController +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class HttpInvalidLocationFullPathEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(HttpInvalidLocationFullPathController()) + } + } + + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "HttpInvalidLocationFullPathEM", + 20 + ) { args: MutableList -> + + setOption(args, "security", "false") + setOption(args, "schemaOracles", "false") + setOption(args, "httpOracles", "true") + setOption(args, "useExperimentalOracles", "true") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) + assertTrue({ ExperimentalFaultCategory.HTTP_INVALID_LOCATION in faults }) + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetEMTest.kt new file mode 100644 index 0000000000..3d46d088d4 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetEMTest.kt @@ -0,0 +1,49 @@ +package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.locationget.HttpInvalidLocationGetController +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +// TODO: RestCallAction.creationLocationId() currently restricts location-id generation +// to POST/PUT and throws otherwise, so this branch silently no-ops on other verbs. +// After that restriction is refactored to allow any verb whose response carried a +// Location header, this can be activated +@Disabled +class HttpInvalidLocationGetEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(HttpInvalidLocationGetController()) + } + } + + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "HttpInvalidLocationGetEM", + 20 + ) { args: MutableList -> + + setOption(args, "security", "false") + setOption(args, "schemaOracles", "false") + setOption(args, "httpOracles", "true") + setOption(args, "useExperimentalOracles", "true") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) + assertTrue({ ExperimentalFaultCategory.HTTP_INVALID_LOCATION in faults }) + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidEMTest.kt new file mode 100644 index 0000000000..fd931aea06 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidEMTest.kt @@ -0,0 +1,43 @@ +package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation.notvalidpath.HttpInvalidLocationNotValidController +import org.evomaster.core.problem.enterprise.DetectedFaultUtils +import org.evomaster.core.problem.enterprise.ExperimentalFaultCategory +import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class HttpInvalidLocationNotValidEMTest : SpringTestBase(){ + + companion object { + @BeforeAll + @JvmStatic + fun init() { + initClass(HttpInvalidLocationNotValidController()) + } + } + + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "HttpInvalidLocationNotValidEM", + 20 + ) { args: MutableList -> + + setOption(args, "security", "false") + setOption(args, "schemaOracles", "false") + setOption(args, "httpOracles", "true") + setOption(args, "useExperimentalOracles", "true") + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) + assertTrue({ ExperimentalFaultCategory.HTTP_INVALID_LOCATION in faults }) + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt index 3855ca3368..8e453e1cd7 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/oracle/HttpSemanticsOracle.kt @@ -684,6 +684,47 @@ object HttpSemanticsOracle { return false } + /** + * Checks the invalid-location oracle: + * + * ANY /X -> response with Location header L + * GET L -> 404 (BUG: location does not point to an existing resource) + * + * Sequence checked: the last two main actions of the individual. + * - second-to-last (previous) — any verb — whose result has a non-blank Location header. + * - last is a GET bound to that Location via [RestCallAction.usePreviousLocationId]. + * - last action's response is 404. + */ + fun hasInvalidLocation( + individual: RestIndividual, + actionResults: List + ): Boolean { + + if (individual.size() < 2) return false + + val actions = individual.seeMainExecutableActions() + val previous = actions[actions.size - 2] + val follow = actions[actions.size - 1] + + // follow-up must be a GET, and it must actually be chained to the previous Location + if (follow.verb != HttpVerb.GET) return false + if (follow.usePreviousLocationId.isNullOrBlank()) return false + + // same auth so a 404 cannot be confused with an authorization problem + if (previous.auth.isDifferentFrom(follow.auth)) return false + + val resPrevious = actionResults.find { it.sourceLocalId == previous.getLocalId() } as RestCallResult? + ?: return false + val resFollow = actionResults.find { it.sourceLocalId == follow.getLocalId() } as RestCallResult? + ?: return false + + // the only structural precondition on the previous response is a non-blank Location header + if (resPrevious.getLocation().isNullOrBlank()) return false + + // BUG: a GET on that Location returns 404 + return resFollow.getStatusCode() == 404 + } + private fun parseFormBody(body: String): Map { return body.split("&").mapNotNull { pair -> val parts = pair.split("=", limit = 2) diff --git a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt index 7d426343f4..161d0ba20f 100644 --- a/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt +++ b/core/src/main/kotlin/org/evomaster/core/problem/rest/service/HttpSemanticsService.kt @@ -122,6 +122,10 @@ class HttpSemanticsService : TimeBoxedPhase{ if(hasPhaseTimedOut()) return nonIdempotentPut() + + if(hasPhaseTimedOut()) return + // – invalid location, leading to a 404 when doing a follow up GET + invalidLocation() } /** @@ -552,4 +556,85 @@ class HttpSemanticsService : TimeBoxedPhase{ prepareEvaluateAndSave(ind) } } + + + /** + * HTTP_INVALID_LOCATION oracle: any response carrying a Location header must point + * to a resource that actually exists — a follow-up GET on that Location must not + * return 404. + * + * Sequence built: + * [...] + * ANY /X -> response with Location header L + * GET L -> oracle target: must NOT be 404 + * + */ + private data class LocationCandidate( + val individual: EvaluatedIndividual, + val sourceIndex: Int, + val source: RestCallAction + ) + + /** + * Every action in [ei] whose response carried a non-blank Location header, + * paired with its index in [RestIndividual.seeMainExecutableActions]. + */ + private fun locationCandidatesIn( + ei: EvaluatedIndividual + ): Sequence { + val mainActions = ei.individual.seeMainExecutableActions() + return ei.evaluatedMainActions().asSequence().mapNotNull { ea -> + val a = ea.action as? RestCallAction ?: return@mapNotNull null + val r = ea.result as? RestCallResult ?: return@mapNotNull null + if (r.getLocation().isNullOrBlank()) return@mapNotNull null + val idx = mainActions.indexOfFirst { it.getLocalId() == a.getLocalId() } + if (idx < 0) null else LocationCandidate(ei, idx, a) + } + } + + private fun invalidLocation() { + + val candidates = individualsInSolution.asSequence() + .flatMap { ei -> locationCandidatesIn(ei) } + .groupBy { it.source.verb to it.source.path } + .values + .map { group -> group.minBy { it.individual.individual.size() } } + + for (candidate in candidates) { + + if (hasPhaseTimedOut()) return + + val ind = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction( + candidate.individual.individual, candidate.sourceIndex + ) + val creator = ind.seeMainExecutableActions().last() + + // runtime URL is resolved from the Location header + // (relative/absolute, possibly with query params). We do not bind to a schema + // The path here is a structural placeholder; the real URL comes from chainState. + val getAction = RestCallAction( + id = "GET:LOCATION-FOLLOWUP", + verb = HttpVerb.GET, + path = RestPath("/"), + parameters = mutableListOf(), + auth = creator.auth + ) + getAction.doInitialize(randomness) + getAction.forceNewTaints() + + try { + // TODO: RestCallAction.creationLocationId() currently restricts location-id generation + // to POST/PUT and throws otherwise, so this branch silently no-ops on other verbs. + // After that restriction is refactored to allow any verb whose response carried a + // Location header, this catch can be dropped and the oracle will fire for all verbs. + creator.saveAndLinkLocationTo(getAction) + } catch (e: IllegalArgumentException) { + continue + } + + ind.addMainActionInEmptyEnterpriseGroup(-1, getAction) + + prepareEvaluateAndSave(ind) + } + } } 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 3e30ff6e1c..bed86c7c8c 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 @@ -1332,11 +1332,11 @@ abstract class AbstractRestFitness : HttpWsFitness() { if(config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_REPEATED_CREATE_PUT)) { handleRepeatedCreatePut(individual, actionResults, fv) } - + if(config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_SIDE_EFFECTS_FAILED_MODIFICATION)) { handleFailedModification(individual, actionResults, fv) } - + if(config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_PARTIAL_UPDATE_PUT)) { handlePartialUpdatePut(individual, actionResults, fv) } @@ -1348,6 +1348,10 @@ abstract class AbstractRestFitness : HttpWsFitness() { if(config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_NON_IDEMPOTENT_PUT)) { handleNonIdempotentPut(individual, actionResults, fv) } + + if(config.isEnabledFaultCategory(ExperimentalFaultCategory.HTTP_INVALID_LOCATION)) { + handleInvalidLocation(individual, actionResults, fv) + } } private fun handleFailedModification( @@ -1481,6 +1485,24 @@ abstract class AbstractRestFitness : HttpWsFitness() { ar.addFault(DetectedFault(category, secondPut.getName(), null)) } + private fun handleInvalidLocation( + individual: RestIndividual, + actionResults: List, + fv: FitnessValue + ) { + if (!HttpSemanticsOracle.hasInvalidLocation(individual, actionResults)) return + + val actions = individual.seeMainExecutableActions() + val creator = actions[actions.size - 2] + + val category = ExperimentalFaultCategory.HTTP_INVALID_LOCATION + val scenarioId = idMapper.handleLocalTarget(idMapper.getFaultDescriptiveId(category, creator.getName())) + fv.updateTarget(scenarioId, 1.0, actions.size - 2) + + val ar = actionResults.find { it.sourceLocalId == creator.getLocalId() } as RestCallResult? ?: return + ar.addFault(DetectedFault(category, creator.getName(), null)) + } + protected fun recordResponseData(individual: RestIndividual, actionResults: List) {