From 41c2aa60489b1322c7e7aacc305ba51274fb0835 Mon Sep 17 00:00:00 2001 From: Omur Date: Thu, 21 May 2026 13:36:12 +0300 Subject: [PATCH 1/5] invalid location test application --- .../HttpInvalidLocationApplication.kt | 49 +++++++++++++++++++ .../HttpInvalidLocationController.kt | 11 +++++ .../HttpInvalidLocationEMTest.kt | 46 +++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationEMTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt new file mode 100644 index 0000000000..5b7b81605c --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt @@ -0,0 +1,49 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation + +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.* + + +@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 { + + data[id] = "Data for $id" + + // bug: Location header points to a different id than the one + // actually stored, so a follow-up GET on it will return 404 + return ResponseEntity.status(201) + .header("Location", "/api/resources/${id + 1000}") + .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..fa254d9290 --- /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,11 @@ +package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.SpringController + + +class HttpInvalidLocationController: SpringController(HttpOracleRepeatedPutApplication::class.java){ + + override fun resetStateOfSUT() { + HttpOracleRepeatedPutApplication.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..efe4a4408d --- /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,46 @@ +package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidlocation + +import com.foo.rest.examples.spring.openapi.v3.httporacle.repeatedput.HttpOracleRepeatedPutController +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.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(HttpOracleRepeatedPutController()) + } + } + + + @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) + assertEquals(1, faults.size) + assertEquals(ExperimentalFaultCategory.HTTP_INVALID_LOCATION, faults.first()) + } + } +} From 607b464ca5519a549e1d356c5aa553861eed2b19 Mon Sep 17 00:00:00 2001 From: Omur Date: Thu, 21 May 2026 13:52:23 +0300 Subject: [PATCH 2/5] rename --- .../invalidlocation/HttpInvalidLocationController.kt | 4 ++-- .../httporacle/invalidlocation/HttpInvalidLocationEMTest.kt | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) 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 index fa254d9290..75cede5b7e 100644 --- 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 @@ -3,9 +3,9 @@ package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation import com.foo.rest.examples.spring.openapi.v3.SpringController -class HttpInvalidLocationController: SpringController(HttpOracleRepeatedPutApplication::class.java){ +class HttpInvalidLocationController: SpringController(HttpInvalidLocationApplication::class.java){ override fun resetStateOfSUT() { - HttpOracleRepeatedPutApplication.reset() + HttpInvalidLocationApplication.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 index efe4a4408d..9734684020 100644 --- 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 @@ -1,9 +1,8 @@ package org.evomaster.e2etests.spring.openapi.v3.httporacle.invalidlocation -import com.foo.rest.examples.spring.openapi.v3.httporacle.repeatedput.HttpOracleRepeatedPutController +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.core.problem.rest.data.HttpVerb import org.evomaster.e2etests.spring.openapi.v3.SpringTestBase import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -16,7 +15,7 @@ class HttpInvalidLocationEMTest : SpringTestBase(){ @BeforeAll @JvmStatic fun init() { - initClass(HttpOracleRepeatedPutController()) + initClass(HttpInvalidLocationController()) } } From aa78b27475f6951495bceec9d6db0d22714906a3 Mon Sep 17 00:00:00 2001 From: Omur Date: Thu, 21 May 2026 16:24:00 +0300 Subject: [PATCH 3/5] invalid-location implementation --- .../HttpInvalidLocationApplication.kt | 7 +- .../HttpInvalidLocationEMTest.kt | 3 +- .../rest/oracle/HttpSemanticsOracle.kt | 47 ++++++++++++ .../rest/service/HttpSemanticsService.kt | 75 +++++++++++++++++++ .../service/fitness/AbstractRestFitness.kt | 26 ++++++- 5 files changed, 152 insertions(+), 6 deletions(-) diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt index 5b7b81605c..fd90ef2065 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt +++ b/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt @@ -38,11 +38,14 @@ open class HttpInvalidLocationApplication { @PathVariable("id") id: Int ): ResponseEntity { - data[id] = "Data for $id" + 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 - return ResponseEntity.status(201) + + 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/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 index 9734684020..c021f85d61 100644 --- 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 @@ -38,8 +38,7 @@ class HttpInvalidLocationEMTest : SpringTestBase(){ assertTrue(solution.individuals.size >= 1) val faults = DetectedFaultUtils.getDetectedFaultCategories(solution) - assertEquals(1, faults.size) - assertEquals(ExperimentalFaultCategory.HTTP_INVALID_LOCATION, faults.first()) + 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..9e9142d6f7 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,53 @@ object HttpSemanticsOracle { return false } + /** + * Checks the invalid-location oracle: + * + * POST|PUT /X -> 2xx 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 (creator) is POST or PUT, has a 2xx status, and its result has a + * non-blank Location header. + * - last is a GET bound to the creator's saved 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 creator = actions[actions.size - 2] + val follow = actions[actions.size - 1] + + // creator must be a potential resource-creating verb (POST/PUT) + if (!creator.isPotentialActionForCreation()) return false + + // follow-up must be a GET, and it must actually be chained to the creator's Location + if (follow.verb != HttpVerb.GET) return false + val expectedLocId = try { creator.creationLocationId() } catch (e: Exception) { return false } + if (follow.usePreviousLocationId != expectedLocId) return false + + // same auth so a 404 cannot be confused with an authorization problem + if (creator.auth.isDifferentFrom(follow.auth)) return false + + val resCreator = actionResults.find { it.sourceLocalId == creator.getLocalId() } as RestCallResult? + ?: return false + val resFollow = actionResults.find { it.sourceLocalId == follow.getLocalId() } as RestCallResult? + ?: return false + + // creator must have succeeded and returned a Location header + if (!StatusGroup.G_2xx.isInGroup(resCreator.getStatusCode())) return false + if (resCreator.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..5b7a9c58c1 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,75 @@ class HttpSemanticsService : TimeBoxedPhase{ prepareEvaluateAndSave(ind) } } + + + /** + * HTTP_INVALID_LOCATION oracle: a POST/PUT that returns 2xx with a Location header + * must point to a resource that actually exists — a follow-up GET on that Location + * must not return 404. + * + * Sequence built: + * [...] + * POST|PUT /X -> 2xx with Location header L + * GET L -> oracle target: must NOT be 404 + * + */ + private fun invalidLocation() { + + val creatorVerbs = listOf(HttpVerb.POST, HttpVerb.PUT) + + for (verb in creatorVerbs) { + + val creatorOps = RestIndividualSelectorUtils.getAllActionDefinitions(actionDefinitions, verb) + + creatorOps.forEach { creatorOp -> + + if (hasPhaseTimedOut()) return + + // GET endpoint to follow up on: same path (e.g. PUT /x/{id} -> GET /x/{id}) + // or the closest descendant (e.g. POST /x -> GET /x/{id}) + val getDef = actionDefinitions.find { it.verb == HttpVerb.GET && it.path == creatorOp.path } + ?: actionDefinitions + .filter { it.verb == HttpVerb.GET && creatorOp.path.isSameOrAncestorOf(it.path) } + .minByOrNull { it.path.levels() } + ?: return@forEach + + // pick the smallest individual where this creator returned 2xx AND a Location + val candidate = RestIndividualSelectorUtils.findIndividuals( + individualsInSolution, verb, creatorOp.path, statusGroup = StatusGroup.G_2xx + ).filter { ei -> + ei.evaluatedMainActions().any { ea -> + val a = ea.action as? RestCallAction ?: return@any false + val r = ea.result as? RestCallResult ?: return@any false + a.verb == verb && a.path.isEquivalent(creatorOp.path) + && StatusGroup.G_2xx.isInGroup(r.getStatusCode()) + && !r.getLocation().isNullOrBlank() + } + }.minByOrNull { it.individual.size() } ?: return@forEach + + val ind = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction( + candidate, verb, creatorOp.path, statusGroup = StatusGroup.G_2xx + ) + + val creator = ind.seeMainExecutableActions().last() + + // Build the follow-up GET and force Location chaining. + val getAction = getDef.copy() as RestCallAction + getAction.resetLocalIdRecursively() + if (getAction.isInitialized()) { + getAction.seeTopGenes().forEach { it.randomize(randomness, false) } + } else { + getAction.doInitialize(randomness) + } + getAction.auth = creator.auth + getAction.forceNewTaints() + + creator.saveAndLinkLocationTo(getAction) + + 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 bd5deb6a39..d0a9e74af5 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 @@ -1323,11 +1323,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) } @@ -1339,6 +1339,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( @@ -1472,6 +1476,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) { From 8d78fe92f7fe831c58638c8d404ae321ed6e22c8 Mon Sep 17 00:00:00 2001 From: Omur Date: Sun, 31 May 2026 16:15:50 +0300 Subject: [PATCH 4/5] handle with chain state --- .../HttpInvalidLocationApplication.kt | 9 +- .../HttpInvalidLocationFullPathApplication.kt | 59 ++++++++++ .../HttpInvalidLocationNotValidApplication.kt | 55 +++++++++ .../HttpInvalidLocationController.kt | 1 + .../HttpInvalidLocationFullPathController.kt | 13 +++ .../HttpInvalidLocationNotValidController.kt | 13 +++ .../HttpInvalidLocationFullPathEMTest.kt | 43 +++++++ .../HttpInvalidLocationNotValidEMTest.kt | 43 +++++++ .../rest/oracle/HttpSemanticsOracle.kt | 36 +++--- .../rest/service/HttpSemanticsService.kt | 110 ++++++++++-------- 10 files changed, 310 insertions(+), 72 deletions(-) rename core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/{ => base}/HttpInvalidLocationApplication.kt (82%) create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/fullpath/HttpInvalidLocationFullPathApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/notvalidpath/HttpInvalidLocationNotValidApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationFullPathEMTest.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationNotValidEMTest.kt diff --git a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/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 similarity index 82% rename from core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationApplication.kt rename to core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/base/HttpInvalidLocationApplication.kt index fd90ef2065..905533f546 100644 --- a/core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/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 @@ -1,11 +1,14 @@ -package com.foo.rest.examples.spring.openapi.v3.httporacle.invalidlocation +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.* - +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"]) 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/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 index 75cede5b7e..ea8ec4537c 100644 --- 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 @@ -1,6 +1,7 @@ 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){ 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/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/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/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 9e9142d6f7..3f3d38d34b 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 @@ -687,13 +687,12 @@ object HttpSemanticsOracle { /** * Checks the invalid-location oracle: * - * POST|PUT /X -> 2xx with Location header L - * GET L -> 404 (BUG: location does not point to an existing resource) + * 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 (creator) is POST or PUT, has a 2xx status, and its result has a - * non-blank Location header. - * - last is a GET bound to the creator's saved Location via [RestCallAction.usePreviousLocationId]. + * - 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( @@ -704,28 +703,29 @@ object HttpSemanticsOracle { if (individual.size() < 2) return false val actions = individual.seeMainExecutableActions() - val creator = actions[actions.size - 2] - val follow = actions[actions.size - 1] + val previous = actions[actions.size - 2] + val follow = actions[actions.size - 1] - // creator must be a potential resource-creating verb (POST/PUT) - if (!creator.isPotentialActionForCreation()) return false - - // follow-up must be a GET, and it must actually be chained to the creator's Location + // follow-up must be a GET, and it must actually be chained to the previous Location if (follow.verb != HttpVerb.GET) return false - val expectedLocId = try { creator.creationLocationId() } catch (e: Exception) { return false } + if (follow.usePreviousLocationId.isNullOrBlank()) return false + // 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. + val expectedLocId = try { previous.creationLocationId() } catch (e: Exception) { return false } if (follow.usePreviousLocationId != expectedLocId) return false // same auth so a 404 cannot be confused with an authorization problem - if (creator.auth.isDifferentFrom(follow.auth)) return false + if (previous.auth.isDifferentFrom(follow.auth)) return false - val resCreator = actionResults.find { it.sourceLocalId == creator.getLocalId() } as RestCallResult? + val resPrevious = actionResults.find { it.sourceLocalId == previous.getLocalId() } as RestCallResult? ?: return false - val resFollow = actionResults.find { it.sourceLocalId == follow.getLocalId() } as RestCallResult? + val resFollow = actionResults.find { it.sourceLocalId == follow.getLocalId() } as RestCallResult? ?: return false - // creator must have succeeded and returned a Location header - if (!StatusGroup.G_2xx.isInGroup(resCreator.getStatusCode())) return false - if (resCreator.getLocation().isNullOrBlank()) 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 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 5b7a9c58c1..d7c44d1c8c 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 @@ -559,72 +559,80 @@ class HttpSemanticsService : TimeBoxedPhase{ /** - * HTTP_INVALID_LOCATION oracle: a POST/PUT that returns 2xx with a Location header - * must point to a resource that actually exists — a follow-up GET on that Location - * must not return 404. + * 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: * [...] - * POST|PUT /X -> 2xx with Location header L - * GET L -> oracle target: must NOT be 404 + * ANY /X -> response with Location header L + * GET L -> oracle target: must NOT be 404 * */ - private fun invalidLocation() { - - val creatorVerbs = listOf(HttpVerb.POST, HttpVerb.PUT) - - for (verb in creatorVerbs) { + private data class LocationCandidate( + val individual: EvaluatedIndividual, + val sourceIndex: Int, + val source: RestCallAction + ) - val creatorOps = RestIndividualSelectorUtils.getAllActionDefinitions(actionDefinitions, verb) - - creatorOps.forEach { creatorOp -> + /** + * 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) + } + } - if (hasPhaseTimedOut()) return + private fun invalidLocation() { - // GET endpoint to follow up on: same path (e.g. PUT /x/{id} -> GET /x/{id}) - // or the closest descendant (e.g. POST /x -> GET /x/{id}) - val getDef = actionDefinitions.find { it.verb == HttpVerb.GET && it.path == creatorOp.path } - ?: actionDefinitions - .filter { it.verb == HttpVerb.GET && creatorOp.path.isSameOrAncestorOf(it.path) } - .minByOrNull { it.path.levels() } - ?: return@forEach + 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() } } - // pick the smallest individual where this creator returned 2xx AND a Location - val candidate = RestIndividualSelectorUtils.findIndividuals( - individualsInSolution, verb, creatorOp.path, statusGroup = StatusGroup.G_2xx - ).filter { ei -> - ei.evaluatedMainActions().any { ea -> - val a = ea.action as? RestCallAction ?: return@any false - val r = ea.result as? RestCallResult ?: return@any false - a.verb == verb && a.path.isEquivalent(creatorOp.path) - && StatusGroup.G_2xx.isInGroup(r.getStatusCode()) - && !r.getLocation().isNullOrBlank() - } - }.minByOrNull { it.individual.size() } ?: return@forEach + for (candidate in candidates) { - val ind = RestIndividualBuilder.sliceAllCallsInIndividualAfterAction( - candidate, verb, creatorOp.path, statusGroup = StatusGroup.G_2xx - ) - - val creator = ind.seeMainExecutableActions().last() + if (hasPhaseTimedOut()) return - // Build the follow-up GET and force Location chaining. - val getAction = getDef.copy() as RestCallAction - getAction.resetLocalIdRecursively() - if (getAction.isInitialized()) { - getAction.seeTopGenes().forEach { it.randomize(randomness, false) } - } else { - getAction.doInitialize(randomness) - } - getAction.auth = creator.auth - getAction.forceNewTaints() + 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() + // TODO: saveCreatedResourceLocation is restricted to POST/PUT; skip other verbs. + // After refactoring RestCallAction.creationLocationId() we should update here. + try { creator.saveAndLinkLocationTo(getAction) + } catch (e: IllegalArgumentException) { + continue + } - ind.addMainActionInEmptyEnterpriseGroup(-1, getAction) + ind.addMainActionInEmptyEnterpriseGroup(-1, getAction) - prepareEvaluateAndSave(ind) - } + prepareEvaluateAndSave(ind) } } } From fb760995d57d5250b0974d8fe70698e77a1bce16 Mon Sep 17 00:00:00 2001 From: Omur Date: Wed, 3 Jun 2026 16:04:54 +0300 Subject: [PATCH 5/5] add disabled get example --- .../HttpInvalidLocationGetApplication.kt | 39 +++++++++++++++ .../HttpInvalidLocationGetController.kt | 13 +++++ .../HttpInvalidLocationGetEMTest.kt | 49 +++++++++++++++++++ .../rest/oracle/HttpSemanticsOracle.kt | 6 --- .../rest/service/HttpSemanticsService.kt | 6 ++- 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/main/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/locationget/HttpInvalidLocationGetApplication.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/com/foo/rest/examples/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetController.kt create mode 100644 core-tests/e2e-tests/spring/spring-rest-openapi-v3/src/test/kotlin/org/evomaster/e2etests/spring/openapi/v3/httporacle/invalidlocation/HttpInvalidLocationGetEMTest.kt 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/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/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/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 3f3d38d34b..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 @@ -709,12 +709,6 @@ object HttpSemanticsOracle { // 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 - // 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. - val expectedLocId = try { previous.creationLocationId() } catch (e: Exception) { return false } - if (follow.usePreviousLocationId != expectedLocId) return false // same auth so a 404 cannot be confused with an authorization problem if (previous.auth.isDifferentFrom(follow.auth)) return false 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 d7c44d1c8c..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 @@ -622,9 +622,11 @@ class HttpSemanticsService : TimeBoxedPhase{ getAction.doInitialize(randomness) getAction.forceNewTaints() - // TODO: saveCreatedResourceLocation is restricted to POST/PUT; skip other verbs. - // After refactoring RestCallAction.creationLocationId() we should update here. 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